The WordPress Chain Massacre SLCyber researchers demonstrated a post-exploitation chain, wp2root, that escalates WordPress core remote code execution to Linux root in under an hour using OpenAI's Codex. The chain exploits a use-after-free in PHP to achieve native code execution and then leverages the Copy Fail kernel bug, affecting nearly every Linux kernel shipped in the past nine years, to gain root access. The research highlights the increasing capability of AI in automating complex exploit development. The WordPress Chain Massacre You can outsource the hacking, but not the understanding Today we walk through wp2root, our post-exploitation chain that begins where wp2shell https://slcyber.io/research-center/exploit-brokers-pay-500000-for-a-wordpress-rce-i-found-one-with-gpt5-6/ ends. Our chain is nothing fancy. Its value is in showing what real-world PHP hacking actually looks like, and how far AI has come. Cooking up a chain like this would usually take a skilled operator a few weeks; we did it with Codex in under an hour. wp2shell is AssetNote's pre-auth remote code execution in WordPress core. It is lovely work, both for the bug chain itself and for how it was found with GPT 5.6-Sol. It lets an anonymous, unauthenticated attacker run PHP code on a stock WordPress site. Running PHP is one thing; owning the box is another. wp2shell drops you inside the PHP interpreter, and on a hardened host that interpreter is locked down. Dangerous functions like system and exec are switched off with disable functions https://www.php.net/manual/en/ini.core.php ini.disable-functions , the filesystem can be mounted read-only, and there may be nowhere to write a file. You can execute PHP, but you cannot yet run an operating-system command, let alone become root. wp2root helps you break out of that box. It takes the constrained PHP execution wp2shell hands you and turns it into native code execution with no PHP-level guardrails left. Then it reaches Linux root via Copy Fail https://copy.fail , a 2026 bug that affects nearly every Linux kernel shipped in the past nine years. It works even against the hardened setups above, and if full root is ever out of reach, a reverse shell is always the fallback. Why go all the way to root? In real-world operations, popping WordPress is rarely the real goal. A WordPress box usually serves a public homepage; it matters, but the data worth stealing tends to live elsewhere. What you want is a durable foothold to pivot from. You could proxy traffic through the box into the internal network, backdoor wp-login.php to collect credentials as real users log in, or just sit quietly and watch. We have built a WordPress chain like this before. It helps to know one WordPress quirk: an administrator can upload a plugin, and a plugin is just PHP that WordPress runs, so administrator access is effectively code execution. Back in 2009, a low-privilege WordPress user could reach that admin-level PHP execution through a pair of WordPress core bugs, an authorization check that could be confused https://core.trac.wordpress.org/changeset/11761 into treating a forbidden admin page as allowed CVE-2009-2854 https://nvd.nist.gov/vuln/detail/CVE-2009-2854 , and a permalink option that WordPress later fed to eval https://core.trac.wordpress.org/ticket/10733 . Around the same time, Stefan Esser was demonstrating the other half of the story, that once you had PHP execution, PHP's own restrictions were not much of a wall and memory-corruption tricks https://blackhat.com/presentations/bh-usa-09/ESSER/BHUSA09-Esser-PostExploitationPHP-PAPER.pdf could push straight past them. Twenty years later, the chain we build today looks much the same: php PHP execution via wp2shell - Serializable UAF - arbitrary read - self-resolving ROP - native code execution - PIC launcher - Copy Fail - root Starting point: wp2shell We will not re-derive wp2shell here. AssetNote's writeup https://slcyber.io/research-center/exploit-brokers-pay-500000-for-a-wordpress-rce-i-found-one-with-gpt5-6/ already does a wonderful job, walking a malformed REST batch request into a route confusion, an author exclude scalar into a pre-auth SQL injection, and forged wp posts rows into WP Post objects that WordPress trusts as its own, all the way to a new administrator and a plugin upload that runs attacker PHP. Go read it there. For us, wp2shell gives an anonymous, unauthenticated attacker PHP execution on the box. Everything below starts from that PHP execution and shows what an attacker can actually do with it. From PHP to native code execution wp2shell leaves us running PHP, but still inside the interpreter's sandbox. Reaching native code from pure PHP means corrupting the PHP engine's own memory. So we look for a memory-corruption bug we can trigger from PHP, one that lets us read arbitrary memory, forge internal objects, and hijack execution. A use-after-free is ideal. It hands us an arbitrary read to map the live process and a fake-object primitive to hijack control flow. php constrained PHP execution - PHP engine UAF - arbitrary read - native code execution - escape from PHP-level restrictions The Serializable UAF This is a self-contained component we built to sit on top of wp2shell. It runs only after the WordPress side has uploaded a minimal endpoint that evaluates attacker PHP, and nothing about it is WordPress-specific. Any way to run PHP would do. The bug we exploit here is old, but still unfixed. We covered the full mechanics and the 21-year history in the original MADBugs write-up https://blog.calif.io/p/mad-bugs-finding-and-exploiting-a ; here is the short version. It lives in PHP's legacy Serializable path. A user-defined unserialize method recursively calls PHP's unserialize while the outer parser is still active, and the engine never takes the serialization lock before invoking it. Inner and outer parse then share one reference table. An inner property-table resize frees a bucket allocation that the outer parser still points into. Sprayed strings then reclaim that freed memory. The outer parser's references are now stale, landing on attacker-controlled bytes, so they become attacker-controlled zvals. A zval is the small struct PHP uses internally to hold a value's type and data. php recursive unserialize - shared reference table - property-table resize - freed buckets - reclaimed stale zvals - arbitrary read Reinterpreting those stale zvals as strings gives an arbitrary read, the ability to read memory at any address we choose. That opens two paths. The first recovers the native system handler and calls it directly, in native code, even though disable functions took away the PHP-level name. On a box where a PHP web shell would find system unavailable, that alone is enough for a reverse shell. The second path does not call system at all. It builds a ROP chain that runs our own position-independent shellcode, which in turn invokes Copy Fail. The PoC ships both paths; the rest of this walkthrough follows the second. Self-resolving ROP Return-oriented programming ROP hijacks execution without injecting any code. The trick is the stack. When a function returns, the CPU takes the next address off the stack and jumps to it. So we fill the stack with a list of addresses, each pointing at a short snippet of existing code a gadget that ends in its own ret . The CPU then runs the gadgets one after another. Chain the right ones and you can do real work, all from code already in the process. The idea goes back to Solar Designer's return-into-libc https://seclists.org/bugtraq/1997/Aug/63 in 1997. Shacham formalized and named ROP in 2007 https://hovav.net/ucsd/dist/geometry.pdf . To build such a chain, you need to know where the gadgets and target functions actually sit in memory. Classic exploits precompute those locations from a copy of the target binary. We work the other way around, discovering every address from the live process at exploitation time. That is what "self-resolving" means here, and it is why the exploit needs no fixed profile. The first job is to find the PHP binary in memory, since it loads at a random address. We use the UAF's arbitrary read to leak one live code pointer, any pointer that lands inside PHP's own machine code. From there we walk backward to the start of the loaded image, parse it, and pick out the functions and gadgets the chain needs. The last step is to get the CPU onto our list of gadgets, a move called a stack pivot. We do it by abusing PHP's own cleanup. We corrupt a variable so PHP treats it as an array, and we control that array's internal bookkeeping. When the array is freed, PHP runs its normal routine to destroy it, but the bookkeeping is booby-trapped, and PHP hands the stack over to our gadget list instead of its own. From that point PHP is no longer in control. The CPU is walking our ROP chain. The chain itself is short. It marks our buffer executable, jumps into the launcher, and, if the launcher returns, restores the buffer's permissions before bailing out of the request. php 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 Copy Fail and the fileless root transition The next stage is a local privilege escalation to root. We used Copy Fail https://copy.fail because it is convenient and on-theme. Any other LPE would slot in with little change to the plumbing around it. The interesting problems are the ones around the LPE: running it from inside a PHP worker, carrying a root shell back out, and keeping every step in memory and quiet. The launcher the ROP chain jumps to, root payload launcher.asm https://github.com/califio/publications/blob/main/MADBugs/wp2root/root payload launcher.asm , is a tiny stager; its only job is to load a second-stage helper ELF, , the program that actually performs Copy Fail. Instead of dropping that helper to disk, the launcher creates an anonymous https://github.com/califio/publications/blob/main/MADBugs/wp2root/root payload helper.c root payload helper.c memfd , writes the helper into it, pins it at fd 197, and asks the kernel to execute it directly with execveat ..., AT EMPTY PATH . php memfd create "php-helper", 0 - dup2 fd, 197 - write root payload helper.c - execveat 197, "", argv, NULL, AT EMPTY PATH Pinning the helper to a fixed descriptor is deliberate. fd 197 sits high enough to stay clear of the descriptors a PHP worker already holds, and because the memfd is created without close-on-exec, it survives every execve in the chain. That is how the launcher and, later, the root /usr/bin/su image both find and run the same in-memory helper by a known number, with nothing on disk. The helper performs Copy Fail. When the kernel runs a program like /usr/bin/su , it does not re-read the file from disk each time; it keeps the executable in the kernel page cache and runs it from there. Copy Fail overwrites this cached copy, with a tiny stub of the helper's own making, so the kernel now runs that stub in place of the real binary. The su file on disk is never modified, so file-integrity monitoring that watches file contents or writes to the binary sees nothing. Because /usr/bin/su is setuid-root, running it executes the stub as root. The stub's only job is to re-enter the full helper from fd 197, now with full root. At that point the exploit can execute a user-chosen root command or return an interactive root shell. PoC and write-ups The full exploit, the local lab, and the deep-dive write-ups are in the wp2root repository https://github.com/califio/publications/tree/main/MADBugs/wp2root : wp2shell write-up https://github.com/califio/publications/blob/main/MADBugs/wp2root/writeups/WP2SHELL WRITEUP.md : the WordPress front-door, from REST desync to pre-auth RCE. full-chain write-up https://github.com/califio/publications/blob/main/MADBugs/wp2root/writeups/FULL CHAIN WRITEUP.md : the PHP-to-native post-exploitation chain covered in this post. README https://github.com/califio/publications/blob/main/MADBugs/wp2root/README.md : how to run the PoC against the bundled lab. Conclusion Attackers always weaponize, and it's getting easier than ever with AI. But domain expertise still matters. The key skill now is knowing what is possible; once you know, the model can carry the rest. That is how this chain took us under an hour. The slow part was writing this article, explaining each step for readers without that background. This is how hacking is becoming. You write clear English prose describing what to do, and the model does it. Feed this article to your favorite model and it will happily reproduce the whole chain. But that raises a harder question. Directing the model takes knowing what is possible, and that knowing usually comes from having done the work yourself. If the model does the work from now on, where does it come from? We learned it the slow way, by hand, over years. How the next person learns it, once the slow way is optional, we honestly do not know. We do know one thing, though. You can outsource the hacking, but not the understanding.