PHP2SHELL: Independent PHP 8.4 Exploit Research with XHack AI CoHunters
Independent Research (PHP core, CVE-2026-109618 / CVE-2026-109619)
3 days
Critical (9.2)
SeverityPHP 8.4
Research targetPHP 8.4 mod_php
RCE LabYes, twice
RCE ReproducedOn This Page
Key outcome
XHack AI CoHunters reproduced remote code execution twice in a PHP 8.4 mod_php Docker lab, with an ASLR bypass and localhost proof.
Table of Contents
15
The cookie the test almost missed
The PHP 8.4 server sent back a cookie that was far too large. That should have been a breakthrough: the response appeared to contain process memory. Yet XHack AI's test tool called the attempt a failure.
The server had not hidden the answer. The tool had thrown it away.
One XHack AI CoHunter found the mistake in the address filter. Another kept the full exploit chain moving. Once the filter was fixed, the team used the disclosure to bypass memory address randomization and reached remote code execution in a local Docker lab. A browser request to the lab's localhost web shell returned the identity of the web server user, uid=33(www-data). XHack AI then reproduced the result with a different process address layout.
This is independent exploit research into already disclosed PHP vulnerabilities. It shows what XHack AI proved on a tested PHP 8.4 build, how its CoHunters got past a false negative, and what defenders can learn from the result.
On 21 October 2026, PHP published fixes for CVE-2026-109618, an out-of-memory handling issue, and CVE-2026-109619, a stale global state issue. Both carry a CVSS 9.2 rating. The PHP 8.4 fix is 8.4.27. PHP security announcement
A Docker lab built for one question
XHack AI wanted to know whether it could turn the PHP 8.4 bug into code execution, not merely trigger an error. It built disposable Docker targets from upstream PHP images. The working target ran PHP 8.4 through Apache's mod_php module. A second PHP 8.4 target used nginx with PHP-FPM so the team could compare how the same early request failure affected a different worker model.
Everything ran on localhost. XHack AI controlled the containers, PHP settings, application, and test requests. No production server, public service, or third-party host was tested.
The team could inspect PHP source and the live worker process. That mattered because an HTTP error does not tell you which internal state survived. The team needed to see what PHP had actually left in memory, then check whether the next request could reach it.

The question was precise: could an unauthenticated request leave useful state in a PHP 8.4 worker, then use that state to reach code execution? In this mod_php lab, the answer was yes.
Meet the CoHunters
CoHunters are named XHack AI research agents with their own roles, memory, and tool budgets. They can share a team board and continue work across sessions. That makes a difference in vulnerability research: a field layout or failed idea can take hours to establish, and the next run should start from that knowledge.
Two CoHunters did the technical work here:
- PHP2Shell Exploit Engineer worked on the full PHP 8.4 chain in the
mod_phpDocker target. It reproduced the bad state, examined what the worker kept, and pushed toward a complete result. - B1-HeapGroomer had a narrower assignment. The disclosure stage looked stuck, so its job was to find out why the apparent memory leak was not producing usable data.
The split was useful. The exploit engineer could keep the whole chain in view. B1-HeapGroomer could spend its attention on one stubborn stage without being pulled toward the final result.
The human researcher set the scope and required proof from the process rather than a matching error message. XHack AI's CoHunters performed the source review, binary inspection, live measurements, debugging, address recovery, and final validation.
Their shared context also removed a common research tax. A PHP field layout, a working container setup, and a record of failed ideas did not have to be rediscovered at the start of every session. The CoHunters could hand a finding to one another and keep going. In this case, that meant the exploit engineer could use B1-HeapGroomer's filter fix as soon as the disclosure became visible.
The false negative
The disclosure stage was reaching memory, but XHack AI's analysis tool was checking returned pointers against an address range that did not fit this Docker platform. A valid address arrived and the filter silently rejected it. The test reported failure before it had judged the target's response.
More attempts could never solve that kind of problem. Every valid result in that range would be discarded again.
B1-HeapGroomer traced the failure to the filter and corrected it. The stage that had appeared broken then returned usable data, and the CoHunters completed the chain. The screenshot below shows XHack AI reporting that the disclosure stage was solved and that the full chain reached code execution.

There were two lessons in that moment. First, a failed test can point to a bad measuring tool rather than a safe target. Second, the unusually large session cookie was the clue to follow. In this lab, it separated a real memory disclosure from a request that simply failed.

