## https://sploitus.com/exploit?id=5000AE00-F349-51E8-B11F-591961ECEC94
# CVE-2024-4947 β V8 Maglev Type Confusion β Full RCE (d8 PoC)
A complete, self-contained exploitation chain for **CVE-2024-4947** (V8 Maglev type
confusion), from the initial type-confusion trigger all the way to **arbitrary code
execution**. The PoC runs against a **non-sandboxed** `d8` build and prints
`CVE-2024-4947-PWNED` to stdout as proof of code execution.
> β οΈ This is a **research / educational** PoC for a **patched, public** vulnerability.
> It targets a development build of the V8 shell (`d8 --allow-natives-syntax`) β **not**
> a real Chrome browser. See [Disclaimer](#disclaimer).
---
## TL;DR
```console
$ ./v8-build-nosandbox.sh # build the vulnerable d8 (WSL2 Ubuntu, ~10-20 min)
$ d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
[engine] dblData0=0004f470 class=0
[inst] addr=0x001dc274 trusted_data(tagged)=0x00202bd5 td=0x00202bd4
[jt] jump_table_start = 0x00000967426cd000 (external code space)
[bridge] memory0_start -> jt, memory0_size -> huge (r/w reach jt+off)
[slot0] before: e9 3b 08 00 00
[shell] wrote 52 bytes @ jt+0x0100: VERIFIED
CVE-2024-4947-PWNED
$ echo $? # 0
```
The 52-byte shellcode is `write(1, "CVE-2024-4947-PWNED\n", 20); exit_group(0)`.
---
## The vulnerability
CVE-2024-4947 is a **type confusion in the V8 Maglev JIT compiler**, fixed in Chrome
`125.0.6422.60` (commit `b3c01ac1e60a`). It was exploited **in the wild** by the Lazarus
APT group. When Maglev compiles a store to a **module namespace object**
(`JSModuleNamespace`), it uses an incorrect `AccessInfo` β the store is compiled as a
plain `mov [[obj + 4], rax]` that writes a controlled value into the **`map` field** of an
adjacent object instead of going through the proper property-store path.
The exploitation proceeds by:
1. Corrupting an object's map to a **fake `NAME_DICTIONARY_TYPE` (0xB2)** map, which
changes where V8 stores the object's hash.
2. Triggering `new WeakRef(...)` so the hash write lands in an **adjacent object's length
slot** β **out-of-bounds** access.
From there we get the classic V8 toolkit: `addrOf`/`fakeObj`, then arbitrary in-cage
read/write.
---
## Exploit chain
```
CVE-2024-4947 trigger (fake NAME_DICTIONARY map + WeakRef hash write)
βββΊ OOB write ββΊ corrupt doubleArray length
βββΊ in-cage 4/8-byte arbitrary R/W (the "engine")
βββΊ overwrite WasmTrustedInstanceData.memory0_start (+0x18)
& memory0_size (+0x20) β huge
βββΊ wasm load8_u / store8 = clean 64-bit R/W bridge
(no software bounds check in compiled code)
βββΊ read jump_table_start (+0x38) β external code space
βββΊ jump table region is RWX (this build)
βββΊ write shellcode into the slack (jt+0x100)
βββΊ repoint func0's `e9 rel32` slot at it
βββΊ call func0 β shellcode β RCE
```
### Step-by-step
| # | Stage | Detail |
|---|-------|--------|
| 1 | **Trigger** | `opt()` writes a fake map via the confused store; `new WeakRef(m)` OOBs `corruptArray.length`. |
| 2 | **Engine** | `addrOf`/`fakeObj`; corrupt `doubleArray`'s length β arbitrary 4-byte R/W at any 4-aligned caged address. |
| 3 | **64-bit bridge** | `WasmTrustedInstanceData.memory0_start` (+0x18) is a raw 64-bit pointer used by compiled wasm `i32.load8_u`/`i32.store8` with **no software bounds check** (out-of-range hits a guard page β SIGSEGV β wasm trap). Redirect it to any address + set `memory0_size` (+0x20) huge β arbitrary 64-bit R/W. |
| 4 | **Find code space** | `jump_table_start` (+0x38) is a raw 64-bit pointer into the external code space (outside the 4 GB cage). |
| 5 | **Write shellcode** | On this build the jump-table region is **RWX** (V8 patches entries at runtime): write the 52-byte shellcode into the slack at `jt+0x100`. |
| 6 | **Repoint slot** | func0's jump-table slot is a 5-byte `e9 ` (target = slot + 5 + rel32). Set rel32 β `jt+0x100`. |
| 7 | **Trigger dispatch** | `inst.exports.r(0)` β JSToWasmWrapper dispatches via slot 0 β shellcode executes. |
---
## Differences from other public PoCs
Most public CVE-2024-4947 PoCs target the **sandboxed** d8 or a real Chrome renderer
(`v8_enable_sandbox=true`) and stop at the type-confusion / OOB primitive. This PoC
targets a **no-sandbox** d8 and carries the chain all the way to code execution. The
differences that matter:
| Dimension | Common public-PoC approach | This PoC |
|-----------|---------------------------|----------|
| **Target build** | sandboxed d8 / Chrome renderer | `v8_enable_sandbox=false` d8 β trusted pointers are direct, external code space is on |
| **64-bit R/W bridge** | overwrite `JSTypedArray.external_pointer` | That path **crashes when reading the code range** (verified empirically); instead overwrite **`memory0_start`** and use raw wasm load/store |
| **Final code-exec route** | code space is W^X β JIT-spray + indirect redirect | jump-table region is **RWX** β direct shellcode write + `e9 rel32` slot patch |
| **Jump-table slot format** | docs commonly assume `movabs rax, imm64; jmp rax` (12 bytes) | measured: 5-byte `e9 `, target = slot + 5 + rel32 |
| **Field offsets** | sandboxed-build layout | no-sandbox `WasmTrustedInstanceData`: `jump_table_start@+0x38`, `memory0_start@+0x18`, `memory0_size@+0x20`; `WasmInstanceObject.trusted_data@+0x0c` |
| **Clean exit** | β | d8 is multi-threaded: must use `exit_group` (231), not `exit` (60), or the process hangs |
See [docs/walkthrough.md](docs/walkthrough.md) for the full technical write-up, including
the empirical findings behind each row.
---
## Repository layout
```
.
βββ exploit/
β βββ Module.mjs # module namespace object corrupted by the trigger
β βββ exploit_rce.mjs # the full chain (trigger β arbitrary R/W β RCE)
β βββ shellcode.S # assembly source for the 52-byte payload
βββ build/
β βββ v8-build-nosandbox.sh # build the vulnerable no-sandbox d8 from V8 source
βββ docs/
βββ walkthrough.md # deep dive: bridge mechanics, layouts, gotchas
```
The exploit imports `Module.mjs` (the vulnerable module namespace object) from its own
directory, so keep the two files together (or adjust the `import` path).
---
## Build & run
### Requirements
- WSL2 (Ubuntu) or any Linux with a C/C++ toolchain, `git`, and ~10 GB free disk
- Google `depot_tools` (`git clone https://chromium.googlesource.com/chromium/tools/depot_tools`)
- V8 source at the vulnerable revision (see below)
### Build the vulnerable d8
```bash
# 1. fetch V8 at the vulnerable tag (12.4.254.16 is the pre-fix release)
export PATH="$HOME/depot_tools:$PATH"
cd ~/v8w && fetch v8 && cd v8
git checkout 12.4.254.16 # or the commit just before b3c01ac1e60a
# 2. first build a normal release d8 (needed to seed args.gn), then:
./v8-build-nosandbox.sh # copies args.gn and appends v8_enable_sandbox = false
```
`v8-build-nosandbox.sh` uses `ninja -C out.gn/x64.release_nosandbox -j6 d8`.
### Run
```bash
out.gn/x64.release_nosandbox/d8 --allow-natives-syntax --module exploit/exploit_rce.mjs
```
Expected output ends with:
```
CVE-2024-4947-PWNED
```
and the process exits with status 0.
> `--allow-natives-syntax` is required for the `%PrepareFunctionForOptimization` /
> `%OptimizeMaglevOnNextCall` runtime calls. This is why the PoC cannot run in a real
> browser β it is a d8 shell proof-of-concept by design.
---
## Reusable techniques
The genuinely reusable (and non-obvious) pieces for V8 exploitation research:
1. **Wasm memory-redirect 64-bit R/W bridge** β when `JSTypedArray.external_pointer`
redirection fails on a target region, redirect `WasmTrustedInstanceData.memory0_start`
instead. Compiled wasm byte loads/stores carry no software bounds check; the signal
trap handler converts only guard-page faults, so mapped-but-non-writable regions crash
rather than trap.
2. **Jump-table writability probing** β the wasm jump-table region's protection varies by
build. Probe it byte-by-byte with `try/catch`; a wasm "out of bounds" trap means a
guard page (unmapped), a real SIGSEGV means mapped-but-RX.
3. **`e9 rel32` slot format** β 5-byte relative jump, not the `movabs` form assumed by
many write-ups. Verified against `--print-wasm-code` disassembly.
---
## Disclaimer
This repository is provided **for educational and defensive-security research only**. It
demonstrates exploitation of a **patched** vulnerability against a **development-only**
V8 build. It is **not** a weapon against modern Chrome, does not bypass the V8 sandbox,
and requires the non-default `--allow-natives-syntax` flag. The author is not responsible
for any misuse.
## Credits / lineage
The **trigger primitive** (fake `NAME_DICTIONARY_TYPE` map + WeakRef hash write) follows
the public analyses of the in-the-wild Lazarus exploit published after the patch β
including Google's and Exodus Intelligence's CVE-2024-4947 write-ups. The later stages
(jump-table probing, bridge, slot patching) were derived empirically against this specific
build during this work. The JIT-spray fallback concept draws on public V8 exploit
techniques (e.g. the CVE-2024-5830 `make_array` pattern).
## License
MIT β see [LICENSE](LICENSE).