Reports / TR-2026-01 · Vulnerability Research
Threat Research Report

PHP2Shell: One Jump From Crash to Compromise

A technical investigation into CVE-2026-109618 and CVE-2026-109619: how a decades-old shortcut in PHP's memory handling became an unauthenticated remote-code-execution class, and how attackers are using it.

PHP2Shell CVE-2026-109618 CVE-2026-109619 Remote Code Execution PHP Web Shell
Severity
CVSS 9.2
v4.0 · Critical · both CVEs
Access needed
None
Network-reachable, pre-auth
Vulnerable since
PHP 5.4.0
Code paths dating to 2012
Fixed in
8.5.12
8.4.27 · 8.3.36 · 8.2.35

/ 00 · OVERVIEWThe root cause, in two listings

PHP runs roughly three-quarters of the web, so a flaw in the interpreter itself is not a flaw in one application. It is a flaw in all of them at once. PHP2Shell is exactly that: two critical bugs in how the PHP engine aborts a request when it runs out of memory or hits a fatal error. Neither is a buffer overflow or a classic injection. Both come down to a single, almost mundane decision made deep in the engine's C code, and the clearest way to see why they happen is to read that code.

Zend/zend_hash.c · insert
/* Zend HashTable insert — the array that backs $_POST and $_FILES.
   Simplified from _zend_hash_str_add_or_update_i(). */

idx = ht->nNumUsed++;        /* (1) the bucket is now "live" to the rest of PHP... */
ht->nNumOfElements++;        /*     ...before it holds anything                    */

p = ht->arData + idx;
p->key = zend_string_init(str, len, persistent);   /* (2) may fail -> longjmp() straight out */

p->h   = ZSTR_H(p->key) = h;        /* (3) never reached if (2) bailed out:   */
ZVAL_COPY_VALUE(&p->val, pData);    /*     key and val stay uninitialised     */
LISTING 1 · The defective ordering at the core of CVE-2026-109618. The engine increases the element count in step (1) before it allocates the bucket's key in step (2). If that allocation fails, PHP's out-of-memory path performs a non-local jump (longjmp()) that escapes this function entirely; step (3) never runs. The result is a bucket the count insists is valid but whose contents were never written. (Calif)

The danger surfaces later, at the end of the request, when PHP tears the array down and trusts that count:

Zend/zend_hash.c · teardown
/* Request teardown — zend_array_destroy() walks every bucket the count claims exists */
for (p = ht->arData; p != ht->arData + ht->nNumUsed; p++) {
    i_zval_ptr_dtor(&p->val);                      /* reads a value that was never written */
    if (p->key) { zend_string_release(p->key); }   /* frees a key that was never set        */
}
LISTING 2 · The consumer. Shutdown iterates to nNumUsed and releases each bucket's value and key (including the half-built bucket from Listing 1), reading and freeing memory that was never initialised. (Calif)
▸ The whole class in one sentence

Publish a count, then do work that can fail, then let the failure skip the step that finishes the object, while a later consumer still trusts the count.

Everything else in this report (the second CVE, the web shells, the botnet traffic, the government intrusion) grows from that one pattern repeated across the engine.

/ 01Introduction

In the week of 21 October 2026 the PHP project shipped an emergency set of releases and two GitHub Security Advisories for a pair of critical, unauthenticated remote-code-execution flaws that the research community had already nicknamed PHP2Shell. We reviewed the two advisories, the engine-level write-up from Calif, and the vendor telemetry published in the days that followed, and reproduced the root-cause reasoning against public PHP source so that we could describe the defect precisely without publishing anything that advances exploitation. This report is the result.

Our goal is narrow and defensive. We explain why these bugs exist at the level of engine logic, how attackers are turning them into code execution and what they do next, and what Saudi operators and defenders should do about it: patch guidance, detection content, indicators of compromise, and an ATT&CK mapping. We have deliberately withheld the material that would turn this into a recipe: there are no tuned request payloads, no heap-grooming steps, no memory offsets, no ASLR-bypass chain and no web-shell code here. The illustrative listings above and below are root-cause explanation, nothing more.

/ 02Key findings

Two critical CVEs, one design flaw.

PHP uses a non-local jump (longjmp() via zend_bailout()) to abort a request on out-of-memory or fatal errors. CVE-2026-109618 (RCE via OOM corruption) leaves request memory half-built when that jump fires mid-allocation; CVE-2026-109619 (RCE via stale global state) leaves a worker's global pointers aimed at a request heap that has already been freed. Both carry CVSS v4.0 9.2 (Critical), both are network-reachable and require no authentication (GHSA-w8hw-w9fx-j79w; GHSA-6wmf-4rv5-p3rp).

Every web-serving PHP is in scope, mod_php worst.

The flaws affect Apache mod_php, PHP-FPM, php-cgi, FrankenPHP and LiteSpeed; PHP rates the risk highest on Apache mod_php, where the bad state is consumed during request startup, before any application code runs (php.net).

The code is old; the exploit is new.

