
Table of contents
18
By Salman Khan, OSCP+, Founder of XHack, SRT (Synack Red Team member)
Read this in 30 seconds: XHack AI reproduced the PHP2Shell exploit in a disposable PHP 8.4 Docker lab, reached command execution as
www-data, and repeated the result after the target’s memory layout changed. The research also found that the same early failure left a reusable worker under Apachemod_phpand PHP’s built-in server, while a PHP-FPM worker exited in this specific test.
- A failed probe nearly hid a working exploit. A CoHunter found that XHack AI’s address filter rejected a valid memory disclosure before the tool could evaluate it.
- The proof went beyond a crash or a large cookie. The team recovered addresses, bypassed ASLR in the lab, and read
uid=33(www-data)from a localhost web shell.- Server behavior changed this chain’s outcome.
mod_phpand the built-in server kept the affected worker alive; PHP-FPM discarded it for the tested startup path. PHP-FPM still needs the security update.- Configuration shortcuts did not settle the risk. Hiding errors, enabling strict session mode, and reducing a tested body limit did not prevent the demonstrated
mod_phpchain.
An exploitability claim should end with evidence from the target, not a screenshot of an error page. That was the standard behind XHack’s PHP2Shell research case study. The question was whether a PHP 8.4 worker could carry bad state from one unauthenticated request into another and end in real code execution.
The answer in XHack’s controlled mod_php lab was yes. More interestingly, the route to that PHP2Shell result exposed two ways a research team can fool itself: reading the wrong fields from a running process and throwing away a valid disclosure because a filter expects the wrong address range. The CoHunters fixed both measurement errors, completed the chain, and then tested what changed when the server model changed.
This article is the analysis of that original work. For the broader CVE timeline, affected branches, and general vulnerability background, see our PHP2Shell vulnerability explainer. The findings below stay within the PHP 8.4 configurations and local targets that XHack actually tested.
PHP’s security team released fixes on October 21, 2026 for CVE-2026-109618 and CVE-2026-109619, both rated 9.2 under CVSS 4.0. One issue concerns out-of-memory handling while request data is only partly initialized. The other concerns global state that outlives the request memory it points into. PHP’s security announcement names web-facing PHP installations across several server models and identifies demonstrated unauthenticated code execution against mod_php.
XHack did not discover these CVEs and does not claim to have tested every deployment. Its independent PHP2Shell question was narrower: can XHack AI reproduce a full PHP2Shell exploit on a known PHP 8.4 build, measure why it works, and explain where the same request chain stops?
That distinction matters. An out-of-memory response only shows a failed request. A crash can be security relevant, but it does not show that attacker-controlled state survived. A memory disclosure is a stronger signal, yet it is still short of command execution. The claim of remote code execution required a final, observable result from the disposable target.
The working lab used upstream PHP 8.4 Docker images and Apache mod_php. Two comparison targets used nginx with PHP-FPM and nginx in front of PHP’s built-in web server. All traffic stayed on localhost. XHack controlled the containers, application, configuration, and test requests. No third-party or production system was part of the experiment.
The research team could inspect PHP source and the live process, so it did not have to infer internal state from a web response. That direct view became decisive when external output was misleading.

