# Discord Activity to Native RCE Chain: 17-Step Exploit Breakdown

> Explore the 17-step Discord Activity to Native RCE exploit chain. Learn how to escalate from a sandboxed Activity page to arbitrary native code execution using V8 type confusion, popup policy manipulation, and Electron IPC hija...

- Repository: [bikini/exploitarium](https://github.com/bikini/exploitarium)
- Tags: exploit-breakdown
- Published: 2026-09-07

---

**The Discord Activity to Native RCE attack chain exploits a V8 type-confusion primitive, popup-policy manipulation, and Electron IPC hijacking to escalate from a sandboxed Activity page to arbitrary native code execution on Windows.**

This deep dive examines the complete end-to-end exploit chain documented in the `bikini/exploitarium` repository, specifically the **Discord Activity Stock Client RCE PoC**. The attack targets Discord 1.0.9245 and achieves deterministic native code execution through a carefully orchestrated sequence moving from renderer compromise to full process control.

## The 17-Step Exploit Chain Explained

The chain follows a linear progression through three distinct phases: **renderer compromise**, **policy bypass**, and **native escalation**. Each step builds on the previous, with no retries or branching allowed—failure at any stage aborts the entire attack.

### Phase 1: Initial Activity Load and Primitive Construction (Steps 1–6)

Steps 1–6 establish the foundation inside Discord's sandboxed Activity environment.

**Step 1: Activity Loads via Discord Proxy**

Discord's proxy forwards the mapped URL `https://<APP_ID>.discordsays.com` to the local server, loading the malicious Activity inside a sandboxed Chromium renderer. This occurs in [`exploit.html`](https://github.com/bikini/exploitarium/blob/main/exploit.html) as the entry point.

**Step 2: Instance Registration and Lease Acquisition**

The Activity generates a random per-session instance ID and requests a one-shot lease from [`server.js`](https://github.com/bikini/exploitarium/blob/main/server.js). The server validates and returns a cryptographically random lease token:

```javascript
// From server.js - lease generation
const lease = crypto.randomUUID();
activeLeases.set(lease, { instance, token: crypto.randomUUID() });
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ lease }));

```

**Step 3: Popup-Policy Worker Deployment**

The server responds with the *popup-policy* worker script and prefetches the second-stage document containing the native payload. This worker survives navigation and maintains execution context across page transitions.

**Step 4: CSP Nonce Propagation**

Discord injects a fresh Content-Security-Policy nonce for every response. The Activity copies this nonce onto every script in the prefetched document, ensuring scripts execute after the nonce rotation—a critical reliability measure.

**Step 5: V8 Type-Confusion Primitive**

The first renderer forces V8 into a type-confusion code path, constructing an **in-cage arbitrary read/write primitive**. This operates within V8's compressed-pointer cage but provides sufficient capability for subsequent stages.

**Step 6: Payload-Driven Discovery and Trampoline Installation**

Using the primitive, the exploit:
- Recovers the V8 cage base address
- Locates the generated-function dispatch entry
- Identifies the native marker
- Installs a compact native trampoline for future code execution

### Phase 2: Popup-Policy Bypass and Auxiliary Window (Steps 7–9)

Steps 7–9 break out of the single-window restriction, enabling multi-renderer coordination essential for the final payload.

**Step 7: Popup-Policy Binary Patching**

The worker modifies the popup-policy instructions in-memory:
- Flips the auxiliary-window branch condition to **always true**
- Fixes sandbox-flag propagation to maintain renderer integrity

```javascript
// Client-side memory manipulation in exploit.html
writeMemory(popupPolicyAddr, patchedBytes);
flushInstructionCache(popupPolicyAddr, patchedBytes.length);
restorePageProtections(popupPolicyAddr);

```

**Step 8: Auxiliary Window Creation**

With the patched policy, `window.open()` now succeeds. The Activity:
- Stops its initial navigation
- Writes the nonce-adjusted second-stage document into the initial document
- Spawns the auxiliary renderer with full second-stage payload

**Step 9: Second Renderer Rebuilds Primitive**

The second renderer repeats the primitive construction from scratch. Crucially, it now **discovers Discord's Electron IPC implementation** in the loaded image—information unavailable to the first renderer due to initial sandbox restrictions.

### Phase 3: Native Escalation and Execution (Steps 10–17)

Steps 10–17 execute the final native payload and restore system state.

**Step 10: Native IPC Payload Construction**

The second-stage renderer builds three Electron IPC calls using a generated Win64 payload:

| IPC Call | Purpose |
|----------|---------|
| `DISCORD_SETTINGS_SET("WEBAPP_ENDPOINT", <ACTIVITY_PROXY_ORIGIN>)` | Redirects Discord's webapp endpoint to attacker-controlled origin |
| `DISCORD_SETTINGS_SET("WEBAPP_PATH", "/native-proof")` | Sets the path for the native payload endpoint |
| `DISCORD_APP_RELAUNCH()` | Triggers Discord process restart with new settings |

**Step 11: Async Persistence Delay**

The payload inserts an explicit **~3 second delay** before the relaunch IPC. This ensures asynchronous settings writes fully persist to disk before process termination.

**Step 12: Relaunch to Temporary Endpoint**

Discord launches a new main renderer for the temporary endpoint `https://<ACTIVITY_PROXY_ORIGIN>/native-proof`, requesting it from the Activity proxy server.

**Step 13: Native Stage Loads**

The native page executes the engine stage again, this time with different objectives:
- Resolves `CreateProcessW`, `Sleep`, and `ExitProcess` from the **stock Discord binary image**
- Allocates fresh executable memory for the final payload

**Step 14: Calculator Process Launch**

The native trampoline executes the final payload:

```asm
; Generated in-memory trampoline (conceptual)
mov rax, [CreateProcessW_address]
lea rcx, [calc_path]          ; L"C:\\Windows\\System32\\calc.exe"
xor rdx, rdx                  ; lpCommandLine = NULL
xor r8,  r8                   ; lpThreadAttributes = NULL
xor r9,  r9                   ; lpProcessAttributes = NULL
mov [rsp+20h], 0              ; dwCreationFlags = 0
call rax                      ; CreateProcessW(...)

```

**Step 15: Sentinel-Based Completion Signaling**

The compromised renderer exits with a **distinct sentinel value**. A fresh renderer detects this sentinel, reports successful native execution, and enters a recovery-wait state.

**Step 16: Settings Restoration**

`run.ps1` executes the cleanup sequence:
- Verifies the [`settings.json`](https://github.com/bikini/exploitarium/blob/main/settings.json) backup via SHA-256 hash
- Restores original Discord configuration
- Writes recovery signal for coordination

**Step 17: Final Discord Relaunch**

The verification page triggers the final Discord relaunch on the original endpoint, returning the client to pristine state with proof-of-completion logged.

## Core Mechanisms Enabling the Chain

The exploit succeeds through three interacting primitives that bypass progressively stronger security boundaries.

### V8 Renderer Memory Primitive

The **type-confusion vulnerability** provides arbitrary read/write within V8's compressed-pointer cage. According to the `bikini/exploitarium` source code, this enables:
- Discovery of native library addresses
- In-memory code generation and trampoline installation
- Cross-renderer state coordination

This primitive is constructed identically in both renderers but applied differently based on available capabilities.

### Activity Popup-Policy Manipulation

Discord's Activity sandbox restricts `window.open()` to prevent multi-window attacks. By **patching the conditional branch** that enforces this restriction, the attacker gains:

- Ability to spawn auxiliary renderers
- Clean separation between primitive construction and IPC discovery phases
- Survival of worker context across navigation

The patch modifies executable code in the renderer process—requiring the memory primitive from Step 5 and careful instruction cache management.

### Electron IPC Bridging

Discord's Electron integration exposes privileged IPC channels normally inaccessible to web content. The second-stage renderer leverages this to:

- Mutate persistent Discord settings ([`settings.json`](https://github.com/bikini/exploitarium/blob/main/settings.json))
- Force application relaunch with attacker-controlled endpoint
- Execute native payload in a fresh renderer with full process privileges

This represents the **critical trust boundary crossing** from renderer compromise to native code execution.

## Key Source Files and Their Roles

| File | Primary Function | Critical Code Elements |
|------|-----------------|------------------------|
| [`server.js`](https://github.com/bikini/exploitarium/blob/main/server.js) | One-shot orchestration server | Lease generation, popup-policy worker serving, `/native-proof` endpoint |
| [`exploit.html`](https://github.com/bikini/exploitarium/blob/main/exploit.html) | Dual-stage renderer payload | Primitive construction, policy patching, IPC payload generation |
| [`wasm-module-builder.js`](https://github.com/bikini/exploitarium/blob/main/wasm-module-builder.js) | Native payload assembly helper | WebAssembly bytecode construction for in-memory code generation |
| `run.ps1` | Environment preparation and cleanup | Binary validation, settings backup/restore, execution coordination |

All files operate under a **strict one-shot execution model** enforced by [`server.js`](https://github.com/bikini/exploitarium/blob/main/server.js)—the lease system prevents replay or reuse of any stage.

## Summary

- The **Discord Activity to Native RCE chain** requires 17 deterministic steps moving from sandboxed web content to arbitrary Windows process execution
- **Three core primitives** enable the attack: V8 memory corruption, popup-policy binary patching, and Electron IPC hijacking
- The exploit uses **dual renderers**—the first escapes sandbox restrictions, the second discovers and abuses privileged IPC channels
- **`run.ps1` guarantees clean execution** by backing up and verifying [`settings.json`](https://github.com/bikini/exploitarium/blob/main/settings.json) via cryptographic hash before and after the proof
- All coordination flows through **[`server.js`](https://github.com/bikini/exploitarium/blob/main/server.js)**, which enforces single-use leases and serves stage-specific payloads

## Frequently Asked Questions

### What Discord version does this exploit target?

This proof-of-concept targets **Discord 1.0.9245** specifically. The exploit depends on exact binary layouts for V8 type-confusion offsets, popup-policy branch locations, and Electron IPC method addresses. Later Discord versions may have patched the underlying V8 vulnerability or modified the vulnerable code paths.

### Why does the chain need two separate renderers?

The first renderer operates under strict Content Security Policy and sandbox restrictions that prevent direct access to Electron IPC channels. By patching the popup policy and opening an auxiliary window, the **second renderer loads with different initialization state** that exposes the Discord-native IPC implementation. This two-stage design cleanly separates privilege escalation from capability discovery.

### How does the exploit persist the malicious endpoint?

The attack uses Discord's own settings persistence mechanism through three IPC calls: `DISCORD_SETTINGS_SET` for the endpoint and path, followed by `DISCORD_APP_RELAUNCH`. The ~3 second delay ensures these **asynchronous writes complete to disk** before the process terminates. Discord's standard relaunch behavior then loads the attacker-controlled endpoint as if it were legitimate configuration.

### What prevents this exploit from working on patched systems?

Three remediation angles exist: **V8 patches** that close the type-confusion primitive, **popup-policy hardening** that prevents runtime modification or removes the auxiliary window restriction entirely, and **IPC access controls** that restrict renderer access to privileged settings channels. Discord has likely addressed one or more of these vectors in subsequent releases.