Sploitus

Exploit for Type Confusion in Google Chrome

githubexploit Β· 2026-08-20

Exploit Code

README204 lines
## 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).