Browser Exploitation Notes
After finishing my FYP Part 1, I finally have some free time to learn browser v8 exploitaiton based on the UTCTF challenge, E-Corp Part 2
Challenge Files
We are given a server file which asks for the size and content of our exploit. It will then call d8 with our exploit.
Dockerfile of the challenge

We are also given a d8 file, which is the standalone binary of the JavaScript engine V8. V8 is the JavaScript engine of Google Chrome.
The args.gn file is a list of arguments that will be used by d8. We can see that the V8 sandbox is disabled. So we don't need to escape the sandbox.
You're looking at a custom patch applied to the V8 engine for UTCTF 2025, which introduces a new built-in function called Array.prototype.confuse
which is defined in builtins-array.c
.
Additional Knowledge
Pointer Tagging
Pointer tagging is a mechanism used to efficiently store and differentiate between different types of data, such as pointers, immediate small integers (Smis), and doubles.
Doubles - actual 64 bit representation without any changes
Pointers - points to objects, the least significant bit is set to 1 (eg: 0x5678 is represented as 0x5679)
SMI - integer that is bitshifted left by 1 bit when in memory (eg: 3 will be stored as 0b110)
Pointer Compression
Typically, pointers are 8 bytes and takes up alot of memory. In pointer compression, the actual pointer values can be represented in 4 bytes. These 4 bytes will store the offset of the object. To calculate the actual address, we can do base_addr + offset
to get the actual address. Example :
Base address = 0x00007fff_00000000
Offset = 0xdeadbeef
Actual address = base + offset = 0x7fff_deadbeef
Helpful Functions
Any address leaks that you get is in double precision floating point since their 64 bit representation is not modified. We would need these js functions to parse them
These DebugPrint() native JS functions that can give you extra information
Debugging
Goal
Most of the time, when exploiting a JavaScript engine, we want the primitives addrof and fakeobj.
With addrof, you can leak the address of an object. With fakeobj, you can create a fake object, allowing you to control it using two variables.
Objects in v8
let a = ["hi", "bye"];
Lets create object a and view it in memory

We can see our JSArray at 0x227c00042b10

In memory, it stores pointers as 32 bit values due to pointer compression. The first value 0x14b8ed corresponds to a MAP
object at 0x227c0014b8ec. Next, 0x725 is our properties. Then, 0x15354d is our elements pointer. Finally, 0x00000004
is the length of our object bitshifted to the left once (original value would be 2).

In V8, every JavaScript object, including arrays, is linked to a Map object. The Map in V8 contains metadata and layout information about the structure of the object. It's essentially the blueprint of how the object is structured and how it should behave.

(remote) gef➤ x/4gx 0x227c0015354d-1
0x227c0015354c: 0x000000040000065d 0x001534ad0015349d
Elements refer to the individual items or values that are stored within the array
Methods of Getting RCE
WASM to RCE
(module
(func (export "main") (result i32)
i32.const 1337
)
)
We can create a sample wasm file like such and then compile it using wat2wasm from the web assembly toolkit.

Then we can use xxd to extract the bytes
const wasmCode = new Uint8Array([...]); // minimal wasm module
const wasmModule = new WebAssembly.Module(wasmCode);
const wasmInstance = new WebAssembly.Instance(wasmModule);
const f = wasmInstance.exports.func;
With the bytes we have, we can put it in the Uint8Array

Now theres a pointer to trusted data

Now, there is a new RWX memory allocated. However, this method did not work for this challenge as the CPU has a feature called pkey
which prevents writing to that new memory region.
JIT to RCE
In V8 (the JavaScript engine), when a function is called many times, it becomes "hot" and the engine tries to speed it up by compiling it using a Just-In-Time (JIT) compiler like Maglev or Turbofan. This compiled code is stored in memory that can be executed (RWX: Read, Write, Execute). So, if you make a function that returns raw bytes (your shellcode) and force V8 to compile it by calling it many times, V8 will place that compiled code — including your shellcode — in executable memory. Then, with a vulnerability, you can hijack the control flow to jump to that memory and run the shellcode, leading to remote code execution (RCE).
for (let i=0; i<999999; i++) {
shell();
};
This function will be placed in a RWX memory region which we will abuse for RCE.
Last updated