# PHP2Shell detection pack: CVE-2026-109618 and CVE-2026-109619

A read-only checker and hunting content for the two critical PHP vulnerabilities published on 21 October 2026.

| CVE | PHP advisory | Issue |
|---|---|---|
| CVE-2026-109618 | [GHSA-w8hw-w9fx-j79w](https://github.com/php/php-src/security/advisories/GHSA-w8hw-w9fx-j79w) | RCE via OOM corruption. An out-of-memory `longjmp()` leaves partly initialised request memory, and request shutdown then processes it. |
| CVE-2026-109619 | [GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp) | RCE via stale global state. Request-owned state survives into the next request on the same worker (use-after-free). |

The CVE-to-advisory mapping comes from the descriptions in the [NCSC alert](https://www.ncsc.gov.uk/news/critical-security-issue-affecting-php). Both advisories are rated Critical, CVSS 4.0 9.2 (`AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H`).

## Fixed versions (taken from both advisories)

| Branch | Affected | Patched |
|---|---|---|
| 8.2 | < 8.2.35 | **8.2.35** |
| 8.3 | < 8.3.36 | **8.3.36** |
| 8.4 | < 8.4.27 | **8.4.27** |
| 8.5 | < 8.5.12 | **8.5.12** |

The advisories list no upstream fix for 8.1 or older. The NCSC says "organisations using any version of PHP are affected" and that organisations on older versions should move to a supported version. Linux distributions may backport the fix into a package that still shows an older version (Ubuntu 22.04 `php8.1` `8.1.2-1ubuntu2.28` and Ubuntu 24.04 `php8.3` `8.3.6-0ubuntu0.24.04.13` were seen during testing). That is why the checker reads the package changelog on Debian/Ubuntu (dpkg) and RHEL/Rocky/Alma (rpm). php.net also published minimal source backports for 7.4, 8.0 and 8.1; the checker cannot detect these in custom builds and will report LIKELY VULNERABLE. Remember that PHP is also bundled inside appliances and products (NCSC priority action 2).

## Files

- `check-php2shell.sh`: Linux checker (bash; Debian/Ubuntu and RHEL/Rocky/Alma with changelog backport checks, Alpine with package version checks only, plus containers and source builds). On Alpine install bash first (`apk add bash`).
- `Check-PHP2Shell.ps1`: Windows checker (PHP for IIS/FastCGI, XAMPP, WAMP, Laragon, Plesk, PATH)
- `php2shell-hunting.kql`: Sentinel and Defender XDR hunting queries
- `sigma/*.yml`: Sigma rules (webserver, linux process_creation, linux syslog)

## Usage

```bash
sudo bash check-php2shell.sh            # root lets it read all logs and package data
sudo LOG_LINES=500000 bash check-php2shell.sh
```
```powershell
powershell -ExecutionPolicy Bypass -File .\Check-PHP2Shell.ps1   # run elevated for logs and the event log
powershell -ExecutionPolicy Bypass -File .\Check-PHP2Shell.ps1 -LogMaxLines 500000
```
`LOG_LINES` (Linux) and `-LogMaxLines` (Windows) set how many lines are read from the end of each log file (default 200000). `LOG_LINES` must be a positive whole number; journald is always read for the last 14 days.
Both scripts are read-only. They read files and query package and event databases, and the only programs they run are the PHP binaries they find, with `-v` and `-i` (`frankenphp version` for FrankenPHP), each with a timeout. They do not change configuration, restart services or write files.

How the scripts choose which binaries to run:
- Linux: candidates come from PATH, standard install paths (`/usr/bin`, `/usr/local/bin`, `/opt/*`, cPanel, Plesk, Remi, Bitnami, XAMPP) and running processes. A file is only run if its name is a PHP SAPI name (`php`, `phpX.Y`, `php-cgi*`, `php-fpm*`, `php-cli`, `lsphp*`, `frankenphp`) **and** it is an ELF binary. Tools such as `phpunit`, `phpcs`, `phpstan`, `php-config` and `phpize`, and shell wrapper scripts, are never run.
- Windows: only `php.exe` and `php-cgi.exe` are run, from known install folders, PATH, running PHP processes and IIS FastCGI registrations.
- Apache mod_php (`libphp*.so`) is listed but not loaded or given its own verdict; use the verdict for the CLI or package of the same branch.

## Verdicts and exit codes

The checker gives a verdict for each binary and an OVERALL verdict:

- **PATCHED**: the package changelog mentions **both** CVE IDs (a distro backport), or the upstream version is at or above the fixed release for its branch (or is on a newer branch).
- **LIKELY VULNERABLE**: the version is below the fixed release, or the branch has no listed fix (8.1 and older), and the changelog mentions neither CVE. This covers source builds, the official Docker `php:` images, Windows builds and Alpine packages below the fixed version. A pre-release build below the fixed version is also LIKELY VULNERABLE, with a note.
- **UNKNOWN**: the version can't be read, the build is a release candidate or development build (`RC`, `alpha`, `beta`, `-dev`) at or above the fixed version, so it may not contain the final fix, only one of the two CVEs is in the changelog, or the package version looks vendor-specific (it doesn't start with the upstream version) and the changelog has no CVE reference. Check with the vendor.

| Exit code | Meaning |
|---|---|
| 0 | Everything PATCHED, or no PHP found |
| 1 | At least one LIKELY VULNERABLE |
| 2 | At least one UNKNOWN, none vulnerable |
| 3 | Nothing vulnerable or unknown, but the log hunt found entries to review |

The log hunt looks at PHP-FPM, Apache, nginx and PHP error logs, syslog/messages and journald (Linux), and at XAMPP/WAMP/nginx/PHP logs plus Application log event 1000 crashes of `php*.exe` (Windows). It searches for `Allowed memory size ... exhausted`, `Out of memory` and workers that exited on a signal or crashed.

The ini values reported are `memory_limit`, `post_max_size`, `session.upload_progress.enabled` and `fatal_error_backtraces` (8.5). For unpatched hosts the checker also lists any partial mitigations it finds.

## Mitigations (from the advisories)

**Update.** "Updating to a release containing the complete state reset patch is the only reliable mitigation." The 109618 fix replaces the OOM `longjmp()` with a delayed soft OOM, plus a hard OOM using `_exit()` at a second, higher threshold. Restart every PHP worker after you update.

If you can't update yet, apply these administratively and restart every worker. None of them is a complete fix.

- **CVE-2026-109618**
  - Set and enforce `memory_limit=-1`. Make sure no pool, directory or application setting restores a finite limit.
  - Keep `post_max_size` conservative and enforce the decoded request size at the frontend (proxy or WAF).
  - Reduce `max_input_nesting_level`, and bound input variables, multipart parts, headers per part and uploads.
  - Block multipart requests at the frontend if the application doesn't use them.
  - A cgroup `memory.max` can help but has limits.
  - Do **not** use `RLIMIT_AS` or `RLIMIT_DATA`.
- **CVE-2026-109619**
  - Set `session.upload_progress.enabled=0`. The advisory suggests keeping it off even after patching if you don't need the feature.
  - On PHP 8.5, set `fatal_error_backtraces=0`.
  - Remove fatal errors and warnings that an attacker can reach, especially in cleanup and shutdown paths.
  - Remove diagnostics from per-request INI overrides: `.htaccess` `php_value`, and `PHP_VALUE`/`PHP_ADMIN_VALUE` sent from nginx or Apache to FPM. Prefer startup configuration and remove obsolete directives.
  - Consider `pm.max_requests=1` if performance isn't a concern.
  - Changing `error_reporting`, using `@`, or turning off `display_errors`/`log_errors` does **not** protect you.
- **NCSC:** if you can neither update nor apply the mitigations quickly, consider taking the system offline. Assess unpatched systems for compromise and backdoors before you patch.

## Detection content

**No official IOCs.** Neither advisory nor the NCSC alert publishes IOCs, so this pack contains none. The queries and rules hunt for the behaviour the advisories describe:
- crafted (typically multipart) request bodies;
- PHP memory exhaustion or fatal errors;
- crashes or restarts of FPM children ("FPM exits the child when request startup fails");
- process activity under the web worker account after a successful RCE.

Every query and rule includes notes on false positives. Treat hits as leads to investigate, and baseline your environment before alerting.

KQL queries:
- Syslog: OOM and worker-signal exits
- W3CIISLog: bursts of POST requests returning 500
- CommonSecurityLog: multipart POSTs with 5xx responses
- AzureDiagnostics: App Gateway WAF body and multipart rule hits, and 5xx bursts in the access log
- DeviceProcessEvents: churn of php-fpm/php-cgi workers, and shells spawned by PHP workers

## Limitations

- Alpine packages have no changelog the checker can read, so on Alpine the verdict is based only on the package version (for example `php83 8.3.35-r0` is LIKELY VULNERABLE, `8.3.36-r0` or later is PATCHED).
- The checker reports version and changelog status only. It can't confirm a patch in custom builds, vendor-bundled PHP (cPanel/Plesk/appliances) or packages whose changelogs don't list CVEs.
- RHEL/Rocky 9 AppStream `php 8.0.30` had no CVE reference when tested, so it is reported LIKELY VULNERABLE until the vendor ships a fix.
- Effective ini values come from `php -i` / `php-fpm -i` / `php-cgi -i`, which read php.ini and conf.d. They do **not** include FPM pool `php_admin_value`, Apache `php_value`/.htaccess, `.user.ini`, IIS web.config or `PHP_VALUE` sent by the frontend. mod_php versions are inferred from the matching CLI or package.
- Logs: only standard paths and the last 200,000 lines of each file are read by default (raise it with `sudo LOG_LINES=500000 bash check-php2shell.sh`). The advisory warns that "absence of an error in logs is not proof of safety" and that a failure can happen before PHP writes anything.
- Windows: there is no package-backport logic, because official windows.php.net builds use upstream version numbers.
