PHP2Shell: The Request Ended, the State Survived
Hypothetical triage: a multipart POST reaches Apache, a PHP memory-limit error follows, and the application never records entry into its controller. The tempting conclusion is a rejected upload. The more useful question is what the worker did during shutdown, before application logging could help. PHP2Shell makes that distinction operational: one failure exposes partially initialized request state to cleanup; the other carries request-owned state into a later request.[10][11]
This analysis covers CVE-2026-109618 and CVE-2026-109619, with primary sources checked on November 6, 2026. Both CNA records assign CVSS 4.0 scores of 9.2, with no authentication or user interaction, but high attack complexity and additional attack requirements.[10][11] Critical does not mean every PHP deployment accepts the same request or offers the same exploitation path.[10][11]
Two bugs, two different clocks
For CVE-2026-109618, PHP’s OOM handling uses longjmp() to abandon the current context.[10]
Request memory can already be reachable without being fully initialized.[10]
An allocation failure interrupts that intermediate state, and shutdown subsequently reads or branches on data that was never made valid.[10]
This is not simply an application forgetting to catch an exception.[10]
Upstream identifies multipart amplification paths: header representations can consume more memory than their wire size, empty file controls still generate metadata, and nested names multiply array work.[6] Requests without a usable declared content length introduce another size-checking problem.[12] The failed allocation need not belong to the malformed state that cleanup later encounters.[6]
For CVE-2026-109619, the clock crosses a request boundary.[11] A worker-global reference points into request memory; the request ends and releases that memory; a later request touches the reference before ordinary initialization replaces it.[3] The CNA description explicitly includes state that was never reset and state repopulated after reset.[11]
The useful attack-chain vocabulary is producer, interruption, release, consumer. The producer publishes request-owned state.[12] A fatal interruption can skip its matching reset.[12] A late diagnostic can also repopulate state already cleared.[7] The consumer must then act before normal initialization.[11] Both requests must reach the same worker.[3] More workers reduce the chance per attempt, not the existence of the defect.[7]
Do not collapse these into one upload bug. OOM can participate in both chains, but the second class also admits non-OOM interruptions.[10][11][12] A control that removes a predictable memory-limit failure does not prove request-global lifetime correctness.[10][11]