What was happening inside PHP
PHP's two advisories describe related problems, but they are not the same bug. CVE-2026-109618 concerns an out-of-memory exit while request data is only partly initialized. CVE-2026-109619 concerns state owned by one request that can still be present when a later request begins. OOM advisory, stale-state advisory
In the PHP 8.4 lab, the interesting work began before the application ran. PHP was parsing a multipart request, the kind commonly used for uploads. Session upload progress can initialize session state during that parsing step, before PHP reaches its normal module initialization. The tested request then caused an out-of-memory failure at a sensitive point.
Normally, the end of a request clears temporary state before a worker handles another one. The vulnerable timing broke that expectation in the tested path. The first request stopped during startup, while the session module still held data associated with that request. Under Apache mod_php in this Docker target, the worker survived and remained available for the follow-up request.
Here is why that matters. A PHP worker can handle more than one request. If the first request ends while request-owned state is still published in the worker, a later request may encounter it. For this particular chain, XHack AI needed both the old session identifier and a flag telling PHP to send a cookie. It checked those values in the live process instead of guessing from the HTTP response.
An out-of-memory message alone would not have proved anything beyond the failure. The state had to survive. The follow-up request had to reach the same worker. And the response had to carry enough of the disclosure for XHack AI to recover the addresses needed for the final step.
PHP's fixes address the underlying OOM behavior and stale state. They cover more than the session path tested here. PHP security announcement
The patches matter for different reasons. PHP changed the OOM exit so cleanup does not continue with partly initialized request data. It also tightened state handling so request-owned global data cannot be consumed after its request is gone. Fixing only the visible cookie behavior would miss the larger lifecycle problem. OOM advisory, stale-state advisory
How XHack AI measured the chain
The first measurements were messy. On what appeared to be the same untouched container, one three-attempt run scored three successes while another scored none. The team treated that contradiction as a problem with the experiment, not as a verdict on PHP.
The chain passes state between requests in one worker. A previous attempt can change the process. A later request can land in a different worker. A single try cannot tell the whole story. XHack AI changed the harness in two ways:
- Fresh Docker target for each sample. A test started with a clean worker unless it was deliberately measuring state across requests.
- Repeated runs for a claim. The team used more than one sample and checked stability with groups of three or five attempts.
The CoHunters found another measurement trap. An early version of the harness read PHP's session globals using a field layout that did not match the PHP 8.4 binary in the container. It returned values that looked plausible but were wrong. XHack AI derived the layout from the exact binary under test before relying on those readings.
The address filter mistake was separate. One error misread the worker's fields; the other rejected a useful disclosure after it arrived. Fixing both let the team follow the same PHP 8.4 chain from the failing request to the final command result.
Four stages, four checks
XHack AI tracked the lab chain as four stages. These are steps in its experiment, not a one-to-one mapping of the two CVEs.
| Stage | What the CoHunters checked |
|---|---|
| 1. Corrupt | Did the early out-of-memory failure occur at the point this chain needed? |
| 2. Disclose | Did the next request return process data through the oversized cookie? |
| 3. Prepare | Did a later state change set up the final step in the same worker? |
| 4. Control | Did the localhost target actually execute a command? |
The third and fourth stages used addresses recovered from the disclosure. That is how the team got past ASLR, the operating system feature that changes where code is placed in memory. XHack AI did not infer success from a crash or a banner. It required the final command output.
The PHP 8.4 result
In the Apache mod_php Docker target, XHack AI completed the chain twice with different process address layouts. The lab showed:
- An unauthenticated request reached the early out-of-memory state needed by this chain.
- A later request in the same worker returned a large
Set-Cookieheader containing process data. - XHack AI recovered the PHP code address and the address needed for its final control step, defeating ASLR in the lab.
- The final stage executed a command as the web server user.
- The localhost web shell returned
uid=33(www-data), confirming the user context.
The browser proof is shown below. It comes from the researcher's localhost Docker target.

