Skip to content
Exploit for Improperly Controlled Sequential Memory Allocation in Php

Exploit for Improperly Controlled Sequential Memory Allocation in Php

Sploitus • October 22, 2026

Scanner, interim-mitigation and incident-hunting tool for **php2shell**, the pair of

critical unauthenticated remote code execution bugs in the PHP runtime:

| CVE-2026-109618 | RCE via out-of-memory corruption | 9.2 |

| CVE-2026-109619 | RCE via stale global state | 9.2 |

Single-file Python 3 (standard library only), plus a POSIX `sh` quick check for hosts

without Python. **No exploit code.** Nothing in this repository attempts to trigger

either vulnerability; the `remote` mode is a passive banner check with an optional

sudo python3 php2shell-guard.py scan # am I patched? (binaries, packages, workers, ini, FPM pools, Docker)

sudo python3 php2shell-guard.py protect --apply # php.net interim mitigations, with --rollback

sudo python3 php2shell-guard.py hunt # was I hit? (logs, session files, webroots, worker children)

python3 php2shell-guard.py remote -f hosts.txt # fleet banner check (your own hosts)

python3 php2shell-guard.py rules modsecurity # WAF / proxy / IDS snippets

sh quickcheck.sh # no python? 10-second answer

Most public repositories are either exploit PoCs or remote banner scanners. Neither

answers the question an operator actually has at 3 a.m.: *is the PHP that serves my

traffic fixed, and if not, what do I do in the ten minutes?* The banner is hidden

by CDNs, Debian/Ubuntu/RHEL backports keep the old version string (Ubuntu 24.04's fixed

build still reports `PHP 8.3.6`), the CLI `php -v` is often not the binary behind

Apache or PHP-FPM, and a restarted package is not the same as a restarted worker.

php2shell-guard works **on the host**, from the inside, and uses three independent

signals so that a backport is never misreported as vulnerable and an old binary is

| **Patch markers in the binary** | The build contains the fix, whatever its version string says. `memory_hard_limit_percent` is a new INI directive added by the full fix (8.2.35 / 8.3.36 / 8.4.27 / 8.5.12). `zend_mm_safe_error_ex` is an exported symbol added by the minimal 7.4 / 8.0 / 8.1 backports that distributions ship (confirmed on RHEL/Alma `php-8.0.30-8.el9_8.1`). | every SAPI, every vendor, Apache `.so` modules, running workers (`/proc//exe` and `maps`) |

| **Package version** | dpkg / rpm / apk / pacman / brew version compared with dpkg and rpm semantics against known fixed builds, and the package changelog grepped for the CVE ids. | distro backports |

| **Upstream version** | `php -v` (or the compile-time `X-Powered-By: PHP/x.y.z` literal for `.so` files) against the fixed-release table. | source builds, Docker official images, Homebrew, Windows |

It then reads the **effective** configuration per SAPI (`php -i`, `php-fpm -i`, FPM

`-tt` pool dumps, static resolution of Debian's per-SAPI `php.ini` + `conf.d`, Apache

`php_admin_*` directives) and flags the settings php.net names in its interim guidance.

git clone

Or drop the two files on a host with `scp`. Requirements: Python 3.8+ (`scan`,

`protect`, `hunt`, `remote`, `rules`) or any POSIX `sh` (`quickcheck.sh`). Root is

not required but without it process maps, some binaries and FPM pool dumps are not

readable, and `protect --apply` cannot write configuration.

![php2shell-guard scan on Ubuntu 24.04: php -v says 8.3.6, the scanner proves the backport](docs/media/scan-patched.png)

kind=fpm version=8.3.6 (-v) status=backported (version string predates the fix; patch markers confirm the build)

patch-markers=memory_hard_limit_percent running pid 10999 found-via=PATH,path-glob,process

ini: upload_progress=On memory_limit=128M post_max_size=8M upload_max_filesize=2M nesting=64

fpm pool [www]: pm.max_requests=0 (no php_* overrides)

