ASIS CTF Finals 2023
To cap off the fantastic year of 2023, I participated with the SKSD team in the ASIS CTF Finals 2023, where we secured 8th place. I extend a huge shoutout and heartfelt thanks to my amazing teammates for their exceptional teamwork throughout the event!
Below is the write-up for the QuickJS pwn challenge named isWebP.js
. This challenge is a JavaScript engine pwn task, inspired by CVE-2023-4863. This challenge was solved by only three teams, and I was the third to successfully solve it.
Pwn
isWebP.js
Did you know that Javascript doesn’t have an image validator by default? That’s why I decided to implement something like isNaN for images (isPNG, isJpeg, isWebP). So far I’ve only implemented isWebP and I wanna do a beta test before submitting the proposal to ECMA. You can also test the feature via the address below. Can’t wait for your feedbacks!
nc 5.75.200.150 1337
Initial Analysis
In this challenge, we received an attachment that included the modified quickjs
, the altered libwebp
, and a compile.sh
script for compiling them. Let’s first take a look at the compile.sh
file.
|
|
Observing the script reveals that the libwebp
version used is vulnerable to the recent exploit CVE-2023-4863. This CVE details an issue in libwebp
related to constructing the Huffman Table, essential for compressing WebP images. WebP images use Huffman Coding for compression, and libwebp
constructs a Huffman Table to decompress them. However, due to inadequate validity checks, it’s possible to craft a malicious WebP image that causes heap overflow during table construction.
In search of a proof of concept (POC) online, we discovered a good POC containing a sample malicious WebP image. Before utilizing the malicious image, let’s first check the patches implemented by the author.
Examining the libwebp.patch
file, the only modification made by the author is the removal of the line of code (LOC) that frees the Huffman table. Therefore, in this challenge, once a Huffman table is allocated, it remains indefinitely.
Next, let’s examine the quickjs.patch
file.
|
|
The key aspect of the patches is the introduction of a new function named isWebP
. This function accepts a Uint8Array
representing a WebP image stream and attempts to decode the image. The version of libwebp
in use is the vulnerable one, therefore, invoking this isWebP
function with the malicious image will trigger the heap overflow bug.
Given that the malicious image is already available online, the primary challenge is to craft an exploit that leverages the heap overflow bug to trigger Remote Code Execution (RCE). This is crucial because to retrieve the flag, we must execute the command /readflag
.
Solution
Testing the heap overflow
First, let’s set up our helper.
|
|
Upon reviewing the information in the POC’s blog, we learn that the POC’s malicious image aims to overwrite chunk+0x3000
. Let’s try to check what kind of overflow that it can trigger in the gdb
. My approach to check about it is:
- Spray numerous pair of (
ArrayBuffer
with a size of0x2f28
andArray
with size0x1
). - Free some of these
ArrayBuffer
objects. - Attempt to allocate the Huffman table, hoping it occupies one of the freed
ArrayBuffer
chunks due to the same size. - Inspect the value at
chunk+0x3000
to analyze the effects of the overflow.
|
|
To debug easier, I used a technique from this blog. The trick involves setting a breakpoint at js_math_min_max
. Additionally, we’ll set a breakpoint at DecodeImageStream+935
, where rax
holds the address of huffman_tables
.
After running the aforementioned code and examining the state in gdb
when breaking at js_math_min_max
, we observe that it indeed overwrites huffman_tables+0x3000
with the value 0x30007
.
|
|
Now that we know the heap overflow is indeed triggered, we need to start thinking on how to leverage this to gain RCE.
Gaining read and write primitive
The strategy here leverages the heap overflow to overwrite the length of an existing data type, enabling Out-of-Bounds (OOB) read/write capabilities. After experimenting in gdb to achieve an optimal heap layout, I found the following sequence effective.
|
|
Here’s a brief explanation of my approach: Initially, I spray pairs of ArrayBuffer(0x2fb0)
and Array(1.1, 2.2)
. This spray aims to achieve a layout like this:
|
|
Freeing some of the ArrayBuffer
objects results in a heap layout like this:
|
|
Then, allocating the huffman_tables
via isWebP
(which effectively allocates a chunk of size 0x2f30
) will reuse these free chunks. Consequently, huffman_tables+0x3000
will point to the Array
size and overwrite it with a large value (0x30007
).
To gain a deeper understanding, let’s set a breakpoint right before we spray the huffman_tables
. We can simply insert a Math.min
call just after freeing the ArrayBuffer
to facilitate this. Then, we’ll examine the largebin
list.
|
|
Assume we successfully allocate the huffman_tables
chunk to the address 0x555555e24df0
. Invoking malloc(0x2f28)
will utilize the aforementioned large_bins
chunk, as its size falls within the bin range of 0x2a00
to 0x3000
. In the memory view outlined above, chunk+0x3000
points to the size field of one of our sprayed Array
objects (size is at chunk+0x40
(0x2
), buffer is at chunk+0x38
(0x0000555555e27e40
)). Due to the overflow, this Array
size will be overwritten.
Next, let’s inspect the output of the JavaScript file we crafted.
|
|
The reason entries y[492]
to y[496]
output values other than undefined
, unlike the rest, is due to the successful overwrite of their size
field through the overflow.
Despite successfully achieving an Out-of-Bounds (OOB) write, as you noted, the OOB read isn’t functioning as expected, only displaying [unsupported type]
. To better understand the OOB write, let’s conduct a small experiment:
|
|
Run the above code and inspect the memory layout.
|
|
After examining the memory layout post OOB write, the following observations are made:
- Each element occupies 0x10 bytes.
- Only 4 bytes can be written per 0x10 bytes.
For a stronger write and read primitive, I explored other data types. The Float64Array
emerged as a strong candidate, enabling both OOB read and write, provided its length is overwritten. To inspect its structure, invoke Math.min(float_arr)
, similar to our previous approach with the Array
(ensure to remove the earlier experiment from our file).
|
|
Below is the memory layout:
|
|
The Float64Array
has a layout similar to Array
, with the size at chunk+0x40
(0x2
) and buffer at chunk+0x38
(0x0000555555822890
). Interestingly, the buffer
chunk’s data
isn’t limited per 0x10
bytes, and the OOB read accurately displays the hexadecimal values as floating points.
Our strategy now involves using the OOB write capability from the Array
to overwrite the size of a newly sprayed Float64Array
, turning it into a new primitive for reading and writing. Observed that altering y[496][9]
and y[496][34]
affects one of the sprayed Float64Array
objects size. To identify the affected Float64Array
, we can mark the first element (y[496][3]
and y[496][28]
) with a unique value.
|
|
Examining the output of this code reveals that z[82]
and z[83]
are the Float64Array
instances impacted by our Array
OOB Write. With these new, unrestricted primitives, we no longer need the limited OOB write.
|
|
Leaking values
Next, let’s explore what values can be leaked using z[82]
in gdb
. Simply execute Math.min(z[82])
and inspect the memory values.
|
|
The data
begins at 0x0000555555e387a0
. Notably, data[7]
reveals a heap
address, and data[14]
discloses a libc
address.
|
|
Having successfully leaked values with the OOB Read, we’re now positioned to achieve Remote Code Execution (RCE) utilizing the OOB Write.
Gaining RCE
To achieve Remote Code Execution (RCE), I noticed that a QuickJS object called JSRuntime
stores a list of function pointers named JSMallocFunctions
.
|
|
This object is located in the heap, where:
JSRuntime->mf
is atheap_base+0x2a0
.JSRuntime->malloc_state
is atheap_base+0x2c0
.
Examining the function used to create a new ArrayBuffer
(js_array_buffer_constructor3
), we see it eventually calls rt->mf.js_malloc(&rt->malloc_state, size);
. If we can redirect rt->mf.js_malloc
to the system
function and set rt->malloc_state
to /readflag
, we can trigger RCE.
|
|
Our goal is to perform an OOB write at these locations. We can use the write primitives of Float64Array
for this purpose.
Remember that Float64Array
maintains a pointer to its data
. Observing the memory layout of our corrupted Float64Array
(z[82]
and z[83]
), we see:
|
|
By overwriting data[11]
with our target address, any assignment to data[0]
will modify the content at our target address.
Using this technique, we can manipulate our corrupted arrays to overwrite rt->malloc_state
and rt->mf.js_malloc
. Subsequent allocation of a new ArrayBuffer
will then execute system("/readflag")
.
Here’s the complete exploit code:
|
|
After sending this code to the server, we can retrieve our flag.
|
|
Flag: ASIS{i_hope_think_they_accept_my_proposal-fd23134fd23134}
Social Media
Follow me on twitter