The vulnerable paths reach back to PHP 5.4.0 (2012). Fixes landed on 21 October 2026 in 8.5.12, 8.4.27, 8.3.36 and 8.2.35, with source backports for the end-of-life 7.4, 8.0 and 8.1 branches. Only updating closes every variant (php.net).

Exposure is enormous, by any definition.

Palo Alto's Unit 42 counts over 22.6 million exposed instances potentially vulnerable to both CVEs via Cortex Xpanse; VulnCheck counts roughly 403,000 internet-facing hosts on supported branches that have a patch available, out of ~2.6 million PHP hosts it sees (Unit 42; VulnCheck).

Attempts are confirmed and widespread; compromises are not.

The first attempts hit sensors on 17 October, four days before disclosure, and scanning escalated within hours of the patch. But as of 23 October, multiple vendors (watchTowr, SOCRadar's own screening, CISA's KEV catalogue) report no confirmed successful compromise of a production target, with one exception: Unit 42's account of a single European-government intrusion driven by agentic AI tooling (SecurityWeek; Unit 42).

AI is the force multiplier.

Memory-corruption exploits normally need hand-tuning for every build and allocator layout. Wiz and VulnCheck both report that AI assistance collapses that specialist effort to hours, which weakens the "high complexity" in the CVSS vector as a real-world defence (Wiz; VulnCheck).

Who should read this

Security leads at regulated Saudi SMEs and mid-market firms running PHP applications; DFIR and SOC analysts building detection; and platform and hosting teams responsible for PHP fleets.

/ 03The non-local jump at the heart of PHP2Shell

To understand both CVEs you only need to understand one mechanism. PHP handles fatal errors (including out-of-memory) with zend_bailout(), a wrapper around the C standard-library function longjmp(). When the engine wraps a critical section in zend_first_try { ... } zend_end_try();, it is setting up a setjmp/longjmp pair; an allocation failure inside that block jumps straight to the request-shutdown handler and skips every step in between. Any structure being built at that instant is simply abandoned where it stands (Calif; GHSA-w8hw-w9fx-j79w).

A non-local jump like this is safe only under one condition: that every allocation sitting between "make this object visible" and "finish building this object" is infallible. PHP's OOM-via-longjmp design broke that invariant everywhere such a window exists. The advisory states it plainly:

"In some contexts, request memory is temporarily reachable before it is fully initialized. If any allocation fails at that point, PHP does not properly account for the early exit and leaves the request memory in an invalid state... The request shutdown procedure may then read or branch on it."

(GHSA-w8hw-w9fx-j79w) Crucially, the allocation that fails need not be related to the memory that becomes invalid: an attacker can force an unrelated, chosen allocation to be the one that tips the request over its memory limit.

PHP does not classify this as a single weakness but as a cluster: use-after-free (CWE-416), improper handling of exceptional conditions (CWE-755), access of an uninitialised pointer (CWE-824), improperly controlled sequential memory allocation (CWE-1325) and incorrect initialisation of a resource (CWE-1419) (GHSA-w8hw-w9fx-j79w). Two failure modes follow from the same design, and they map onto the two CVEs.

/ 04CVE-2026-109618: counting a bucket before it exists

The first mode is the one in Listings 1 and 2: a failed allocation interrupts a context that has already published its memory but has not yet filled it, and shutdown then reads or branches on that invalid data. The HashTable insert is the canonical example because the HashTable is the structure that backs PHP's superglobal arrays, and it is populated during request startup, before any application script runs, as the engine registers request variables. The producing call chain runs entirely inside php_request_startup() (Calif):

call chain · php_request_startup()
URL-encoded POST  root[x]=v
  php_std_post_handler() -> php_register_variable_ex()
    -> zend_symtable_str_update_ind() -> _zend_hash_str_add_or_update_i()   /* Listing 1 */

RFC1867 upload    root[x], filename="..."
  rfc1867_post_handler() -> register_http_post_files_variable_ex()
    -> php_register_variable_ex() -> ... -> _zend_hash_str_add_or_update_i()
LISTING 3 · How ordinary request-variable registration reaches the vulnerable insert, before application code executes. (Calif)

The remaining question is how an attacker reaches an out-of-memory condition while staying inside PHP's apparent limits, so that the request is accepted and parsed rather than rejected up front. The advisory lists the conditions at a conceptual level: a multipart request with no usable declared content length can pass the post_max_size check yet keep supplying body data; a part may carry an unbounded number of headers whose in-memory representation dwarfs their wire size; and empty file controls do not count toward max_file_uploads but still create all six $_FILES metadata entries, which nested field names multiply into many allocations (GHSA-w8hw-w9fx-j79w; Calif). We describe these only in principle; the point for defenders is that the trigger is a multipart request, and the fault lands in request setup, not in a script.

⚠ Counter-intuitive hazard

Capping memory with operating-system limits (RLIMIT_AS, RLIMIT_DATA, Apache RLimitMEM, systemd LimitAS= or ulimit -v) makes allocations fail at the limit, and that failure takes the vulnerable longjmp() path. These limits make exposure worse, not better.

/ 05CVE-2026-109619: a worker that remembers a freed request