The upper chain fails within one request. The lower chain requires state to survive into a later request handled by the same worker. This is a conceptual map, not exploit syntax.
The SAPI changes the reachable chain
For the OOM class, upstream confirms unauthenticated HTTP RCE against Apache mod_php, including ordinary multiworker prefork configurations.[6]
Startup body parsing can fail before application code runs, yet Apache still invokes shutdown.[6]
FPM instead exits a child when request startup fails.[6]
That breaks this startup sequence, but source analysis still finds FPM susceptible through OOM after successful startup; the advisory does not claim a complete demonstrated FPM exploit for this CVE.[6]
The stale-state advisory has different evidence.
It confirms session-lifecycle RCE for both mod_php and FPM.[7]
The validated FPM route publishes state during application execution after startup, suffers a fatal error, and later reaches an early upload-progress consumer.[7]
Do not transfer the first advisory’s narrower FPM evidence statement to the second vulnerability.[3][7]
Configuration placement matters.
Apache per-directory INI overrides and frontend-supplied FPM PHP_VALUE or PHP_ADMIN_VALUE diagnostics can run early.[7]
Normal FPM pool initialization and .user.ini do not constitute that same pre-initialization diagnostic consumer.[7]
Session upload progress provides a separate early consumer, enabled by default, without requiring the application to call session_start().[7]
These are reachability distinctions, not an FPM exemption.[7]
FrankenPHP and LSAPI also appear in upstream’s susceptibility analysis, but candidate paths and confirmed RCE are not interchangeable labels. Record the SAPI, frontend parameter forwarding, loaded extensions, effective configuration and attacker-reachable application operations before making an exposure assertion.[6][7]
Patch the runtime, not the dashboard number
The first fixed upstream releases for both requested CVEs are 8.2.35, 8.3.36, 8.4.27 and 8.5.12.[3] The supported-branch CNA ranges identify earlier releases in each corresponding branch as affected.[10][11] They are not a finding that unlisted, unsupported branches are safe.[10][11]
Distribution packages require a different comparison.
Vendors may backport fixes without adopting the newest upstream version string.[8]
Bind the installed package release to the vendor advisory, then bind that package to the process actually serving requests.
Red Hat explicitly distinguishes restarting httpd or php-fpm from replacing a container or pod.[2]
The OOM patch changes the failure mechanism: a delayed soft OOM reaches a VM safe point; a higher hard threshold exits immediately.[6] The stale-state patch restores the separate lifetime invariant by resetting request-owned state before later early consumers can encounter it.[7] Those properties explain why reducing upload size is not equivalent to installing both fixes.[6][7]
A closure record should include package identity or image digest, advisory coverage for both CVEs, SAPI, worker replacement evidence, and a private check through the actual serving pool.
CLI PHP alone is insufficient: it can load a different binary or configuration.
Do not leave a public phpinfo() endpoint behind.
This is the same artifact-to-runtime binding problem discussed in the site’s SBOM and VEX decision pipeline.
Workarounds break links, not the whole class
If patch deployment is temporarily blocked, follow the current vendor instructions as a bounded exception.
Red Hat’s guidance disables session.upload_progress.enabled and requires worker restart.[2]
On PHP 8.5, disabling fatal_error_backtraces removes the confirmed backtrace producer.[2]
Neither setting covers every possible request-state family.[2]
For the OOM path, administratively enforced memory_limit=-1 removes the predictable PHP per-request limit, but also removes an availability control.[2]
An underlying allocation failure can still reach vulnerable handling.[2]
Use an external availability boundary and conservative decoded-body limits; do not substitute RLIMIT_AS or RLIMIT_DATA, which can force the dangerous allocation failure.[8]
A smaller post_max_size is risk reduction, not a patch.[2]
Apply settings before parsing, prevent lower-scope overrides, and restart every serving worker. Treat error suppression as cosmetics, not lifetime repair: the stale-state consumer can access data before PHP decides whether to display or log a diagnostic.[7]
Keep the third issue separate.
PHP’s October 23 guidance for CVE-2026-115560 describes a specific combination: configure php_binary or php_serialize, switch to php at runtime before session_start(), and leave upload progress enabled.[8]
As checked November 6, upstream still recommends disabling upload progress until the next regular bugfix release for that path.[8]
The October 21 fixes for the two requested CVEs must not be represented as fixing this third issue.[8]
A bounded mechanism check, not a PHP exploit
For this article, a symbolic Python model was executed behind a single loopback HTTP worker. It does not run PHP, corrupt memory, validate RCE, or prove vendor patch completeness. Its purpose is narrower: make the two lifetime invariants observable without inventing a product-specific payload.
The OOM branch publishes an uninitialized marker, then models early exit versus initialization before cleanup. The cross-request branch publishes a marker, models request-memory release, and checks a later request through the same worker. The fixed branch clears the reference before release. The recorded assertions were:
vulnerable_invalid_cleanup = true
fixed_invalid_cleanup = false
vulnerable_stale_reference = true
fixed_stale_reference = false
bind = 127.0.0.1
listener_closed = true
A successful model run demonstrates only the model. It cannot resolve heap-layout requirements, extension-specific producers, frontend transformations or exploit reliability. Keep that evidence boundary when evaluating third-party demonstrations, just as with generated fuzzing harnesses and evidence gates. No exploit was executed against this site or another remote target.
Hunt the transition, then contain the worker identity
As of the November 6 check, CISA’s current KEV feed lists both exact CVEs, added October 26.[9] Both entries mark known ransomware use Unknown and forensic triage Yes.[9] KEV establishes exploitation evidence, not a particular victim count, actor or universal exploit recipe. Its federal directive obligations should not be presented as a deadline binding every private operator.[9]
For triage, preserve frontend request metadata, PHP and service logs, worker identifiers, crash artifacts and deployment history before rotating away evidence. Correlate repeated multipart failures or fatal errors with the same worker’s later requests. Then look for changes in worker behavior: unexpected child processes, outbound connections, new executable files, modified startup paths or access to application credentials. These are hunting hypotheses, not published PHP2Shell-specific IoCs.
Do not require a noisy warning to open the investigation, and do not equate an empty error log with absence of exposure. Upstream explicitly warns that relevant diagnostics may be filtered, unexercised or reached before logging succeeds.[7]
The demonstrated privilege boundary is the web worker account, not automatic root access.[6] Scope containment around that account’s writable paths, secrets and outbound authority. If an implant is found, preserve and correlate it before deletion; the site’s PHP web-shell hunting case is relevant to that follow-on investigation, not evidence that those older compromises used PHP2Shell.
The decision point is concrete: replace vulnerable serving processes with vendor-verified builds, prove both fixes are present, and keep any remaining configuration exception tied to its exact attack path. A quiet controller log, a larger worker pool and a clean CLI version are not substitutes for that evidence.
Sources
- Red Hat: PHP2Shell security bulletin RHSB-2026-013
- PHP Security Team: CVE-2026-109619 announcement
- PHP upstream: RCE via OOM corruption
- PHP upstream: RCE via stale global state
- PHP 2026 news archive and security advisories
- CISA Known Exploited Vulnerabilities catalog (JSON)
- CVE Program CNA record: CVE-2026-109618
- CVE Program CNA record: CVE-2026-109619
- PHP2Shell: Calif Research technical account
💜 Enjoyed this content? Support the blog with USDT (TRC20):
TX7obcjHQbDUXb4mGqoASEu1QFTKT2CFGG