PATCHED /usr/lib/apache2/modules/libphp8.3.so

kind=module version=8.3.6 (bytes) status=backported (...)

patch-markers=memory_hard_limit_percent running pid 11006 found-via=module-glob,process-maps

ini-files: /etc/php/8.3/apache2/php.ini scan-dir: /etc/php/8.3/apache2/conf.d

PATCHED dpkg php8.3-fpm 8.3.6-0ubuntu0.24.04.13 8.3.6-0ubuntu0.24.04.13 >= 8.3.6-0ubuntu0.24.04.13 (dpkg)

* every PHP executable on `PATH` and in ~60 known locations (Debian/Ubuntu, RHEL family,

Remi, cPanel EA4, Plesk, LiteSpeed/CyberPanel, DirectAdmin, Docker official images,

Homebrew, MAMP/XAMPP/ServBay, FreeBSD, Windows XAMPP/Laragon/WAMP);

* Apache `libphp*.so` modules, from `LoadModule` lines and from the memory maps of running

* the binary behind every running `php-fpm`, `php-cgi`, `lsphp`, `frankenphp` worker, and

whether that binary was **replaced since the worker started** (`(deleted)` in `/proc`),

which means "patched on disk, still vulnerable in memory";

* dpkg / rpm / apk / pacman / Homebrew package state, with changelog CVE references;

* effective INI per SAPI: `session.upload_progress.enabled`, `fatal_error_backtraces`,

`memory_limit`, `post_max_size`, `upload_max_filesize`, `max_input_nesting_level`,

plus FPM pool `php_admin_*` overrides and `pm.max_requests`;

* Apache `php_value`/`php_admin_value`/`RLimitMEM` directives (php.net warns against

`RLIMIT_AS`/`RLIMIT_DATA`: they cause exactly the allocation failure the bug needs);

* with `--docker`, every running container: PHP version and patch markers inside it.

Severity drops automatically once a runtime is confirmed patched (e.g. upload progress

still enabled becomes LOW, as php.net suggests keeping it off anyway).

Options: `--json`, `-o report.json`, `--docker`, `--no-config` (faster). Exit code 0

### `protect` — interim mitigations with rollback

Applies the php.net advisory's "if you cannot update immediately" measures, in the

places that apply **before request parsing** (the advisory is explicit that `ini_set()`

and `.user.ini` do not help). Dry run by default.

sudo python3 php2shell-guard.py protect # show the plan

sudo python3 php2shell-guard.py protect --apply --restart # upload-progress off (+ 8.5 backtraces off)

sudo python3 php2shell-guard.py protect --apply --post-max 32M --memory-unlimited --restart

sudo python3 php2shell-guard.py protect --rollback --apply --restart # undo everything