The second mode is subtler and, on modern PHP 8.5, the stronger one. A PHP worker process handles many requests in sequence. If one request stores a pointer to its own request-owned memory in a worker-lifetime global, and a fatal error skips the step that would clear that global, the request heap is freed while the global still points into it. The next request on the same worker can then touch that dangling pointer before normal initialisation replaces it. The advisory frames this as a producer and a consumer, and the essence reduces to a few lines:

illustrative pseudo-code · producer / consumer
/* CVE-2026-109619, reduced to its essence (illustrative pseudo-code) */

/* --- Request N: the "producer" --- */
GLOBAL.ptr = request_owned_allocation();  /* publish a pointer into THIS request's heap */
...
longjmp();                 /* a fatal error (often OOM) escapes here... */
GLOBAL.ptr = NULL;         /* <-- ...so this reset never runs          */

free_request_heap();       /* heap released; GLOBAL.ptr now dangles */

/* --- Request N+1 on the SAME worker: the "consumer" --- */
if (GLOBAL.ptr) {          /* runs BEFORE normal re-initialisation clears it */
    use(GLOBAL.ptr);       /* read / free / branch on freed memory           */
}
LISTING 4 · The producer/consumer pattern behind CVE-2026-109619. The two halves run in different HTTP requests handled by the same persistent worker. (GHSA-6wmf-4rv5-p3rp)

PHP gives five conditions for a concrete instance: (1) a reachable operation stores request-owned state in a globally accessible location; (2) that state is not reset, or is repopulated after its normal reset; (3) PHP frees the request memory while the persistent worker keeps the reference; (4) the same worker handles another request; (5) an operation reads, frees or branches on the stale state before normal initialisation resets it (GHSA-6wmf-4rv5-p3rp).

Producers are fatal interruptions that abandon an operation before it tidies up: parse and compile failures, uncaught errors, a failed require, Maximum execution time exceeded, and OOM after state was already published. Consumers are operations that run before a request is fully initialised. Two are confirmed. The first is INI diagnostics: a warning, notice or deprecation raised while applying configuration can consume stale error state; it need not be fatal, displayed or logged, so error_reporting, the @ operator, display_errors and log_errors offer no protection. The second is session upload progress, which can reach session handling before ordinary session initialisation, is enabled by default, and does not require the application to call session_start() (GHSA-6wmf-4rv5-p3rp).

Three concrete cases belong to this CVE, and this is where published aggregators frequently mis-map the two identifiers. The authoritative mapping from php.net and the GHSAs places all of the following under 109619, not 109618:

  • Fatal-error backtrace (PHP 8.5, the strongest demonstrated case). With the new fatal_error_backtraces setting enabled, fatal-error reporting stores backtrace state owned by the request, and "resetting of this global state is always missed during shutdown." A diagnostic on a later request reaches the stale value first. The advisory calls this "the basis of the confirmed mod_php exploitation." The exact C field name circulating in secondary summaries is unconfirmed in a primary source; the behaviour (a reset that is always skipped) is confirmed (GHSA-6wmf-4rv5-p3rp; php.watch).
  • Session upload-progress lifecycle. Request startup parses input before modules are activated, and shutdown gates module cleanup on the same flag, so an early failure frees the heap while the session globals still point into it. Listing 5 shows why the reset is skipped.
  • Generic unserializer bookkeeping (the PHP-FPM path). An interrupted unserialize() leaves the standard-extension globals pointing at a freed decoder structure with a non-zero nesting level, which the next request's session handling treats as a live decode. Reached in real apps through session_decode(), unserialize(), and framework helpers such as WordPress's maybe_unserialize() (Calif). The downstream memory-reclamation primitive this enables is deliberately omitted from this report.
main/main.c · startup & shutdown
/* main/main.c — request startup parses input BEFORE modules are "activated" */
PG(modules_activated) = 0;
php_hash_environment();      /* parses multipart input; session upload-progress runs here */
zend_activate_modules();     /* normal module RINIT */
PG(modules_activated) = 1;

/* php_request_shutdown() — extension shutdown is gated on that same flag */
if (PG(modules_activated)) {        /* still 0 if we failed early, so... */
    zend_deactivate_modules();      /* ...the session RSHUTDOWN never runs */
}
shutdown_memory_manager(...);       /* ...yet the request heap is freed anyway */
LISTING 5 · Why the session reset is skipped. Because multipart parsing can touch the session module before PG(modules_activated) is set, and shutdown only runs module cleanup when that flag is set, a worker can survive with session globals still pointing into a freed heap. (Calif)

We note in passing that some secondary coverage attributes an additional OPcache map_ptr type-confusion case to this family. We could not confirm that case in any primary source (the two GHSAs and the Calif write-up), and we do not present it as confirmed.

/ 06What the patch changed

The fix is as instructive as the bug, and it shows why no configuration change is a full substitute for it.

the two fixes, in principle
/* CVE-2026-109618 */
/* BUG:  OOM unwinds immediately, in the middle of an operation */
if (allocation_failed) longjmp(bailout);     /* escapes with half-built state */

