# The WordPress Chain Massacre

Post-exploitation chain turning constrained PHP execution (via wp2shell) into native code execution through a PHP Serializable UAF, then root via a page-cache LPE.

- Payout: Research
- Severity: critical
- Program: WordPress
- Write-up by: Calif
- Disclosed: 2026-08-05
- Write-up quality: 8/10
- Original: https://blog.calif.io/p/the-wordpress-chain-massacre
- Library page: https://bugbountylibrary.com/r/blog%3Ablog-calif-io-p-the-wordpress-chain-massacre
- Tags: area:web domain:oss domain:web impact:privesc impact:rce kind:research platform:research sev:critical tech:linux tech:php technique:chained technique:source-review vuln:deserialization vuln:memory-corruption vuln:privilege-escalation vuln:rce

**Target:** PHP interpreter / Linux kernel page cache (post-exploitation from WordPress)
**Root cause:** PHP's legacy Serializable unserialize path recursively invokes user unserialize() without taking the serialization lock, so inner and outer parsers share one reference table; an inner property-table resize frees buckets the outer parser still references, yielding stale attacker-controlled zvals.
**Steps:**
1. Obtain PHP execution via wp2shell (pre-auth RCE in WordPress core).
2. Trigger recursive unserialize() from a user-defined Serializable::unserialize() so inner and outer parse share the reference table.
3. Cause an inner property-table resize to free bucket allocations still referenced by the outer parser.
4. Spray strings to reclaim freed buckets with attacker-controlled bytes, turning stale references into controlled zvals.
5. Reinterpret stale zvals as strings for arbitrary read; leak a live code pointer inside PHP's image.
6. Walk backward to the PHP image base, parse it, resolve functions and gadgets (self-resolving ROP).
7. Corrupt a variable into an array with booby-trapped bookkeeping; on free, PHP's destroy routine pivots the stack to the gadget list.
8. ROP marks buffer executable and jumps to PIC launcher (root_payload_launcher.asm).
9. Launcher: `memfd_create("php-helper", 0)` -> `dup2(fd, 197)` -> write helper -> `execveat(197, "", argv, NULL, AT_EMPTY_PATH)`.
10. Helper performs Copy Fail: overwrite the kernel page-cache copy of setuid `/usr/bin/su` with a stub; running su executes stub as root, which re-enters helper from fd 197.
**How found:** Recognized that PHP execution alone is boxed by disable_functions and read-only mounts, so sought a memory-corruption bug triggerable from pure PHP; chose a UAF for its arbitrary-read plus fake-object primitives, and built the ROP chain to resolve all addresses from the live process rather than a binary profile.
**Exploitation:** Starting from unauthenticated PHP execution, the UAF gives arbitrary read and a fake-object primitive; a self-resolving ROP chain pivots the stack via fake array destruction, marks a buffer executable, and jumps to a PIC launcher that loads a helper ELF from an anonymous memfd pinned at fd 197 (no disk writes). The helper overwrites the page-cached setuid /usr/bin/su image with a stub; executing su runs the stub as root, which re-enters the full helper from fd 197 to run a root command or spawn a root shell. Fallback path calls the native system handler directly for a reverse shell despite disable_functions.
**Escalation:** PHP-level RCE -> native code execution bypassing disable_functions/read-only FS -> kernel page-cache overwrite of setuid su -> root; root enables pivoting, wp-login.php credential backdooring, internal recon.
**Insight:** When PHP-level guards (disable_functions, read-only FS) block you, target the interpreter's own memory: a recursive unserialize() UAF yields arbitrary read and fake zvals, and self-resolving ROP from a leaked code pointer avoids needing a binary profile. For fileless LPE, overwrite the kernel page-cache image of a setuid binary via memfd+execveat so on-disk integrity checks see nothing.

## Proof of concept (copied exactly from the original report)

**memfd helper exec via execveat**

```
memfd_create("php-helper", 0)
 -> dup2(fd, 197)
 -> write root_payload_helper.c
 -> execveat(197, "", argv, NULL, AT_EMPTY_PATH)
```

**Serializable UAF exploitation chain outline**

```
recursive unserialize
 -> shared reference table
 -> property-table resize
 -> freed buckets
 -> reclaimed stale zvals
 -> arbitrary read
```

**Self-resolving ROP chain outline**

```
arbitrary read
 -> leak a live code pointer
 -> locate the PHP image
 -> resolve functions and gadgets
 -> fake array destruction
 -> stack pivot
 -> mark buffer executable
 -> PIC launcher
```