XHack separated the chain into four experimental checkpoints. These are its test stages, not a claim that each CVE maps cleanly to one step.
| Checkpoint | Evidence the team required | What it did not prove by itself |
|---|---|---|
| Corrupt | The early failure occurred at the useful point in request startup | That stale state survived |
| Disclose | A later request returned process data in an oversized session cookie | That the addresses could be used for control |
| Prepare | The same worker carried the state needed for the final step | That code had executed |
| Control | The local target returned command output from the web server account | That every PHP deployment is exploitable the same way |
The key PHP2Shell dependency is worker continuity. PHP workers may serve more than one request. For this chain, an initial request had to leave request-owned state behind, and a later request had to reach the worker holding it. The number of attempts could rise when a server had multiple workers or unrelated traffic. A single failure was not a reliable safety verdict.
The lab produced an oversized Set-Cookie header. In context, that was a clue that process data had escaped into the response. Yet the test tool labelled the attempt unsuccessful. If the team had accepted the verdict, it would have missed a usable disclosure in a vulnerable target.
B1-HeapGroomer, a CoHunter assigned to the stuck disclosure stage, traced the problem to the analysis tool. Its filter checked returned pointers against an address range that did not fit the Docker platform. A valid value arrived, but the filter rejected it before the exploit could use it. Sending more requests could not fix that logic error; each valid value in the same range would be discarded again.
Once the filter matched the environment, the previously hidden PHP2Shell disclosure became usable. PHP2Shell Exploit Engineer, the CoHunter responsible for the larger chain, could take that finding into the next stage without restarting the investigation. The case study records both roles and the handoff.

There was a second PHP2Shell measurement problem. An early harness read PHP’s session globals using a field layout that did not match the exact PHP 8.4 binary in the container. The values looked plausible, which made the error more dangerous than a hard failure. XHack derived the layout from the binary under test before relying on the live readings.
These were separate mistakes. One read the wrong memory fields; the other filtered out a real response. Fixing them did not make PHP more vulnerable. It made the experiment capable of seeing what the target was already doing.
The cookie was a signal, not the final finding. A large response can have several explanations; XHack had to connect it to state in the PHP worker and then to usable addresses. The CoHunters inspected the process to see whether the old session identifier and a flag to send a cookie survived after the first request.
That is the practical difference between a scanner alert and PHP2Shell exploitability research. The alert says something abnormal happened. The research asks whether the abnormality persists, which process owns it, and whether it changes what a later request can do.
The team also resisted an attractive but false assumption: that the exploit required a visible out-of-memory error. It later completed the chain with display_errors off. A check that waits for the error text on a page could therefore return a false negative even when the bad state remains.
The mechanism in PHP’s out-of-memory advisory starts before ordinary application execution. PHP can parse a multipart request and initialize session upload progress during input processing. If memory exhaustion interrupts that work at the wrong point, cleanup may not see a fully initialized request.
The related stale global state advisory explains why a later request matters: worker-global fields can still refer to memory owned by a request that has ended. When the worker begins another request, early operations may read that stale state before normal initialization replaces it. The result can be a use-after-free.
XHack’s PHP2Shell path connected those two behaviors. The tested multipart request reached the early out-of-memory point while session state was being prepared. Under mod_php, the worker returned from that failed request and stayed available. The next request could encounter the state it had left behind.
This was not a claim that every multipart POST is an exploit. The timing of the failure, the state left in the worker, and the later request reaching the same worker all mattered. XHack measured the internal point of failure instead of treating any out-of-memory page as equivalent.
The team repeated PHP2Shell tests with fresh Docker targets because a previous attempt could alter a worker. It also varied settings one at a time and compared groups of runs. Those controls exposed a narrow range where its fixed request shape reached the useful failure point. The case study withholds the exact memory limits and body sizes. They describe one lab experiment and would be a poor substitute for patch guidance anyway.
The mod_php chain did not end at disclosure. XHack AI recovered the PHP code address and another address needed for final control from the response. That let it get past address space layout randomization, or ASLR, in the tested process. The final stage executed a command as the web server user.
A browser request to the lab’s localhost web shell returned uid=33(www-data). That output matters because it comes from the target account, not from the exploit tool’s own success banner. The team then restarted into a different process address layout and completed the full chain again. The second result showed that the disclosure and address recovery still worked when the original addresses were no longer valid.