/* FIX: defer the unwind to a safe point */
if (allocation_failed) set_soft_oom_flag();  /* raise a flag, keep going        */
/*  ...the unwind now happens only at the next VM safe point, where no          */
/*  half-built bucket is observable; a second, higher threshold calls _exit().  */

/* CVE-2026-109619 */
/* FIX: reset request-owned globals UNCONDITIONALLY at the request boundary, */
/*      regardless of how the previous request ended                         */
GLOBAL.ptr = NULL;
LISTING 6 · 109618 replaces the immediate longjmp() with a delayed "soft OOM" raised at the next VM safe point (plus a hard _exit() ceiling); 109619 resets the wider request-owned state so nothing survives into operations that precede normal initialisation. (GHSA-w8hw-w9fx-j79w; GHSA-6wmf-4rv5-p3rp)

The soft-OOM design restores the invariant that the original longjmp() broke: because unwinding now occurs only at a VM safe point, shutdown never observes a half-built bucket or a half-published global. PHP states bluntly that for 109619 "updating to a release containing the complete state reset patch is the only reliable mitigation". There is no generic configuration that closes every producer and consumer (GHSA-6wmf-4rv5-p3rp).

/ 07Affected versions and SAPIs

The vulnerable session upload-progress path dates to PHP 5.4.0; the HashTable case is reachable from PHP 7.3 onward per Calif. Fixes shipped on 21 October 2026.

ComponentAffectedFixed in
PHP 8.5< 8.5.128.5.12
PHP 8.4< 8.4.278.4.27
PHP 8.3< 8.3.368.3.36
PHP 8.2< 8.2.358.2.35 (branch EOL 31 Dec 2026)
PHP 7.4 / 8.0 / 8.1all releasessource backports only (branches otherwise unsupported)
Both CVEs: CVSS v4.0 9.2 Critical, CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H. The raised attack complexity (AC:H) and required attack prerequisites (AT:P) reflect the need to land OOM precisely and reach the right worker, not any need for credentials. (GHSA-w8hw-w9fx-j79w; Calif; php.net)

The server API (SAPI) in front of PHP decides how reachable the flaw is, because each SAPI handles a failed request differently:

SAPICVE-2026-109618CVE-2026-109619
Apache mod_phpHighest risk. Calls request shutdown even when startup failed, consuming the bad state before any application code runs. Application-independent and pre-auth.RCE validated (backtrace and session paths).
PHP-FPMChild exits on startup failure, removing itself from the pool; susceptible via a post-startup OOM producer by source analysis.Session path validated on a normal dynamic pool; INI consumer only via front-end-supplied values.
FrankenPHPCalls shutdown unconditionally like mod_php; likely susceptible to application-independent attacks.No early consumer found in the default config. Not immune.
LiteSpeed / LSAPIModels FPM; application-specific exploits likely but unvalidated.Conditional, via early environment INI values.
php-cgiListed by PHP as at risk.Listed by PHP as at risk.

The mod_php-versus-FPM difference is itself a clean illustration of the mechanism: on mod_php the startup-failure path runs php_request_shutdown(NULL), so the consumer is reachable with no authentication and no application cooperation, whereas FPM's fpm_main.c handles startup failure by shutting the module down and returning FPM_EXIT_SOFTWARE, killing the worker so that no consumer runs (GHSA-w8hw-w9fx-j79w; Calif).

/ 08How attackers weaponise it

We describe the attack at the level a defender needs to recognise it, not reproduce it. The observed and expected chain is:

01
Fingerprint PHP and its version
Recon
02
Force the memory fault with crafted multipart requests
Trigger
03
Optionally leak memory to defeat ASLR
Leak
04
Achieve code execution in the PHP worker
Execution
05
Move to the operating system
Post-exploit

Fingerprinting. VulnCheck's canary network recorded reconnaissance preceding exploitation: probes of / and /index.php, requests for /phpinfo.php and /info.php, and broad browser-user-agent sweeps. Distinctive scanner user-agents included php2shell-scan/109618, php2shell-scan/1.3 (eigen-oppervlak controle; CVE-2026-109618/109619), php2shell-verify/1.0 and Mozilla/5.0 (php2shell-poc) (VulnCheck).

Forcing the fault. Most observed attempts abuse PHP's session upload-progress path through multipart/form-data requests (per Hadrian via SecurityWeek). Behaviourally, Datadog describes the chain as: a request fails during initialisation and produces a memory-exhaustion error; shutdown destroys $_POST; PHP reads an unfinished upload bucket as a valid array; and cleanup invokes a callback that ultimately runs a command. Calif demonstrated that hundreds of empty file-upload fields can exhaust the default memory limit while staying within the upload size limit (SecurityWeek; Datadog; Island.io).

Calibration bursts. Because the exploit is fragile with respect to heap layout, ASLR and display_errors, attackers calibrate before firing. VulnCheck's telemetry shows the sequence clearly: a scan burst, then "verification" checks (php2shell-verify/1.0), then a short exploitation-attempt window. That is a fingerprint pass to confirm the version and memory conditions before the trigger (VulnCheck).

