Skip to content

PHP2Shell Explained: The Two Bugs and Who Is Really Exposed

PHP2Shell Explained: The Two Bugs and Who Is Really Exposed

I read the PHP2Shell advisory twice and still got one thing wrong. I assumed “unauthenticated RCE in PHP” meant a bug in some function I could grep my codebase for. It doesn’t. Neither bug lives in a function you call. They live in what PHP does when a request goes badly, and that is why this pair of CVEs has people more rattled than the usual patch-Tuesday noise.

Short version for the impatient: CVE-2026-109618 and CVE-2026-109619 are both memory-safety bugs in PHP’s request lifecycle, both scored 9.2 on CVSS v4.0, and both are fixed in 8.5.12, 8.4.27, 8.3.36 and 8.2.35. If you want the step-by-step patch routine, I wrote that already in my PHP 8.5.12 patch checklist. This post is the other half: what the bugs actually are, and which of your servers are really in the blast radius.

I haven’t reproduced either bug and I’m not going to. Everything below comes from the vendor advisories, and I’ll say where I’m inferring.

The first bug: PHP runs out of memory at a bad moment

CVE-2026-109618 is an out-of-memory problem. According to Rapid7’s write-up, when an allocation fails while PHP is setting up request data, PHP abandons the setup partway through. The request structures are then half initialized. Cleanup code later treats them as if they were fine, and that is the path to code execution.

The part that surprised me is how the attacker gets PHP to run out of memory. Rapid7 says a crafted multipart request can exhaust memory even when its size looks like it fits inside your configured limits. So “my post_max_size is small” is not the comfort it sounds like.

Why does this matter more on one setup than another? Because of what happens after the failure. On Apache with mod_php, cleanup still runs even though startup failed, so the demonstrated attack works before any of your application code executes. On PHP-FPM, a failed startup kills the worker, which breaks that particular sequence. Rapid7’s source analysis says FPM may still be susceptible after a successful startup, but their advisory reports no complete FPM exploit for this CVE.

I’d read that as “FPM has a better day than mod_php”, not “FPM is safe”. The distinction affects how fast you move, and I’ll come back to it.

The second bug: a request inherits the last one’s leftovers

CVE-2026-109619 is a use-after-free across requests. A worker process handles many requests over its life. Some internal state is supposed to be reset between them. In this bug, a pointer survives pointing at memory that was already freed at the end of an earlier request, and a later request on the same worker can read it before initialization clears it.

What puts the stale pointer there? Fatal errors, mostly. Memory exhaustion and execution timeouts can interrupt the cleanup. Rapid7 also says some non-fatal shutdown diagnostics can repopulate error state after it was reset.

Two confirmed exploit paths are described. One needs mod_php on PHP 8.5 with fatal_error_backtraces switched on, which is the default. The other goes through the session upload progress feature, which is also enabled by default. That one affects both mod_php and PHP-FPM, and it can consume the stale state even if your application never calls session_start().

That last detail is the one I keep thinking about. Plenty of apps never touch sessions, so the natural reaction is “this doesn’t apply to me.” It does, because the feature is on at the PHP level, before your code decides anything.

Who is actually exposed

Here’s the matrix as I understand it, with the caveat that I’m summarizing vendor text.

All web-serving SAPIs are in scope: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. The PHP team’s own wording, as quoted by Huntress, is that all PHP installations serving web requests are at risk. CLI-only scripts that never take a web request are a different story, though I’d still patch them because the changelog carries other fixes too.

mod_php is the highest risk. FPM sits a notch lower for the OOM bug but is fully in play for the session path. If you run a typical Nginx plus FPM stack, you are exposed to 109619 through upload progress and possibly to 109618 after startup.

Versions: 8.2 before 8.2.35, 8.3 before 8.3.36, 8.4 before 8.4.27 and 8.5 before 8.5.12. Older branches are messier, because for 7.4, 8.0 and 8.1 the patched build carries the same version number as the vulnerable one and only the build date differs.

Wiz, according to Huntress’s summary, demonstrated unauthenticated RCE against WordPress, Drupal and Joomla. That detail matters if you’re a freelancer like me. A client’s WordPress site on shared hosting is the same bug as my own VPS, with a different person holding the keys.

What a 9.2 score does and doesn’t tell you

Both CVEs carry the same CVSS v4.0 vector, AV:N/AC:H/AT:P/PR:N/UI:N, plus high impact on confidentiality, integrity and availability. Read the first half of that slowly. Network reachable, no privileges, no user interaction. That is the scary part. But AC:H means high attack complexity and AT:P means the attack needs specific conditions to be present, such as a particular SAPI or setting.

So the score is honest about two things at once. The bug is as bad as a bug gets once the conditions line up, and getting the conditions to line up takes work. That matches what Rapid7 found when they tried the public exploits: each one worked for one combination of OS, PHP version and web server and fell over elsewhere.

I find that more useful than the headline number. It tells me the risk is not evenly spread across my servers. A stock mod_php box on a distro I can name is the worst case. A containerized FPM pool with upload progress disabled is a much harder target. I’d still patch both, but I know which one gets my evening.

What the exploit situation looks like

I want to be careful here because the story changed several times in a week.

PHP shipped the fixes on October 21, a day earlier than planned after details leaked on the 20th. By the 23rd Datadog Security Labs noted working public proofs of concept. Rapid7’s testing found that each public exploit works only for a specific combination of OS, PHP version and web server, and that none looked broadly reliable. They haven’t developed a general exploit themselves.

That is a useful corrective to the panic, and a poor reason to wait. A bug that needs a tailored exploit per environment is still a bug that people with a lot of free time will tailor. And watchTowr reported seeing exploitation through its honeypot network, so “nobody is trying” is not a safe assumption. Watching the timeline unfold is its own lesson, and I’d rather you learn it from this post than from an incident ticket.

What I’d do in the next hour

Pick the box with mod_php first. If you have one, patch it before anything else, then FPM boxes that take public uploads, then the rest.

Check what is really running, because the package version and the running process can disagree. A restart is part of the fix, since old workers keep running old code.

php -v
php-fpm8.4 -v 2>/dev/null
ps -eo pid,lstart,cmd | grep -E 'php-fpm|php-cgi|frankenphp|lsphp' | grep -v grep

If a worker’s start time is before your upgrade, it is still the old binary. Restart the service, then run that last line again.

Then do the one thing that costs nothing while you wait for a maintenance window: set session.upload_progress.enabled=0 in php.ini or your FPM pool, not through ini_set() or .user.ini, and restart. The advisory is explicit that those two apply too late. It is a partial measure for one bug, so treat it as a seatbelt.

I maintain PHP servers for clients (there’s more on that on my portfolio), and the order for a bug like this is always inventory, patch, restart, verify. If you want a baseline for the rest of the box, my VPS hardening notes cover the stuff I set up before PHP is even installed. This week, run that inventory command on every server you own before you go to bed, and write down which ones still show an old worker start time.