php2shell explained: what the two PHP bugs actually do
How CVE-2026-109618 and CVE-2026-109619 work, why mod_php is the worst case, what exploitation looks like so far, and a checklist for your PHP servers.
Yesterday we wrote about php2shell from the Moodle admin’s side: which PHP versions got fixes and which Moodle releases can run them. This post is for the people who run the servers. What do the two bugs actually do, why does the SAPI matter, and how do you check a real box?
Short version: both bugs are memory-safety flaws in how PHP cleans up after a request dies badly. Neither needs a bug in your application. Since 26 October both are on CISA’s list of exploited vulnerabilities.
Two bugs, one pattern
PHP fixed CVE-2026-109618 and CVE-2026-109619 on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.1 Both have a CVSS v4.0 score of 9.2 in the GitHub advisories.23 The bugs were reported by researchers from Wiz and others, and Wiz’s write-up says “every PHP version released in the last decade is affected by at least one of these vulnerabilities.”4
The shared pattern: a request gets killed halfway through something, and PHP’s bookkeeping does not get put back in order before the memory is reused.
CVE-2026-109618: out-of-memory leaves half-built state
When PHP runs out of memory, it uses longjmp() to jump out of whatever it was doing. The advisory explains that “in some contexts, request memory is temporarily reachable before it is fully initialized.”2 If an allocation fails at that moment, the memory stays invalid. Request shutdown then reads or branches on it.
The interesting part is where the out-of-memory error comes from. The advisory names multipart body parsing at request startup as the main trigger:
- A multipart request without a usable
Content-Lengthcan get past thepost_max_sizecheck and keep sending data. - Each part can carry any number of headers, which cost far more PHP memory than their size on the wire.
- Empty file fields don’t count towards
max_file_uploads, but still create file arrays. Nested field names multiply that.
The fix removes longjmp() for out-of-memory. PHP now does a “soft” stop at the next safe point, and calls _exit() if memory goes past a second, higher limit.2
CVE-2026-109619: stale pointers survive into the next request
This one is a use-after-free across requests. Some globals that live for the whole worker process (error, session, object and extension state) can keep pointers into memory that belonged to one request. The advisory describes four steps:3
- A request stores a pointer to its own memory in a global.
- A fatal error, an out-of-memory error or a late warning skips the reset.
- The request’s memory is freed. The global still points at it.
- The next request on the same worker reads that pointer before normal initialization resets it.
The fatal errors in step 2 include ordinary ones: parse errors, failed require, uncaught errors and “Maximum execution time exceeded”. Two confirmed “consumers” in step 4 run before PHP sets up the request: warnings about invalid or deprecated ini values, and session upload progress. The advisory notes that changing error_reporting or using @ does not block the first path.3
The strongest case is PHP 8.5’s fatal_error_backtraces setting, which left backtrace state behind after a fatal error. That is the basis for the confirmed mod_php exploit.
Why “before any application code runs”
PHP’s advisory says the risk is highest for mod_php, “with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs.”1
The reason is the request lifecycle. PHP parses multipart bodies during request startup, before your script is loaded. Wiz notes the upload progress code can set up session state at that stage, and that their memory leak attack “works even when the application never calls session_start().”4 So your framework’s input filtering, login checks and WAF rules inside the app never see the request. They haven’t started yet.
mod_php, FPM, FrankenPHP: who is exposed
Every SAPI that serves web requests is in scope, per PHP: FPM, mod_php, php-cgi, FrankenPHP and LiteSpeed.1 The details differ. This table is taken from the two GitHub advisories.
| SAPI | CVE-2026-109618 (OOM) | CVE-2026-109619 (stale state) |
|---|---|---|
| Apache mod_php | Confirmed. Full RCE validated over HTTP with default settings | Confirmed RCE via the fatal backtrace path |
| PHP-FPM | Source analysis only, no complete exploit shown | Conditional. Session upload progress RCE validated on a normal dynamic pool |
| FrankenPHP | Likely, from source analysis | Worker mode: no early consumer found in default config, “not immune” |
| LiteSpeed (LSAPI) | Likely, not validated | Conditional |
Why mod_php comes out worst:
- For the OOM bug, mod_php runs request shutdown even when startup fails. FPM kills the child process when startup fails, so the shutdown code never sees the broken state from that path.2
- For the stale state bug, Apache applies
.htaccessandphp_value/php_flagsettings before PHP’s request startup. That gives an attacker an early ini warning to reach the stale pointer. On FPM, pool values apply when the worker starts and.user.iniafter initialization, so FPM is exposed only when the web server passesPHP_VALUEorPHP_ADMIN_VALUEthat cause an early warning.3
Persistent workers matter for CVE-2026-109619. The bad request and the request that uses the stale pointer must hit the same worker. More workers and worker recycling lower the odds per request, but the advisory is clear that they “don’t remove the condition.”3 It even suggests pm.max_requests=1 if you can afford the performance hit.
Neither advisory discusses the CLI. PHP’s own advisory scopes the risk to installations serving web requests, so cron jobs and php on the command line are not the concern here.
The third bug: CVE-2026-115560
On 23 October PHP added a short note about a separate remote code execution bug, CVE-2026-115560, not fixed by the 21 October releases.1 It needs two things at once:
- The application sets
session.serialize_handlertophp_binaryorphp_serializein config, then switches it tophpwithini_set()beforesession_start(). - Session upload progress is still on.
PHP calls this setup unlikely and will publish a full advisory with the next regular release. Canada’s Cyber Centre repeats the same interim fix.5
The mitigation is the same line that helps against CVE-2026-109619:
session.upload_progress.enabled = 0
Two traps. Set it in php.ini, the FPM pool, or the web server config, not with ini_set() or a .user.ini, because those apply too late. And restart every worker afterwards.1 The GitHub advisory recommends keeping it off even after you patch, if you don’t use the feature.3
What has happened since 21 October
The timeline moved fast:
- 17 October: Amazon’s threat intelligence team saw the first exploit attempt, four days before the patches. AWS’s CISO said attempts came from multiple actors, and warned “defenders should not assume the public disclosure date is the earliest date of exploitation.”6
- 20 October: PHP had to bring the release forward after details, including the CVE IDs, leaked.6
- 21 and 22 October: watchTowr saw mass exploitation attempts, but the public proof of concept code did not achieve code execution. JFrog warned about fake PoCs.7 Dataminr described activity as “limited to scanning, fingerprinting” with no known compromises.6
- 26 October: CISA added both CVEs to its Known Exploited Vulnerabilities catalog, with a due date of 29 October for US federal agencies.8
- 27 October: the Cyber Centre updated its alert to say open-source reporting shows both CVEs exploited in the wild.5
Not everyone rates it the same. Red Hat scores CVE-2026-109619 at 8.1 and keeps it “below Critical” because exploitation “requires hitting a precise out-of-memory condition during request processing.”9 Wiz agrees exploitation is hard, describing “an exceptionally complex chain of steps,” but says AI tools let them adapt working exploits to WordPress, Drupal and Joomla in hours.4
On detection, Talos shipped Snort coverage and Corelight released open Zeek and Suricata rules, according to SecurityWeek.7
PHP has been here before
Bugs that hit PHP before your code runs are not new. Three well-known ones:
| CVE | Year | What went wrong |
|---|---|---|
| CVE-2012-1823 | 2012 | php-cgi read query strings without = as command-line options, so -d could set ini values and run code |
| CVE-2019-11043 | 2019 | In some FPM setups, PHP-FPM wrote past a buffer into FastCGI data, opening a path to RCE |
| CVE-2024-4577 | 2024 | php-cgi on Windows: “Best-Fit” character conversion let attackers smuggle PHP options into the command line |
Sources: the CVE records linked in each row.10
The earlier three were about how PHP is connected to the web server: CGI argument handling and FastCGI buffers. php2shell is different. It sits in PHP’s own memory and error handling, which is why it reaches every SAPI to some degree.
Checklist for your servers
1. Find the PHP that serves web traffic, not the CLI one. php -v tells you the CLI version, which can differ from what Apache or FPM loads. Check php-fpm8.3 -v (or your distro’s binary name), or put a phpinfo() page behind auth and read “PHP Version” and “Server API”.
2. Check if you run mod_php. On Debian or Ubuntu, apachectl -M | grep -i php lists php_module if mod_php is loaded. If “Server API” says “Apache 2.0 Handler”, that’s mod_php. “FPM/FastCGI” is FPM.
3. Don’t trust the upstream version number on distro packages. Distros backport fixes. Ubuntu 22.04 fixed this in php8.1 version 8.1.2-1ubuntu2.28, on a branch PHP itself no longer supports.11 Debian 12 fixed php8.2 in 8.2.35-1deb12u1 (DLA-4848-1) and Debian 13 fixed deb13u1 (DSA-6574-1).12 Look for the CVE in the changelog:php8.4 in 8.4.27-1
# Debian / Ubuntu
apt changelog php8.3-fpm | grep -E 'CVE-2026-10961[89]'
# RHEL family
rpm -q --changelog php-fpm | grep -E 'CVE-2026-10961[89]'
Red Hat’s CVE page did not list errata when we checked and points to its bulletin RHSB-2026-013.9
4. If you can’t patch today, apply the ini mitigations from PHP’s advisory and restart all workers:1
memory_limit = -1
post_max_size = 8M ; 32M if uploads are needed
upload_max_filesize = 8M ; keep below post_max_size
session.upload_progress.enabled = 0
fatal_error_backtraces = 0 ; PHP 8.5 only
Read the caveats. PHP says these “do not make an installation safe.” memory_limit=-1 means one request can eat the server’s RAM, so cap memory with a container or cgroup limit instead, and do not use RLIMIT_AS or RLIMIT_DATA, which trigger the vulnerable path.2 Enforce request size at the web server or proxy, including requests without a usable Content-Length. If you don’t accept multipart uploads, block them at the front.
5. Watch for the signs Wiz lists:4
- Odd multipart requests followed by memory-limit errors in the PHP log.
- Bursts of HTTP 500 responses.
- Very large or malformed
PHPSESSIDcookies in responses. - PHP workers dying and respawning for no clear reason.
Turn on log_errors with a working error_log. The GitHub advisory warns that a clean log does not prove you are safe.3 The Cyber Centre also says to look for unexpected processes, files and outbound connections from the web server user.5
6. Don’t forget bundled PHP. Appliances, control panels and containers ship their own PHP. Check each one.
What this means for Moodle
Moodle is a PHP application, so a Moodle site is only as safe as the PHP under it. When PHP upgrades get stuck, an old custom plugin is often the reason. Getting those plugins working on current PHP is the kind of work we do.
Sources
-
PHP Group, News archive 2026: security advisory of 21 October 2026 and informational advisory of 23 October 2026. ↑ ↑ ↑ ↑ ↑ ↑
-
PHP Group on GitHub, GHSA-w8hw-w9fx-j79w: RCE via OOM corruption. ↑ ↑ ↑ ↑ ↑
-
PHP Group on GitHub, GHSA-6wmf-4rv5-p3rp: RCE via stale global state. ↑ ↑ ↑ ↑ ↑ ↑ ↑
-
Canadian Centre for Cyber Security, AL26-026: Critical vulnerabilities impacting PHP, updates of 23 and 27 October 2026. ↑ ↑ ↑
-
BleepingComputer, Amazon says PHP2Shell exploitation began 4 days before patches. ↑ ↑ ↑
-
SecurityWeek, Tech and security companies respond to PHP2Shell vulnerabilities. ↑ ↑
-
CISA, Known Exploited Vulnerabilities catalog entries for CVE-2026-109618 and CVE-2026-109619. ↑
-
Red Hat, CVE-2026-109619. ↑ ↑
-
CVE Program, CVE-2012-1823, CVE-2019-11043 and CVE-2024-4577. ↑
-
Canonical, CVE-2026-109618. ↑
-
Debian, Security tracker: CVE-2026-109618. ↑
Need a Moodle plugin built or rescued?
Tell the scoping assistant what you need. You get a fixed price and a timeline in a few minutes, and an engineer reviews it within one business day.
Get a fixed quote