The second full run mattered. Restarting the target changed its memory layout. Reaching code execution again showed that XHack AI's disclosure and address recovery were doing their jobs. The proof remains scoped to the tested PHP 8.4 build and configuration.
Code execution as the web server user is a serious foothold. On a real host, that account may be able to read application files, configuration, or credentials and act on whatever the application can reach. The exact impact would depend on that host's permissions and isolation. XHack AI demonstrated the foothold in its own disposable target and did not test a third-party system.
Why one success was hard to get
The worker had to keep the right state after the first request, and a follow-up request had to reach that same worker. With one mod_php worker, the chain had a clearer path. With Apache's five-worker prefork setup in this lab, the disclosure stage succeeded about once in three attempts because a follow-up request could land in another child process.
That changed reliability, not the result. XHack AI reproduced the full chain, but it did not claim that every request would work. A report that gives only a success screenshot, with no attempt count or process context, misses this important part of the story.
The memory limit also affected this fixed request shape. XHack AI swept the lab setting with a fresh target for each sample. The required failure point appeared only in a narrow range. Exact memory_limit values: [REDACTED]. The team repeated the edge tests with fresh workers and varied parser-related settings across more than sixty samples. Those experiments describe this lab request; the configuration values are withheld.
This is a good example of why the team measured the exact internal point of failure. A request that crashes PHP too early has not reached the state the chain needs. A request that stays below the memory ceiling has not triggered it either. Looking only for an out-of-memory message would have mixed those different outcomes together.
Body size mattered too. Holding the lab setting steady and changing one field's size moved the failure point. Successful body size: [REDACTED]. This explains the chain's sensitivity in one Docker experiment; it does not identify a safe setting for a PHP server. PHP says updating is the only complete mitigation. A different request can reach an out-of-memory failure differently. PHP OOM advisory
Settings that did not save the lab target
Several controls looked promising until XHack AI tested them:
| Setting or assumption | What the PHP 8.4 lab showed |
|---|---|
display_errors = Off |
The full chain still reached code execution. The error text disappeared, but the bad state did not. |
session.use_strict_mode = 1 |
It did not stop this tested path. |
A smaller post_max_size |
The team still reached parsing with chunked delivery; the tested limit is [REDACTED]. |
Default max_input_vars |
The tested request stayed under it. |
| HTTPS-only access | Transport encryption would not change PHP's request handling. |
| Application login | The tested input path ran before application authentication code. |
| A body-inspecting WAF | The team did not evaluate a WAF as a primary control. |
The display_errors result corrected an assumption inside the research itself. The team had first treated visible errors as a requirement. XHack AI ran the full chain with errors hidden and still reached code execution. A remote check that waits for an out-of-memory error on the page could miss an affected target.
XHack AI also measured settings that changed this particular request. With output_buffering = 0, its tested disclosure stage did not fire. Disabling session upload progress stopped the early session path used by this chain. The PHP-FPM startup test lost the state when its worker exited. These are observations from the Docker lab, not a menu of complete fixes. PHP's advisories describe other possible paths through the underlying vulnerabilities.
PHP recommends enforcing decoded request size at the frontend, including requests without a usable Content-Length. That guidance is more useful than treating this lab's redacted measurements as defensive settings. PHP OOM advisory
What the PHP-FPM test showed
XHack AI also ran the PHP 8.4 startup request through nginx and PHP-FPM. The early out-of-memory failure still happened. This time, however, the FPM worker exited. The next request landed in a new worker, so this specific startup chain lost the old state it needed.
That difference comes from where the failure is caught in the tested server paths. In Apache mod_php, the worker returned from the failed request and was able to accept another one. In this FPM startup path, the failure ended the worker's request loop. The next worker started clean. XHack AI checked that behavior in the running processes rather than assuming it from the name of the server.
The team checked that result against normal worker behavior. Six harmless requests kept the same process identifier. The failing startup request changed it, and the FPM pool log recorded child exited with code 70. An ordinary out-of-memory error during script execution did not have the same effect in this lab: that worker returned an error page and carried on.
XHack AI repeated the startup test with static, on-demand, and dynamic pool policies. The worker exit remained the observed result for this request path. That is a useful explanation of why the CoHunters' mod_php chain stopped in the PHP 8.4 FPM target.
It is not a general FPM safety claim. PHP's advisory describes a different, validated session lifecycle RCE path under FPM that begins after successful application startup. Moving an unpatched server to FPM does not replace the PHP fix. PHP stale-state advisory
The worker exit did leave a useful trace in the lab. An abnormal FPM child exit and pool log entry can help an operator investigate this request shape. Other attack paths may leave different traces.
How to check your own PHP 8.4 installation
Start with the installed package and its vendor patch record. PHP 8.4.27 contains the upstream fixes for these two CVEs. If a Linux vendor has backported the patches, use that vendor's security advisory to verify the package. Check the PHP instance actually serving the application; a proxy's Server header can describe something else. PHP security announcement
A small multipart request may tell you whether upload-progress behavior is reachable, but it is not a vulnerability verdict. A missing cookie can have several causes. A visible out-of-memory error shows only that a request failed. Neither result tells you whether the PHP 8.4 installation has the patches or whether another path is reachable.
The reverse is true as well. A server can be affected while showing no error in the response, especially when display_errors is off. An external check may also hit a static route or a different PHP worker. This is why XHack AI read its lab worker directly, and why an operator should verify the installed package rather than trust one HTTP response.
That distinction mattered in XHack AI's lab. The team did not call the chain successful until it had measured the worker, recovered the disclosure, and read the command result over localhost. Defenders do not need to repeat that exploit. They need to confirm the installed fix.
What XHack AI brought to the work
This result took more than sending a large POST request. XHack AI's CoHunters combined several kinds of work, kept the details straight, and checked each claim against the Docker target.
Source and binary work. The agents followed PHP 8.4's request, session, and server paths, then derived the field layout and needed symbol locations from the exact binary in the lab. That caught the early harness error caused by reading the wrong fields.
Live process measurement. A debugger was not available in the container, and attaching one during the out-of-memory state killed the process. XHack AI inspected the worker directly to see whether the session state and cookie flag had survived. It did not have to guess from an error page.
A focused CoHunter handoff. When disclosure looked stuck, B1-HeapGroomer took that one problem. It found the filter that was rejecting a real result. Meanwhile, PHP2Shell Exploit Engineer kept the larger chain moving. Their shared context meant the discovery could be used immediately.
Repeatable experiments. The harness started fresh Docker targets, changed settings one at a time, tracked worker state, and recorded repeated attempts. Several hundred instrumented samples helped XHack AI separate a stable lab finding from a lucky run.
Careful claims. A matching error was not enough. The CoHunters looked for the session state, the oversized cookie, recovered addresses, and the final localhost command output. They also challenged their own earlier belief that visible errors were required. That willingness to correct the test is part of what made the final result credible.
Proof that matched the claim. The CoHunters did not stop at memory corruption or an oversized cookie. They reached command execution, read the web server user's identity, restarted into a new address layout, and did it again.
The best moment in the research was also the least flashy: XHack AI caught a bug in its own measuring tool. Without that correction, a working PHP 8.4 disclosure would have stayed hidden behind a false negative.
What defenders should do
Update PHP 8.4 to 8.4.27 or later, or install a verified vendor backport. PHP says an update is the only complete mitigation for these CVEs. PHP security announcement
PHP also issued a 23 October follow-up about a separate, unusual session upload progress RCE path. It advises setting session.upload_progress.enabled=0 until the next regular bugfix release. Keep that precaution in place when updating to 8.4.27.
If you cannot update immediately, follow PHP's guidance for both advisories. For the confirmed session path, disable session.upload_progress.enabled in administrative configuration and restart workers. Enforce decoded request-size limits at the web server or proxy. These steps reduce exposure, but they do not replace the patch. OOM advisory, stale-state advisory
Keep error logging enabled and investigate unusual multipart POSTs, early out-of-memory failures, and unexpected worker exits. The PHP 8.4 Docker experiments show why a hidden error or a failed test request is weak evidence of safety. PHP-FPM needs patching too.
Independent research outcome and disclosure
After PHP disclosed the CVEs, XHack AI independently built and measured an exploit chain in its PHP 8.4 Apache mod_php Docker lab. Its CoHunters bypassed ASLR using a measured disclosure, verified unauthenticated code execution as www-data on localhost, and reproduced the result after the process address layout changed.
The public screenshots show the local target, the CoHunter finding, the oversized cookie, and the web shell proof. This article leaves out the full request construction, memory layout, and final-stage exploit material. The result belongs to the tested PHP 8.4 build and configuration; PHP's advisories remain the source for patch decisions.
Research disclosure note: This work was carried out solely for controlled security research and defensive awareness. The working exploit, payloads, exploit tooling, and step-by-step reproduction details are private research materials. XHack AI will not publish or share them with the public.
All testing used disposable containers controlled by the researcher. No production system or third-party host was tested. For PHP 8.4, update to 8.4.27 or later, or install a verified vendor backport, and follow PHP's current session upload progress precaution.
Engagement details
Independent Research (PHP core, CVE-2026-109618 / CVE-2026-109619)
Open Source Infrastructure / Web Hosting
Independent Exploit Research with XHack AI CoHunters
3 days
2026
Start your engagement
Talk to XHack Research
XHack delivers the same rigorous methodology behind every case study. Let us pressure-test your defences.
Talk to XHack Research