Two critical vulnerabilities in the PHP engine itself that let an unauthenticated attacker run commands on your web server with nothing but ordinary HTTP requests. No login, no application bug, no file upload the app accepts — the flaw lives in the runtime, below whatever framework or CMS is installed.
Upgrade to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 — then restart every PHP worker (Apache, PHP-FPM). Updating WordPress, Laravel, Drupal or plugins does not fix this. Check changelogs if you use distro packages, which may backport the fix under older version numbers.
PHP2Shell is the name researchers gave to an exploit chain built on two memory-safety bugs in the PHP interpreter. Both are classified CWE-824: Access of Uninitialized Pointer, both score CVSS 4.0 9.2 (Critical), and both are remotely triggerable without authentication or user interaction. The vulnerable code paths date back to PHP 5.4.0 (2012) — a fourteen-year-old bug reaching every release since.
The bugs were found by researchers at Calif, Wiz, and OpenAI's Codex security team (plus Quarkslab on one), working with the PHP security team, and disclosed as GitHub advisories GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp. The PHP changelog addresses both with a single line — "Avoid state corruption via bailout" — which names the root cause precisely.
A stale pointer is read back as a string with an attacker-inflated length and returned in the Set-Cookie: PHPSESSID response header — up to ~67 KB of worker memory per request. Leaks heap and library addresses, defeating ASLR, and can expose passwords, session tokens, API keys, even TLS private keys.
The freed memory is reoccupied with a forged HashTable whose destructor pointer (pDestructor) is set to the address of system() inside libphp. When the engine cleans up the value, it calls the attacker's function instead. Commands run as the web server user.
When I started digging into this, the first thing I wanted to understand was the trigger — because every vulnerability abuses something built to be useful. PHP has a feature called session.upload_progress: while PHP is still reading a multipart/form-data request body off the wire, it writes a running description of the upload — its name, size, bytes received so far — into the user's $_SESSION. A second request can read that session data and report progress back to the browser.
An attacker reaches it with an ordinary multipart POST — no login required:
POST /index.php HTTP/1.1 Host: example.com Content-Type: multipart/form-data; boundary=----Boundary123 Connection: close ------Boundary123 Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS" upload-test ------Boundary123 Content-Disposition: form-data; name="file"; filename="test.txt" Content-Type: text/plain This is a test file. ------Boundary123--
The critical detail is when this runs. Parsing the request body is one of the first things PHP does — the progress updates are written from inside the body-parsing (SAPI) callbacks, long before the request environment is initialised and long before the target script compiles and runs:
Reaching this code needs no session_start() in the target page, no valid login, no application logic at all. Any URL that accepts a POST is enough — even one whose script does nothing but print a single line. This pre-execution window is the foothold: the whole of PHP2Shell happens inside it, while the application is still fast asleep.
The upload-progress feature is not itself the vulnerability. The vulnerability is what happens when PHP runs out of memory while that feature is in the middle of its work.
PHP caps per-request memory with memory_limit. When a request tries to exceed it, PHP cannot carry on — so it performs what the engine calls a bailout: it uses longjmp() to jump straight back to a safe recovery point, abandoning whatever it was doing. This is the mechanism behind the familiar "Allowed memory size exhausted" fatal error. Normally a request tidies up after itself, freeing memory and clearing pointers — but a bailout skips all of that. It is an emergency exit, not an orderly shutdown.
The flaw is a matter of timing. If the out-of-memory condition strikes after the engine has freed a block of memory but before it has cleared the pointer referring to that block, the bailout leaves the pointer behind — a classic dangling pointer:
// the pattern, simplified: char *ptr = malloc(100); // Event 1: allocates memory free(ptr); // Event 2: memory is freed // OOM bailout happens here — longjmp skips the cleanup! ptr = NULL; // Event 4: pointer is cleared (NEVER RUNS)
The allocator considers the block freed and available for reuse — but the pointer still holds its old address. Classified as CWE-824 (Access of Uninitialized Pointer) and CWE-416 (Use After Free).
On its own, a dangling pointer in a dead request would be harmless — the process ends, the OS reclaims everything. What makes it dangerous is the server model: under Apache prefork with mod_php, a single worker process serves one request after another without restarting between them. The request ends, but the worker does not.
The attacker controls what the pointer now points at — because the attacker controls what Request B allocates. From this single defect the researchers built two attacks: CVE-2026-109618 reads the corrupted state (disclosure), CVE-2026-109619 steers it (execution).
I worked through this in the TryHackMe PHP2Shell room lab. Confirming the target takes seconds — and the punchline is the application itself:
PHP 8.4.26 — one release short of the 8.4.27 fix — and the entire application is one line that prints OK. No login form, no file upload, no database, no bug in this code to exploit. If the attack works against this page, it works against the runtime, not the application: a WordPress or Laravel site is no better protected just because its own code is sound.
| Setting | Needed for the exploit |
|---|---|
| PHP version | Vulnerable build: anything from 5.4.0 up to 8.2.34 / 8.3.35 / 8.4.26 / 8.5.11 |
| SAPI | Apache mod_php, prefork (worker reuse is what keeps the stale pointer alive) |
| session.upload_progress.enabled | 1 (the default) — the delivery mechanism |
| memory_limit | Low enough to provoke OOM reliably (the attack sends a 12 MB field) |
| display_errors | 1 |
| session.use_cookies / use_strict_mode | 1 / 0 |
Modern systems randomise where code and data load (ASLR), so an attacker who wants to redirect execution must first learn the real addresses. CVE-2026-109618 removes that protection by turning the dangling pointer into a leak of the worker's own memory.
Conceptually, the response looks like this: Set-Cookie: PHPSESSID=[32-byte real id][~67 KB of worker memory] That memory can hold: passwords, session cookies, API keys, TLS private keys — and the code addresses the exploit needs.
The exploit runs the leak twice. The first pass discloses the heap chunk base (found by scanning the cookie for 2 MB-aligned pointers); the second discloses the libphp base by reading a pointer at a known offset into the library:
The addresses differ on every run — that is ASLR doing its job. The attack simply reads them out each time rather than guessing. With the heap and the engine's own code located, the second CVE takes over.
The leak told the attacker where everything is. CVE-2026-109619 turns that knowledge into a command running on the server — by reoccupying the freed memory not with data to read, but with a carefully forged object that hijacks the engine's own clean-up machinery.
When PHP destroys a value, it may call a function pointer stored alongside it — a destructor — to release the value's resources. The exploit forges an internal HashTable structure in the reoccupied memory with its destructor pointer, pDestructor, set to the address of system() inside libphp (an address the leak made known). When the engine tidies up that value at the end of request handling, it believes it is calling a harmless destructor — and instead calls system() with attacker-controlled bytes as its argument.
That is remote code execution from nothing but multipart POST requests to a page that prints OK — against a server with no application vulnerability of any kind, with commands running as the web server user.
The corruption is probabilistic — a failed attempt at the leak stage does not mean the target is safe; the attack can simply be retried.
End to end, the chain is: rotate workers over HTTP until one carries a corrupted session pointer, run the Set-Cookie leak twice to learn the heap and libphp bases, then reoccupy the freed memory with a forged HashTable whose destructor points at system(). The full chain completes in under ten seconds, entirely over plain HTTP, with commands running as the web server user.
The exploit code itself is intentionally not published here. The technique is described in full above so defenders can understand and detect it; for hands-on practice, work through it in a legal lab environment such as the TryHackMe PHP2Shell room, and only against systems you own or have explicit written permission to test.
The attack leaves distinctive marks — any of these, and especially the combination, is worth alerting on while patching is in progress:
Treat a suspected incident as both code execution and data disclosure: audit credential, session-token and key exposure too, and consider rotating secrets on exposed hosts.
Because PHP2Shell lives in the runtime, "updating the website" misses it entirely. Updating WordPress, Drupal, Joomla or Laravel changes the application; the vulnerable code is in PHP, underneath all of them. The only complete fix:
; php.ini — temporary exposure reduction only session.upload_progress.enabled = 0 # removes the trigger for most reported techniques opcache.enable = 0 # frustrates the code-execution path ; on PHP 8.5 only: fatal_error_backtraces = 0 # addresses CVE-2026-109619 specifically
Check every web server — and don't forget embedded copies (sidecars, workers, appliances need separate inventory):
# check the version php -v # VULNERABLE, e.g.: PHP 8.4.26 # one release short of the fix # FIXED, e.g.: PHP 8.4.27
| Question | Answer |
|---|---|
| Which PHP versions? | Believed affected back to 5.4.0 (2012). Fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35; source backports published for 7.4 / 8.0 / 8.1. Distro packages may carry the fix under older numbers — check the changelog for the CVE IDs. |
| Which SAPIs? | Apache mod_php is the easiest target and highest risk; also PHP-FPM, php-cgi, FrankenPHP, LSAPI, Windows PHP. |
| Does updating WordPress / plugins fix it? | No. This is in the PHP runtime. |
| Does the app need sessions or uploads? | Not necessarily — the upload-progress trigger runs before application code, even if the app never calls session_start(). |
| What does the attacker get? | Code execution as the web worker user (not root automatically) — enough to read configs/DB creds, drop webshells, and pivot. Plus potential memory disclosure of secrets and keys. |
CVE-2026-109618 • CVE-2026-109619 • CWE-824 / CWE-416 • CVSS v4.0 9.2 (Critical). Technical content on this page draws on the author's notes and the TryHackMe PHP2Shell room. Last updated 24 Oct 2026.