This supports a precise PHP2Shell conclusion: XHack AI reproduced unauthenticated remote code execution on the tested PHP 8.4 mod_php build, and it repeated that result across two different address layouts. It does not provide a measured success rate for the internet, a guarantee against every build, or a claim that every request will work.
The distinction between impact and reliability is important here. Execution as www-data is a serious foothold. What that account could read or reach on a real host would depend on file permissions, credentials, isolation, and the application’s environment. XHack proved the foothold in its own container; it did not test any real organization’s downstream access.
With one worker, the follow-up request had an obvious path back to the process holding the stale state. With Apache’s default prefork arrangement in the lab, it could land in another child. The disclosure therefore succeeded only some of the time. More workers and unrelated traffic changed the attempt count without removing the underlying condition.
The CoHunters did not use a lucky PHP2Shell screenshot as a reliability estimate. They restarted targets, repeated samples, and inspected worker state to separate a real failure of the chain from a request that simply reached the wrong process. That same discipline explains why the mistaken address filter was consequential: it turned a valid success into a tool-reported failure regardless of how many attempts the team made.
XHack sent the same early PHP 8.4 startup request through three server models. All three reached an early out-of-memory failure. The difference was whether the worker that held the bad state survived for a later request.
| Tested server model | Worker after the early failure | Result for this request chain |
|---|---|---|
Apache mod_php | Survived and accepted another request | Full chain reached RCE |
| nginx with PHP-FPM | Exited; replacement worker began clean | This startup chain lost its stale state |
| PHP built-in server behind nginx | Survived and accepted another request | Full chain reached RCE |
The PHP-FPM observation has a narrow PHP2Shell meaning. It shows that this early-startup route failed to carry state across requests in the tested FPM setup. It does not mean PHP-FPM is safe from CVE-2026-109618 or CVE-2026-109619. PHP’s advisories cover other routes through the underlying issues, and the PHP project tells operators of web-facing PHP installations to update.
XHack checked the FPM behavior against the running processes. Harmless repeated requests retained the process identifier, while the failing startup request replaced the worker and the pool log recorded an abnormal child exit. An ordinary out-of-memory error during script execution did not have the same effect. The startup result persisted under static, dynamic, and on-demand FPM pool policies in the lab.
The built-in server result is the PHP2Shell counterexample to an easy conclusion that leaving Apache solves the problem. Its worker survived the tested failure, and XHack AI completed code execution there too. The research tool recorded its uncertainty when nginx obscured the backend server model, tried the relevant paths, and reached a verified callback on the fourth attempt. Earlier attempts missed stages of the chain.

