Browser exploitation primer (V8 / JavaScriptCore / Chakra)
TL;DR: Modern browser exploitation = find a bug in the JavaScript engine (V8 in Chrome/Edge; JavaScriptCore in Safari; SpiderMonkey in Firefox) → gain arbitrary read/write in the renderer → escape the sandbox. The JS-engine bug is often a JIT type-confusion or out-of-bounds in JIT-compiled code. After the JS-engine win, you have a renderer with no privileges; escape requires a second bug (windows-sandbox-and-appcontainer-escape). Companion to type-confusion-kernel and hevd-uaf-walkthrough.
Why JavaScript engines are the target
A browser executes attacker JS in every page visit. The JS engine compiles JS to native code (JIT). Bugs in the engine = remote code execution from a website.
Engines:
- V8 — Chrome, Edge, Node.js, Electron. Open-source; most studied.
- JavaScriptCore (JSC) — Safari, Pwn2Own staple.
- Chakra — legacy Edge (replaced by V8 in 2020).
- SpiderMonkey — Firefox.
The “renderer process” model
Modern browsers split work:
- Browser process: trusted, full privileges.
- Renderer processes: one per tab; sandboxed (AppContainer on Windows, seccomp on Linux).
- GPU process, Network process, Utility processes — each sandboxed.
A JS-engine exploit lands in the renderer — which is sandboxed. To pop calc on the user’s machine, you need a second bug to escape the sandbox.
For OSEE / Pwn2Own purposes, the JS bug alone is sometimes the showcase; the chain to sandbox escape adds points.
Bug class 1 — type confusion in JIT
JIT compilers assume types of variables to generate fast code. If the assumption can be invalidated mid-execution while the JIT thinks it’s still valid, the JIT code runs with wrong-type assumption → OOB read/write.
Classic pattern:
1
2
3
4
5
function foo(arr) {
arr[0] = 0; // JIT assumes arr is a number array
arr[1] = "hello"; // wait, this should change the array's element kind
return arr[0]; // JIT-emitted code still treats as number array
}
When the JIT-optimised code is invoked with an array of strings but the JIT didn’t deoptimise, it reads memory expecting integers — gets pointers — confused.
Bug class 2 — bounds-check elimination (BCE)
JIT removes bounds checks when it can prove the index is safe. Bug: prove succeeds via a flaw → JIT removes the check → attacker-controlled index → OOB.
CVE-2019-5786 (V8) — classic BCE bug.
Bug class 3 — incorrect type widening / narrowing
When two paths merge in JIT IR, types are unioned. Bug: union too narrow → JIT uses wrong type for one of the paths.
Bug class 4 — UAF in DOM
Browser DOM is rich; a JS reference to a DOM node can outlive the node if the renderer’s GC and DOM lifecycle disagree. UAF — see use-after-free-kernel pattern but in renderer.
CVE-2020-6418 — V8 UAF.
Building the addrof / fakeobj / arb-r/w primitive chain
Once you have an OOB read/write inside the JS engine, the standard escalation:
- addrof(obj) — given a JS object, return its address as a number.
- fakeobj(addr) — given an address, return a JS object pretending to live there.
- With both → arbitrary read (fakeobj at target address, read its “properties”).
- arbitrary write (set “properties” on the fakeobj).
- Find the address of a JIT-compiled function → overwrite its code → execute attacker code.
1
2
3
4
5
6
7
// Pseudo-code; actual V8 uses different object layouts
let leak_array = new Float64Array(1);
let object_array = [{}];
// Use the bug to swap their internal pointers
// Now reading leak_array[0] gives the address of object_array[0]
let addr = read_as_double(leak_array, 0);
JIT details: V8’s “Sea of Nodes” IR, JSC’s “Air” IR. Knowing these helps craft the trigger.
Stage to RCE
Once arb-r/w in renderer:
- Find a JIT-compiled function (well-known offset from a JS function object).
- Locate its native code page (it’s RWX in older builds; W^X with permission flip on modern).
- Write shellcode into the page (or use an existing gadget if W^X strict).
- Call the JS function → executes shellcode.
Modern V8 ships W^X JIT (V8 trusted-mode): JIT pages are not directly writable from JS arb-write. Bypasses use WASM JIT pages (still RWX in some configs) or escape via gadget chain.
Stage to sandbox escape
The renderer’s shellcode runs in AppContainer with no useful permissions. To pop calc:
- Find a Mojo IPC bug (windows-sandbox-and-appcontainer-escape).
- Send malformed IPC to the browser process.
- Browser process is compromised → has user-level privileges.
- Optionally, chain a kernel bug for SYSTEM.
Full chain (browser-exploit speak):
- “renderer exploit” — JS engine bug → RCE in sandbox.
- “sandbox escape” — IPC bug → renderer-to-browser.
- “kernel exploit” or “EoP” — for full SYSTEM.
Each chain is a 6-12 month research project for top researchers; OSEE doesn’t expect end-to-end.
What’s gone — Chakra, IE
- Internet Explorer was deprecated in 2022.
- Chakra (legacy Edge) was replaced by V8 in 2020.
- Both still in Pwn2Own historic record but not current targets.
Learning sequence
- CTF JIT exploitation — pwn.college, Real World CTF browser categories.
- Read public Pwn2Own writeups for browser entries.
- Build a basic V8 d8 (developer shell) exploit for a fixed CVE.
- Reproduce a public n-day browser exploit.
- Original bug research — see Pwn2Own / browser bug bounties.
Tools
- d8 — V8’s developer shell.
- JavaScriptCore JSC shell — analogous for Safari.
- WinDbg / lldb — debug the renderer.
- TurboFan IR visualiser — see V8’s optimised IR.
--no-sandboxflag in dev builds to test renderer exploits without sandbox escape.
OSEE alignment
OSEE EXP-401’s modern syllabus includes:
- Browser-style heap feng-shui.
- ASLR / DEP / CFG bypass via JS-controlled primitives.
- Sandbox escape via win32k / COM.
The full real-world chain is beyond OSEE; OSEE teaches the primitives you’d combine.
References
- V8 source
- JavaScriptCore source
- Pwn2Own writeups index — ZDI
- Saelo (Samuel Groß) — JS engine exploitation (foundational writeups)
- Faraz Faroughi / Phrack — JIT bugs (search)
- See also: type-confusion-kernel, use-after-free-kernel, hevd-uaf-walkthrough, windows-sandbox-and-appcontainer-escape, arbitrary-read-write-primitives