124 lines
3.2 KiB
Ruby
124 lines
3.2 KiB
Ruby
##
|
|
# This module requires Metasploit: https://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Post::Windows::LDAP
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Windows Gather Active Directory Service Principal Names',
|
|
'Description' => %q{
|
|
This module will enumerate servicePrincipalName in the default AD directory
|
|
where the user is a member of the Domain Admins group.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => [
|
|
'Ben Campbell', # Metasploit Module
|
|
'Scott Sutherland' # Original Powershell Code
|
|
],
|
|
'Platform' => [ 'win' ],
|
|
'SessionTypes' => [ 'meterpreter' ],
|
|
'References' => [
|
|
['URL', 'https://www.netspi.com/blog/entryid/214/faster-domain-escalation-using-ldap'],
|
|
]
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptString.new('FILTER', [true, 'Search filter, DOM_REPL will be automatically replaced', '(&(objectCategory=user)(memberOf=CN=Domain Admins,CN=Users,DOM_REPL))'])
|
|
])
|
|
|
|
deregister_options('FIELDS')
|
|
end
|
|
|
|
def run
|
|
domain ||= datastore['DOMAIN']
|
|
domain ||= get_domain
|
|
|
|
fields = ['cn', 'servicePrincipalName']
|
|
|
|
search_filter = datastore['FILTER']
|
|
max_search = datastore['MAX_SEARCH']
|
|
|
|
# This needs checking against LDAP improvements PR.
|
|
dn = get_default_naming_context(domain)
|
|
|
|
if dn.blank?
|
|
fail_with(Failure::Unknown, 'Unable to retrieve the Default Naming Context')
|
|
end
|
|
|
|
search_filter.gsub!('DOM_REPL', dn)
|
|
|
|
begin
|
|
q = query(search_filter, max_search, fields, domain)
|
|
rescue RuntimeError => e
|
|
# Raised when the default naming context isn't specified as distinguished name
|
|
print_error(e.message)
|
|
return
|
|
end
|
|
|
|
if q.nil? || q[:results].empty?
|
|
return
|
|
end
|
|
|
|
fields << 'Service'
|
|
fields << 'Host'
|
|
|
|
# Results table holds raw string data
|
|
results_table = Rex::Text::Table.new(
|
|
'Header' => 'Service Principal Names',
|
|
'Indent' => 1,
|
|
'SortIndex' => -1,
|
|
'Columns' => ['cn', 'Service', 'Host']
|
|
)
|
|
|
|
q[:results].each do |result|
|
|
rows = parse_result(result, fields)
|
|
next if rows.nil?
|
|
|
|
rows.each do |row|
|
|
results_table << row
|
|
end
|
|
end
|
|
|
|
print_line results_table.to_s
|
|
stored_path = store_loot('ad.computers', 'text/plain', session, results_table.to_csv)
|
|
print_good("Results saved to: #{stored_path}")
|
|
end
|
|
|
|
def parse_result(result, fields)
|
|
rows = []
|
|
row = []
|
|
|
|
0.upto(fields.length - 1) do |i|
|
|
field = (result[i][:value].nil? ? '' : result[i][:value])
|
|
|
|
if fields[i] == 'servicePrincipalName'
|
|
break if field.blank?
|
|
|
|
spns = field.split(',')
|
|
spns.each do |spn|
|
|
new_row = row.dup
|
|
split = spn.split('/')
|
|
if split.length == 2
|
|
new_row << split[0]
|
|
new_row << split[1]
|
|
rows << new_row
|
|
else
|
|
print_error("Invalid SPN: #{field}")
|
|
end
|
|
end
|
|
else
|
|
row << field
|
|
end
|
|
end
|
|
|
|
rows
|
|
end
|
|
end
|