The optional leak. On the 109618 disclosure path, a memory-disclosure variant returns reclaimed worker memory to the attacker. Wiz demonstrated the server returning a chunk of its own memory inside a session cookie; Datadog describes PHPSESSID leaks supplying valid addresses to defeat ASLR. This stage matters twice over: it is what turns an opportunistic failed attempt into reliable code execution, and it can exfiltrate secrets without any code execution at all. Another request's passwords, tokens and keys can leave the server in a cookie (Island.io; Datadog).

Exploit maturity. The picture is uneven and worth stating honestly. VulnCheck holds a weaponised exploit for 109618 (customers only) that chains a memory leak into RCE on PHP 8.4.26, x86-64, with ASLR enabled and display_errors=off; it found one public GitHub proof-of-concept that bypassed ASLR for that exact build and default heap configuration only, and failed elsewhere. Sansec reports that the public exploits it saw do not work against a default install and mostly need a separate file-inclusion bug in the target application. The consistent theme across sources is that reliable, portable RCE remains hard today, but is expected to generalise, and that AI assistance is what shortens the per-build adaptation (VulnCheck; Sansec; Wiz).

/ 09The second stage: web shells, botnets and an agentic intrusion

Once code runs inside the PHP worker, reaching the operating system follows the same patterns as any PHP RCE, and this is where host telemetry sees the attack regardless of how clever the memory corruption was. A successful execution in VulnCheck's observed attempt printed the marker string PWNED:: in the HTTP response; the follow-up requests tried to read a dropped session file and all returned 404, which VulnCheck recorded as an unsuccessful drop (VulnCheck).

How a web shell is deployed and used (behaviour, not code)

In the attempts observed so far, the attacker writes a small PHP file into a path the worker can serve (commonly the document root or a session/upload directory) so that subsequent commands can be sent over ordinary HTTP and survive the worker restart that the exploit tends to cause. Defenders do not need the shell's source to catch this: the tell-tales are a new .php file appearing with no change-management record, and, far more reliably, the PHP worker spawning a child process (a shell or an interpreter) to run the attacker's commands. We describe the shell only at this behavioural level by design.

Commodity botnet

Researchers tied PHP2Shell attempts to a Mirai-derived DDoS botnet and demonstrated an ASLR bypass (SecurityWeek). A named "Potassium" botnet with specific file and DNS indicators has circulated in secondary reporting; we could not confirm that name, its attribution, or those indicators in any primary source, and treat them as unverified. The generic Mirai second-stage pattern (a dropper fetches an architecture-matched binary, the host joins a DDoS swarm and scans for more victims) is well established for PHP and IoT targets (Qualys TRU).

Targeted, agentic intrusion

The most serious reported case is Unit 42's. At a European-government target, within two hours of initial exploitation the attacker reached password hashes and MFA secrets, stood up an NTLM-relay tunnel into Active Directory, and attempted Domain Admin escalation. Reconnaissance ran through a web shell in short, serially-spaced batches with quick pivots after failures, and the command output was wrapped in section-header delimiters (==SHELLUSER== and ==PASSWD==) producing machine-parseable text for an AI model to consume. Unit 42 attributes the post-exploitation to agentic AI attack platforms, and notes a European government took its website offline as a precaution (Unit 42). (Unit 42's 23 October brief contains a typo rendering the second identifier as "CVE-2019-109619"; the correct identifier is CVE-2026-109619.)

The two profiles (opportunistic botnet operators and at least one capable, AI-assisted actor) are consistent with multiple actors using the same bug, which SOCRadar also infers from the variety of payloads and the shared anonymisation infrastructure (SOCRadar).

/ 10Impact

PHP2Shell sits below the application, and that single fact drives its severity. Patching your application, adding a framework WAF rule, or hardening your code does not remove it. Sansec notes its own Shield rules run after the vulnerable code, and Calif reports the condition reachable behind Magento, WordPress, Drupal, Joomla, phpMyAdmin, Nextcloud, Laravel, Symfony and others. On mod_php the bad state is consumed during request startup, so application logs may hold little context. A successful attacker gets code execution as the web worker account (www-data, apache, nginx, wwwrun, iusr, or an IIS app-pool identity), and from there the usual path is application config and .env files, database credentials, cloud metadata and API keys, then lateral movement. The Unit 42 case shows that reaching hashes and MFA secrets can take two hours, not days (Sansec; Calif; Unit 42).

Two consequences deserve emphasis for Saudi operators weighing their exposure:

  • The memory-disclosure path leaks even without a shell, so any worker that has been internet-exposed since mid-October should have its reachable secrets treated as potentially compromised.
  • Failure is cheap for the attacker: a crashed worker is simply replaced, so brute-force calibration costs almost nothing, which is why honeypots see hundreds of near-identical attempts from a single source.

/ 11Timeline