That tool behavior is worth more than a green success label. It shows why a quick external fingerprint can be inconclusive, why a missed attempt is not proof of safety, and why XHack kept the final verdict tied to command output. It also sets a limit on the claim: the team tested one PHP 8.4 request path, not all possible paths on every SAPI.
Several controls might look sufficient if the problem is reduced to an error page or a large upload. XHack tested them against its actual chain instead.
| Setting or assumption | Observed result in the PHP 8.4 lab |
|---|---|
display_errors = Off | Hid error text; the full chain still reached execution |
session.use_strict_mode = 1 | Did not stop the tested path |
Smaller post_max_size | The team still reached parsing using chunked delivery at the tested limit |
Default max_input_vars | The tested request stayed below it |
| HTTPS-only access | Did not alter PHP’s request handling |
| Application login | Ran too late to block this pre-application input path |
output_buffering = 0 | Prevented this particular disclosure stage in the lab |
| Disabled session upload progress | Stopped the early session path used by this chain |
Those are PHP2Shell experimental observations, not a menu of complete fixes. A different request path could interact with the same underlying vulnerabilities differently. The team did not evaluate a WAF as a primary control, so this research cannot say whether a particular WAF rule would hold up.
The strongest operating decision is still the PHP update. PHP 8.4.27 contains the fixes for the two CVEs, and a Linux vendor’s verified backport may also provide them. A proxy header, hidden error, failed remote probe, or switch to FPM cannot establish that the installed runtime is fixed.
PHP issued an additional October 23 session upload progress notice about a separate, unusual RCE route. It recommends session.upload_progress.enabled=0 until the next regular bugfix release. Operators should keep that precaution distinct from the two CVE fixes and follow current PHP guidance for their installed branch.
Start PHP2Shell remediation with the PHP package that actually serves the application. Check its version and the distribution vendor’s patch record; a banner from nginx or Apache may describe only the front end. For PHP 8.4, verify 8.4.27 or a vendor-confirmed backport for CVE-2026-109618 and CVE-2026-109619.
Review unusual multipart requests, out-of-memory events, and unexpected worker exits in logs, especially when they occur together. These can justify investigation, but none alone is a reliable verdict on compromise or exploitability. The XHack lab showed both a hidden error during a working chain and an FPM worker exit for a path that did not retain state.
If patching must wait, use PHP’s advisory guidance on session upload progress and decoded request-size enforcement at the front end. Treat these as exposure reductions. The official security announcement says updating is the only complete mitigation for the two CVEs.
This case shows a specific role for XHack AI CoHunters. The human researcher defined an authorized, disposable scope and insisted on proof from the target process. PHP2Shell Exploit Engineer followed the complete chain. B1-HeapGroomer focused on the failing disclosure stage and found the bad filter. Their shared work let the main effort use the corrected result immediately.
XHack AI read the PHP 8.4 source and binary, inspected the worker, changed one lab variable at a time, and required a final localhost command result. Its strongest contribution was not simply finding a path to code execution. It was catching and repairing its own false negative before making the claim.
That is also where the limits of this PHP2Shell research belong. The public case study includes redacted screenshots and experimental results. The working payloads, full request construction, memory layout, and final-stage exploit materials remain private. The tests were run only against containers the researcher controlled.
XHack’s AI agent keeps pentest chats and session data on the user’s local computer, where the user can delete them. For an organization that wants this kind of evidence against its own systems, XHack’s research and red-team team can scope an authorized assessment and report what was actually demonstrated, including inconclusive results and environment-specific limits.
No. The PHP project disclosed and fixed CVE-2026-109618 and CVE-2026-109619. XHack AI performed independent exploitability research after disclosure on controlled PHP 8.4 Docker targets. Its original contribution here was reproducing code execution, diagnosing a measurement false negative, and comparing the tested chain across server models.
It measured state left in a worker, recovered a memory disclosure and the addresses needed for the final stage, reached command execution as www-data, and repeated the full mod_php result after the process address layout changed. A localhost browser response containing uid=33(www-data) supplied target-side proof of execution.
No. The FPM worker exited after the specific early-startup request XHack tested, so that chain lost the state it needed. PHP’s advisories describe other routes through the vulnerabilities and include web-facing FPM installations in the update guidance. Patch the installed PHP runtime regardless of this one lab result.
No. A request may hit another worker, a tool may reject a valid result, a proxy may obscure the backend, or the tested path may differ from the vulnerable one. XHack found both a bad address filter and an incorrect PHP field layout in early measurements. Package and vendor patch verification are stronger evidence of remediation.
No. The case study shares the outcome, redacted screenshots, and the boundaries of the experiments. XHack keeps the working payloads, full request construction, and final-stage exploit materials private. The public evidence is enough to explain the result without turning the case study into a reproduction guide.
XHack’s PHP 8.4 work answers a practical question that a CVE description cannot: did an independent, instrumented test reach code execution, and under which observed conditions? In its mod_php lab, the answer was yes twice. The built-in server also carried the chain to RCE. FPM discarded the stale state for the tested startup route, a useful boundary that does not remove the need to patch.
The most transferable PHP2Shell finding is the false negative. A valid disclosure was present while the tool said it had failed. XHack AI caught that contradiction, corrected the measurement, and required target-side proof before reporting success. For defenders, the action remains straightforward: verify the PHP fix in the runtime actually serving requests, follow PHP’s current session guidance, and treat a quiet error page or failed probe as insufficient evidence of safety.
Categories
Related articles