[CVE-2026-109618&CVE-2026-109619] No Login Required: Inside PHP2Shell, the 9.2-Severity Bug That Let Anyone Take Over Your Server
1. What is PHP2Shell? Item Detail Nickname PHP2Shell CVE IDs CVE-2026-109618, CVE-2026-109619 Disclosure date October 21, 2026 (official PHP security team announcement) CVSS score 9.2 each (CVSS v4.0, crit
1. What is PHP2Shell?
| Item | Detail |
|---|---|
| Nickname | PHP2Shell |
| CVE IDs | CVE-2026-109618, CVE-2026-109619 |
| Disclosure date | October 21, 2026 (official PHP security team announcement) |
| CVSS score | 9.2 each (CVSS v4.0, critical) |
| Vulnerability class | Unauthenticated Remote Code Execution (RCE) |
| Trigger point | Before any application code runs — during PHP's own request-parsing stage |
| Blockable by auth? | No — it fires earlier than any authentication logic |
| Affected PHP | 5.4 through 8.5 |
| Affected runtimes | Apache mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed — any PHP runtime that serves HTTP requests |
| Representative affected software | WordPress, Magento, Drupal, Joomla, MediaWiki, Roundcube, phpMyAdmin, Nextcloud, Laravel, and more |
| Exploited in the wild? | Yes — attack attempts captured by Amazon's MadPot honeypot as early as October 17, 2026, four days before public disclosure |
| Discovered by | Calif (researcher Ryan Austin, via OpenAI's "Patch the Planet" project), independently also found by Wiz Research |
The line that matters most here is "Blockable by auth?". Unlike a typical web vulnerability, no amount of hardened login logic or application-layer defense can stop this — PHP's own engine processes the malicious request before your code ever gets a chance to run.
2. What the two CVEs have in common: treating an interrupted operation as complete
Both CVEs live in different code paths, but they share the same root pattern:
A fatal error interrupts PHP mid-operation, and PHP later treats that incomplete state as if it were valid.
CVE-2026-109618 — RCE via out-of-memory (OOM) corruption
When PHP converts form submissions into the $_POST array, it inserts keys and values into an internal hash table in a specific order: first it bumps the entry counters, then it allocates memory for the key. If that allocation exceeds memory_limit and fails in between those two steps, PHP raises a fatal error and abandons the insertion — but the counters already reflect the new entry, even though its key and value were never initialized. During cleanup at the end of the request, PHP trusts those counters and interprets the uninitialized memory as a legitimate PHP value.
CVE-2026-109619 — RCE via stale global state
A single PHP-FPM or mod_php worker process handles many requests over its lifetime. Certain worker-wide fields (such as upload-progress tracking or fatal-error backtrace state) are meant to belong to a single request, but in some paths they aren't properly reset before the next request begins. This is a classic use-after-free: a later request follows a stale pointer into memory that's already been freed and possibly reused. Because PHP's session upload-progress tracking is enabled by default and runs even without an explicit session_start() call, almost any script that accepts multipart uploads is in the attack surface.
3. Source code: the root cause behind CVE-2026-109618
This is the actual pre-patch snippet from PHP's internal hash-table function, as analyzed and published by security researchers. This is not exploit code — it's the location of the bug in PHP's own engine source.
// Zend/zend_hash.c — inside _zend_hash_str_add_or_update_i()
idx = ht->nNumUsed++;
ht->nNumOfElements++;
p = ht->arData + idx;
p->key = key = zend_string_init(str, len, GC_FLAGS(ht) & IS_ARRAY_PERSISTENT);
If the length of an attacker-supplied key exceeds memory_limit, PHP throws a fatal error and gives up on the insertion — but at that point the counters already count the new bucket, while the bucket's key and value were never initialized.
Line-by-line breakdown
| Line | What it does | The problem |
|---|---|---|
idx = ht->nNumUsed++; |
Increments the hash table's "slots used" counter | The count goes up before anything has actually been written |
ht->nNumOfElements++; |
Increments the "element count" too | Same pre-emptive bookkeeping |
p = ht->arData + idx; |
Grabs a pointer to that slot | At this point, whatever p points to is empty — or still holds leftover data from an earlier allocation |
p->key = key = zend_string_init(...) |
Allocates fresh memory for the key string and fills it in | If this exact allocation fails, the three lines above have already executed and can't be rolled back |
In short: the code counts first and fills in content second. If zend_string_init() fails due to memory exhaustion on that fourth line, the hash table believes it has one more element than it actually initialized — and whatever bytes happen to sit at that slot (empty, or leftover from a previous allocation) are what get read later.
What happens when the request ends
PHP still runs its normal teardown of $_POST at the end of the request. During that teardown, it walks every counted bucket and interprets its contents as a PHP value — including the "incomplete" bucket we just looked at. If the bytes sitting in that slot aren't random but were deliberately placed there by an attacker, the cleanup code follows them as if they were a real array structure. The moment it invokes the "destructor callback" embedded in that fake structure, an attacker-chosen function (such as system()) runs with attacker-chosen arguments.
4. How the attack actually plays out (conceptual overview)
We're deliberately not publishing the exact payload layout or the heap-grooming technique — with a large number of servers still unpatched, a reproducible recipe would be directly weaponizable. What follows is the mechanism at a conceptual level, based on public security research (Datadog, JFrog, watchTowr, and others). The attack breaks down into three stages:
- Force memory exhaustion at a precise moment: the attacker crafts a form field sized so that an internal PHP allocation fails at exactly the vulnerable point.
- Seed the unfinished memory slot with chosen bytes: by exploiting heap-reuse patterns, the attacker arranges for the uninitialized memory region to contain bytes of their choosing.
-
Turn cleanup into a function call: when PHP tears down the request, it misreads the planted data as a valid array and invokes the embedded callback — effectively calling something like
system()with the attacker's command.
This also typically requires defeating ASLR (address space layout randomization) to find valid memory addresses. Researchers have reported that PHP's upload-progress feature can leak fragments of process memory back to the client via the PHPSESSID cookie — an auxiliary information-disclosure path attackers can use to make the exploit more reliable.
Summary of the flow:
Request initialization fails (memory_limit exceeded)
→ $_POST teardown runs at request shutdown
→ PHP misinterprets the unfinished bucket as a "valid array"
→ Destroying that fake array invokes its callback
→ system(attacker's command) executes
What's deliberately left out is exactly which bytes get planted in that unfinished slot, and through what request. That part — the heap-grooming technique, precise field sizing, and memory-leak method — is what actually makes the attack reproducible, and we're not including it while so many servers remain unpatched.
How the patch fixes it
The patch changes how memory exhaustion interrupts execution. Hitting a soft limit now defers the error to a safe point, letting an in-progress operation finish initializing its data; crossing a higher hard limit instead terminates the worker immediately, skipping the normal teardown over potentially invalid state. Interestingly, the hash-table insertion still increments its counters before allocating the key — even in the patched version. Only the surrounding error-handling behavior changed. That means the broader pattern — updating metadata before the memory it describes is actually allocated — may still exist elsewhere in PHP, and researchers expect more bugs following this same shape to surface over time.
5. Timeline
| Date | Event |
|---|---|
| 2026-08-01 | Security firm Calif discovers multiple RCE paths |
| 2026-08-11 | Calif reports the first bug to PHP, starting coordinated disclosure |
| 2026-08-24 | Wiz Research independently reports the vulnerabilities to PHP |
| 2026-10-17 | AWS MadPot honeypot detects exploitation attempts before public disclosure |
| 2026-10-20 | Details leak; watchTowr confirms remote code execution |
| 2026-10-21 07:43 | Cloudflare reports early attack activity |
| 2026-10-21 14:00 | PHP ships patched releases; vulnerabilities officially disclosed |
6. Affected versions and fixes
| PHP branch | Affected | Fixed in |
|---|---|---|
| 8.2.x | Before 8.2.35 | 8.2.35 |
| 8.3.x | Before 8.3.36 | 8.3.36 |
| 8.4.x | Before 8.4.27 | 8.4.27 |
| 8.5.x | Before 8.5.12 | 8.5.12 |
| 7.4 / 8.0 / 8.1 | All versions | No official release; minimal source backports provided |
| 7.3 and earlier | All versions | No fix; upgrade to a supported branch |
Any PHP runtime that serves HTTP requests is in scope — Apache mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed, and container images that bundle their own PHP interpreter all need to be checked.
7. How to respond
Patching is the only complete fix. Everything below is a stopgap, not a cure.
- Update to PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 or later as soon as possible
- If you use vendor packages (Debian, RHEL-family, etc.), check the vendor's own security advisory
- Rebuild affected container images and restart or replace every worker — stale runtimes can linger in memory otherwise
- If you can't patch immediately:
-
CVE-2026-109618: enforce
memory_limit=-1at the administrator level, paired with request- and memory-size limits at the frontend/container layer. Don't substitute process-level limits likeRLIMIT_AS/RLIMIT_DATA— they can actually make the triggering allocation failures easier to hit. -
CVE-2026-109619: set
session.upload_progress.enabled=0, and on PHP 8.5,fatal_error_backtraces=0 - Until patched, consider isolating internet-facing applications or blocking anomalous requests at a reverse proxy before they reach PHP
-
CVE-2026-109618: enforce
8. Detection signals
No single signal is conclusive, but correlating these three layers helps:
- Unusual form submissions: abnormally long field names, unexpected control bytes or null bytes in the request body
-
Memory-exhaustion errors in logs: fatal errors like
Unknown on line 0, which indicate failure during request initialization — before any script even started running -
Unexpected shell processes spawned by web workers: an Apache or PHP-FPM process spawning
sh,bash, etc. is the most direct evidence of successful exploitation
A successful exploit doesn't necessarily crash the worker or return an error response — an HTTP 200 doesn't mean the request was harmless.
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.