The weakness class is not new. Interrupting PHP through memory_limit was documented as far back as 2004, and a 2024 OSTIF/Quarkslab audit noted early signs of the OOM problem without proving exploitation. The exploitable chains, and the coordinated response, came in 2026.

  • 2004

    Interrupting PHP through memory_limit is first documented.

  • 2024

    An OSTIF/Quarkslab audit notes early signs of the OOM problem, without proving exploitation.

  • 11 AUG 2026

    Calif reports a first bug to PHP (under OpenAI's "Patch the Planet" effort).

  • 24 AUG 2026

    Wiz reports its findings to PHP (same effort).

  • 17 OCT 2026

    Amazon's honeypots record the first exploitation attempts, four days before the patch, from commercial anonymisation infrastructure.

  • 20 OCT 2026

    Details leak publicly and a proof-of-concept video is mistakenly published ahead of schedule, which the PHP Foundation says accelerated PoC development. VulnCheck reproduces the bugs that evening.

  • 21 OCT 2026

    PHP releases 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 with both advisories. Cloudflare, which shipped WAF protection at 03:02 UTC hours ahead of the release, observes attackers mutating payloads to evade it.

  • 23 OCT 2026

    PHP publishes an informational advisory on a third, lower-severity variant, CVE-2026-115560 (a session-upload-progress edge case when session.serialize_handler is switched at runtime), to be handled in the regular release cycle. The same day, Unit 42 publishes its post-exploitation observations.

Sources: php.net; SecurityWeek; VulnCheck; Wiz; Unit 42.

/ 12Recommendations

For site operators and developers (prevention)

  • Update. This is the only complete fix. Move internet-facing Apache mod_php first, then PHP-FPM, FrankenPHP and LiteSpeed, then internal systems, to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 or a vendor backport whose changelog names CVE-2026-109618. A backport keeps its old version number, so verify by changelog, not by php -v (php.net).
  • Restart every worker. Restart PHP-FPM, Apache or the container, and rebuild any image that bundles PHP, so no old worker survives.
  • Apply the stop-gaps only as a bridge, never as a destination. If you genuinely cannot patch yet: set session.upload_progress.enabled=0 in php.ini or pool config (not ini_set()), set fatal_error_backtraces=0 on PHP 8.5, lower post_max_size, max_file_uploads and max_input_nesting_level, and block multipart/form-data at the edge where the app does not use it. PHP's own developers warn each setting removes only one producer or consumer (GHSA-6wmf-4rv5-p3rp; php.net).
  • Do not cap memory with OS limits. RLIMIT_AS, RLIMIT_DATA, Apache RLimitMEM, systemd LimitAS= and ulimit -v trigger the vulnerable path and make things worse.
  • Inventory every PHP you run, including bundled copies inside appliances and legacy servers outside the patch cycle.

For defenders (detection and response)

  • Lead with the host signal. A PHP worker spawning a shell or interpreter is the single strongest indicator; build and tune the rule in the next section before anything else.
  • Rotate exposed secrets. Treat memory disclosure as possible for any worker internet-exposed since 17 October: rotate database, API, session and signing keys, and any reachable TLS keys, and review access logs for that window.
  • After a confirmed hit, isolate and preserve the host and its memory, rebuild from a known-good image where a shell ran, and do not rely on deleting the web shell alone.
  • Harden to limit the next one: run workers as low-privilege accounts in per-site pools or containers, deny outbound connections from web servers by default, keep secrets out of files the worker can read, and alert on any child process of a PHP worker.

/ 13Detection content

The exploit runs inside the engine, so network signatures catch attempts and host telemetry catches success. The exploit can return HTTP 200 and leave the worker running, so detection must be behaviour-led, not response-code-led (Datadog).

Tier 1: host process lineage (highest value)

Alert when any PHP worker spawns a shell or interpreter:

sigma · php_worker_spawns_shell.yml
title: PHP worker spawns shell or interpreter (possible PHP RCE / PHP2Shell)
logsource:
  category: process_creation
detection:
  parent_php:
    ParentImage|endswith: ['/php-fpm', '/php-cgi', '/httpd', '/apache2', '/frankenphp', '/lsphp']
    ParentImage|contains: 'php'            # covers php-fpmN.N, lsphpNN
  parent_iis:
    ParentImage|endswith: '\w3wp.exe'      # PHP under IIS
  child_exec:
    Image|endswith: ['/sh', '/bash', '/dash', '/python', '/python3', '/perl',
                     '/ruby', '/node', '\cmd.exe', '\powershell.exe', '\pwsh.exe']
  condition: (parent_php or parent_iis) and child_exec
falsepositives:
  - Monitoring plugins (Nagios, Zabbix, LibreNMS)
  - Hosting panels (cPanel, Plesk, DirectAdmin)
  - Phusion Passenger spawn helpers
level: high
LISTING 7 · A Sigma rule for the core host signal. Datadog ships equivalent coverage as Workload Protection rules potential_web_shell_v2 / potential_web_shell_parent_v2 and Cloud SIEM rule def-000-9h1. (Datadog; Unit 42)

Tier 1b: second-stage behaviour (XQL-style logic)

Raise severity when the child of a PHP worker also matches Unit 42's signal categories: any standalone revshell / pipe_to_shell / writable_exec, OR three or more distinct recon tools, OR recon plus a follow-on signal.

SignalMatches
revshell / pipe_to_shell/dev/tcp/, nc -e, socat ... exec:, mkfifo, sh -i, pty.spawn(, curl ... | sh
writable_execExecution from /tmp, /var/tmp, /dev/shm, a memfd: or a (deleted) image
download / stagingcurl -o, wget to a file, certutil -urlcache, bitsadmin /transfer, then chmod +x in a temp path
cred_accessReads of .env, .aws/credentials, /proc/<pid>/environ, 169.254.169.254, metadata.google.internal
reconBursts of whoami, id, uname -a, cat /etc/passwd, ip a, ps aux from the web account, especially when output is wrapped in section markers
Source: Unit 42

Tier 2: log strings

The strongest log tell is a memory-exhaustion error located in startup rather than a script:

error.log · signatures
# PHP / server error logs: OOM in request startup, not in a script (Datadog-confirmed)
PHP Fatal error:  Allowed memory size of 16777216 bytes exhausted ... in Unknown on line 0

# Apache worker segfault logged when an exploit guessed an address wrong (Datadog-confirmed)
AH00052: child pid NNNN exit signal Segmentation fault (11)

The in Unknown on line 0 location means the failure happened before any script ran; treat a short burst on one worker as possible setup activity even without a crash. Patched builds may log a worker _exit(137) when memory crosses the new hard ceiling, which is useful only when correlated with the above, not as a standalone signal. (The strings zend_mm_heap corrupted and bare "SIGSEGV", and 500/502 bursts as a primary signal, circulate in some write-ups but are not supported by Datadog's article; treat them as unverified.) (Datadog)

Tier 3: network / WAF (attempt detection)

Alert on multipart/form-data bodies with hundreds of parts, a multi-megabyte fill field, or deeply nested machine-generated field names; on PHPSESSID and PHP_SESSION_UPLOAD_PROGRESS fields in apps that do not use them; on stalled uploads (a large Content-Length that never completes); and, as the memory-disclosure tell, on unusually long, percent-encoded or binary Set-Cookie: PHPSESSID= values, including on pages that never start a session. WAF engines may truncate bodies or miss chunked content, so verify what yours actually inspects. Vendor coverage shipped quickly: Palo Alto Threat ID 543004 (plus best-practice signatures 97622–97625), and managed rules from Cloudflare, F5, Fastly and Imperva. That is virtual patching, not a fix (Datadog; Unit 42).

/ 14Summary

PHP2Shell is a textbook demonstration of a rule the Zend engine quietly violated for over a decade: a non-local jump is only safe if nothing between "publish" and "finish" can fail. Two CVEs, two listings (a count raised before a bucket is filled, and a global pointer left aimed at a freed heap) account for the entire class, and the patch is as simple to describe as the bug: defer the unwind to a safe point, and reset request-owned state unconditionally.

For defenders the practical shape is equally simple. Patch to the October releases and restart every worker; nothing short of that is reliable. Until then, watch the one thing the attacker cannot hide no matter how elegant the memory corruption: a web server process spawning a shell. Exploitation attempts are already global and began before the patch existed; reliable, portable RCE is still hard today but is being generalised with AI assistance, so the window in which "high complexity" protects an unpatched host is closing.

▸ Bottom line

Treat any internet-facing PHP as a priority, and treat the secrets it has touched as already spent.

/ APPENDIX AIndicators of compromise

All network indicators are defanged. Confidence reflects how specific each indicator is to this activity.

IndicatorTypeConfidenceNote
PHP worker spawns sh/bash/python/perl/node/php -r/cmd/powershellBehaviourHighThe core detection (Listing 7)
Output wrapped in ==SHELLUSER==, ==PASSWD== section markersBehaviourHighUnit 42: machine-parseable output for an AI model
php2shell-scan/109618User-AgentHighVulnCheck scanner
php2shell-scan/1.3 (eigen-oppervlak controle; CVE-2026-109618/109619)User-AgentHighVulnCheck scanner
php2shell-verify/1.0User-AgentHighVulnCheck verification pass
Mozilla/5.0 (php2shell-poc)User-AgentHighVulnCheck PoC
PWNED:: in any HTTP response bodyStringHighVulnCheck: marker of successful execution
Multipart boundary ------p2s9rdme2st6guc9g0rafic; upload filename p2s.txtNetworkMediumVulnCheck observed attempt
Request path /download.php?file=php://filter/...NetworkMediumVulnCheck: session-file read attempt
Session file sess_xqsuvwz3n97wu8j3emz1axuo5y (PHPSESSID xqsuvwz3n97wu8j3emz1axuo5y)HostMediumVulnCheck observed attempt
Allowed memory size ... exhausted ... in Unknown on line 0Log stringMediumOOM in startup; also benign under heavy load
Oversized / malformed Set-Cookie: PHPSESSID= on multipart responsesNetworkMediumMemory-disclosure signature (Wiz/Datadog)
New .php file in web root or /tmp with no change recordHostMediumPossible web shell
85[.]11[.]167[.]243 (AS209630, NL)IP · scanningLowVulnCheck, 21 Oct; hit 188 canaries
45[.]198[.]224[.]169 (AS215925)IP · probingLowVulnCheck, 21 Oct
109[.]71[.]252[.]97 (Datalix, DE)IP · probeLowVulnCheck, 21 Oct
221[.]14[.]159[.]134 (AS4837, CN)IP · exploit attemptLowVulnCheck, 21 Oct (drop 404'd)
91[.]90[.]123[.]25 (AS9009, M247)IP · verificationLowVulnCheck, 21 Oct
⚠ IP caveat

Scanning sources sit largely on commercial anonymisation infrastructure, which SOCRadar notes points to multiple actors. Use these IPs for retrospective log search, not blocklisting. Unverified indicators to avoid over-trusting: the "Potassium" botnet name and its specific file/process/DNS artifacts (e.g. /tmp/nfs, fallback resolvers) circulate in secondary reporting but are not confirmed in any primary source; only a generic Mirai-variant tie-in is confirmed (VulnCheck; SOCRadar; SecurityWeek).

/ APPENDIX BMITRE ATT&CK mapping

No retrieved primary source published an explicit ATT&CK matrix; the mapping below is Cignify's analytical derivation, each technique tied to observed behaviour (Unit 42; VulnCheck; Datadog).

TacticTechnique
Initial Access TA0001Exploit Public-Facing Application (T1190)
Execution TA0002Command and Scripting Interpreter (T1059), incl. Unix Shell (T1059.004), PowerShell (T1059.001)
Persistence TA0003Server Software Component: Web Shell (T1505.003), Scheduled Task/Job: Cron (T1053.003), Account Manipulation: SSH Authorized Keys (T1098.004), Create Account (T1136), Create or Modify System Process (T1543)
Privilege Escalation TA0004Valid Accounts: Domain Accounts (T1078.002)
Defense Evasion TA0005Reflective / memfd execution (T1620), File and Directory Permissions Modification (T1222), writable-path execution from /tmp, /dev/shm, memfd
Credential Access TA0006OS Credential Dumping (T1003), Unsecured Credentials: Credentials in Files (T1552.001) / Private Keys (T1552.004) / Cloud Instance Metadata API (T1552.005), Multi-Factor Authentication Interception (T1111), Forced Authentication / NTLM relay (T1187)
Discovery TA0007File and Directory Discovery (T1083), System Information Discovery (T1082), System Owner/User Discovery (T1033), System Network Connections Discovery (T1049), Remote System Discovery (T1018), Account Discovery (T1087), Network Service Discovery (T1046)
Lateral Movement TA0008Remote Services: SMB/Windows Admin Shares (T1021.002), Use Alternate Authentication Material: Pass-the-Hash (T1550.002)
Command and Control TA0011Application Layer Protocol (T1071), Non-Application-Layer Protocol (T1095), Non-Standard Port (T1571), Ingress Tool Transfer (T1105)
Exfiltration TA0010Exfiltration Over C2 Channel (T1041)
Impact TA0040Network Denial of Service (T1498), Endpoint Denial of Service (T1499), Mirai-derived second stage

/ REFERENCESSources

Primary sources

  1. php.net: 2026 security advisories, release notes and 23 Oct informational advisory
  2. GHSA-w8hw-w9fx-j79w: RCE via OOM corruption (CVE-2026-109618)
  3. GHSA-6wmf-4rv5-p3rp: RCE via stale global state (CVE-2026-109619)
  4. Calif: PHP2Shell engine-level analysis
  5. Wiz Research: PHP2Shell technical analysis
  6. Unit 42: threat brief on CVE-2026-109618 / CVE-2026-109619 (updated 23 Oct)
  7. Sansec: PHP2Shell research and EOL backport table
  8. VulnCheck: PHP2Shell canary telemetry and exploit status
  9. Datadog Security Labs: detection content and log signals

Additional reporting (secondary; claims attributed in text)

  1. SecurityWeek: patches land as first exploitation attempts hit sensors (Amazon, watchTowr, Hadrian, Mirai tie-in)
  2. SOCRadar: PHP2Shell overview and screening
  3. Island.io: memory-disclosure and multipart-OOM behaviour
  4. php.watch: PHP 8.5 fatal_error_backtraces
  5. Qualys TRU: generic Mirai second-stage pattern on PHP/IoT targets
Less firmly sourced claims, flagged in the text

The "Potassium" botnet name and its file/DNS IOCs (unconfirmed in any primary source; only a generic Mirai tie-in is confirmed); the OPcache map_ptr type-confusion case (not found in a primary source); the exact C symbol for the PHP 8.5 fatal-error backtrace global (behaviour confirmed, identifier not); and the zend_mm_heap corrupted / bare-SIGSEGV / 500-502-burst log signals (not supported by Datadog).

Disclaimer This report is published by Cignify for defensive cybersecurity and research purposes only. It describes the root cause, impact and detection of CVE-2026-109618 and CVE-2026-109619 so that organisations can protect themselves. It deliberately contains no working exploit, proof-of-concept request, memory-corruption primitive, memory offset, ASLR-bypass chain or web-shell code. Act only on systems you own or are explicitly authorised to test. Cignify accepts no liability for misuse of the information herein.
Cignify Threat Research
Riyadh · Offensive Security
Running PHP in production?

Find out if you're exposed before an attacker does.