| every `conf.d` scan dir of every SAPI (Debian per-SAPI dirs, `/etc/php.d`, Remi, cPanel, Plesk, LiteSpeed, Docker, Homebrew, DirectAdmin) | `zz-php2shell-guard.ini`: `session.upload_progress.enabled=0`, `fatal_error_backtraces=0` (ignored by ` / `php7_module` / `php5_module`, optional `LimitRequestBody`; verified with `apachectl -t` and removed again if the test fails |

| `--restart` | restarts the running `php*-fpm`, `apache2`/`httpd`, `lsws`, `frankenphp` services (systemd, SysV `service`, or `brew services`) |

`--memory-unlimited` follows the advisory's `memory_limit=-1` advice for

CVE-2026-109618. It removes the controllable allocation failure but trades it for

availability risk, so pair it with request-size limits at the proxy (`rules`) and a

cgroup/container memory boundary. The advisory also says plainly that **no

configuration change makes an unpatched install safe**; `protect` buys time, it does

sudo python3 php2shell-guard.py hunt --days 14

sudo python3 php2shell-guard.py hunt --webroot /var/www/site --logs /var/log/apache2/error.log --json -o hunt.json

* Error logs (Apache, nginx, PHP-FPM, LiteSpeed, cPanel, Plesk, syslog, rotated `.gz`):

`Allowed memory size ... exhausted in Unknown on line 0` (OOM before any script ran:

the 109618 signature), worker segfaults (`AH00052`, `exited on signal 11`), session

destruction failures, upstream resets, execution-time fatals.

* Session files under every configured `session.save_path`: PHP open tags or serialized

object markers injected via upload filenames.

* Webroots (auto-detected from Apache/nginx/LiteSpeed/Caddy config or `--webroot`):

PHP files modified inside the window, and webshell heuristics (`eval(base64_decode(`,

`system($_GET[`, `preg_replace /e`, known shell family names, PHP code hidden in

* Processes: shells, interpreters, `curl`/`wget`/`nc`/`socat` whose parent is a web

worker (the strongest signal; Elastic and Datadog detections use the same idea).

* Persistence: cron, systemd units, `authorized_keys`, `/etc/passwd`, `ld.so.preload`,

Exit code 1 when a CRITICAL/HIGH indicator was found. Amazon observed the first

exploitation attempt on **2026-10-17**, four days before disclosure; use a window that

### `remote` — fleet banner check (your own hosts)

python3 php2shell-guard.py remote

python3 php2shell-guard.py remote -f hosts.txt --workers 16 --csv -o fleet.csv

python3 php2shell-guard.py remote --probe-waf

One `GET` per target. Reads `X-Powered-By` / `Server`, notes `PHPSESSID` cookies and

CDN/WAF fingerprints (Cloudflare, Imperva, Sucuri, Akamai, CloudFront, Imunify360,

Fastly, Bunny, mod_security). Verdicts: `likely_vulnerable`, `likely_patched`,

`branch_only` (e.g. `PHP/8.3` without patch level), `eol_branch`, `no_banner`,

`--probe-waf` sends **one ~250-byte multipart POST** whose only field is named

`PHP_SESSION_UPLOAD_PROGRESS` (no file part, no session id, no PHP code). A 403/406/429

means the edge filters the upload-progress feature; a 200/404/405 means requests reach

PHP unfiltered. It is a WAF coverage check, not a vulnerability test, and cannot

A banner is a hint, never proof. CDNs hide it, backports keep old numbers, and the ini

state is invisible from outside. Confirm with `scan` on the host. Only scan systems you

python3 php2shell-guard.py rules # all

python3 php2shell-guard.py rules modsecurity # one

python3 php2shell-guard.py rules --out /etc/php2shell-rules

The same content lives in [`rules/`](rules/): ModSecurity v2/v3 (ids 9951000+), nginx

(+ OpenResty body check), Apache httpd, HAProxy (`req.body` inspection), Suricata/Snort

(sids 9951100+), Caddy/FrankenPHP. They block the session upload-progress field and PHP

tags in multipart bodies, chunked multipart POSTs without `Content-Length` (sidesteps

`post_max_size`), oversized/over-nested forms, and log the response-side signatures.

Cloudflare, Imperva and Imunify360 ship managed rules; these are for everyone else and

for defence in depth. A WAF is a stopgap, and attackers were mutating payloads within

PATCHED /usr/sbin/php-fpm83 8.3.36 fix marker present in binary

VULNERABLE /opt/remi/php74/root/usr/bin/php 7.4.33 EOL branch 7.4.33 and no backport marker; confirm with your vendor

Same discovery and marker logic, POSIX `sh`, works in busybox/Alpine and in the

official `php:*` Docker images that have no Python. `--json` for one object per line.

## What the vulnerabilities are (short version)

Both bugs are in the engine, below any application code, and are reachable by an

unauthenticated POST to any PHP endpoint, including one that only prints "OK".

* **CVE-2026-109618.** PHP handles out-of-memory with `longjmp()`. If the limit is hit

while request data is only partially initialised (for instance during multipart body

parsing, before the script runs), request shutdown later treats the half-built

structures as valid. Confirmed exploitable with default settings on mod_php; FPM,

FrankenPHP and LSAPI are considered susceptible. The fix replaces the `longjmp()` with a

delayed "soft" OOM at the safe point plus a hard `_exit()` threshold

* **CVE-2026-109619.** Worker-wide state (error, session, object, extension) can keep

pointing at memory of a request after that request's heap was freed. A later

request handled by the same worker uses it before normal initialisation resets it. The

default-enabled session upload-progress feature is a confirmed trigger that runs even

if the application never calls `session_start()`; `fatal_error_backtraces` on PHP 8.5

Affected: every PHP serving HTTP (PHP-FPM, mod_php, php-cgi, FrankenPHP, LiteSpeed),

from PHP 5.4 onward. Fixed upstream in **8.5.12, 8.4.27, 8.3.36, 8.2.35** (2026-10-21).

7.4 / 8.0 / 8.1 have minimal source backports and vendor packages (Ubuntu 22.04

`8.1.2-1ubuntu2.28`, RHEL 8 `php:7.4`, RHEL 9 `php-8.0.30-8.el9_8.1`, ...); older branches

have nothing. Details leaked before the embargo ended, exploitation was seen from

2026-10-17, and Cloudflare reported mutating payloads on day one.

Tested in this repository's CI and by hand on: Ubuntu 24.04 (backported

`8.3.6-0ubuntu0.24.04.13`, FPM + mod_php + Apache), AlmaLinux 9 (`php-8.0.30-8.el9_8.1`

RHEL backport), Alpine 3.24 (`php83-8.3.36-r0`), official Docker images `php:8.3-fpm-alpine`,

`php:8.4-apache`, `php:8.1-fpm`, `php:8.3.20-fpm`, macOS Homebrew `php 8.4.3`.

Discovery paths exist for Debian/Ubuntu, RHEL/Alma/Rocky/Fedora, Remi SCL, cPanel EA4,

Plesk, LiteSpeed/OpenLiteSpeed/CyberPanel, DirectAdmin, ISPConfig-style `/opt/php-*`,

FreeBSD, Homebrew/MacPorts/MAMP/XAMPP/ServBay, Windows (XAMPP, Laragon, WAMP, IIS

`php-cgi`). `protect` writes to the matching `conf.d`, pool and Apache locations of each.

If your layout is missing, open an issue with `php --ini` output.

| `scan` | all patched / no PHP | something vulnerable | undetermined | usage |

| `hunt` | no CRITICAL/HIGH indicator | indicator found | | usage |

| `remote` | nothing likely vulnerable | likely vulnerable target | all unreachable / no banner | usage |

| `protect` | done / dry run | | | needs root / bad args |

* A build that carries the fix but was not restarted still runs the old code; `scan`

flags stale workers on Linux only (`/proc`).

* Vendor backports that neither add the upstream markers nor mention the CVE in the

changelog are reported as *not confirmed*, never as patched. Compare with your vendor's

* `hunt` finds traces; a clean result is not proof. Successful exploitation can return

* `remote` cannot see configuration or backports and is deliberately passive.

* `protect` edits configuration; read the dry run first and keep the `.bak` files.

Primary sources used while building this tool are listed in

[docs/REFERENCES.md](docs/REFERENCES.md): the php.net advisory, both GitHub security

advisories (GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp), the php-src fix and backport

branches, and the Datadog, watchTowr, Tenable, Qualys, Elastic, Imperva, Cloudflare,

NCSC-NL, Debian, Ubuntu and AlmaLinux write-ups.

Fixed-version tables (`DEB_FIXED`, `RPM_FIXED`) and discovery globs are the parts most

likely to need updates as vendors ship. Pull requests with the advisory link are

welcome. Run `python3 -m unittest discover -s tests` and keep `rules/` in sync with

`php2shell-guard.py rules --out rules` (the test suite checks this).

MIT. Use on systems you own or are authorised to assess. The authors are not

responsible for misuse. Not affiliated with the PHP project.

Made by [SwordSec](

Extracted Entities

Attack Types (1)

Domains (1)

Tools (1)