Back Sploitus Exploit for Improperly Controlled Sequential Memory Allocation in Php
Detects exposure to the PHP core RCEs **CVE-2026-109618** and **CVE-2026-109619**
("PHP2Shell", published 2026-10-21) in three modes:
| `url` | blackbox scan of a web target over HTTP, no shell access needed |
| `local` | host checks on the machine the scanner runs on |
| `ssh` | the same host checks on a remote host over SSH |
The scanner is a detector, not an exploit. The only request it sends is a benign
`session.upload_progress` arming probe that contains no PHP code unless you ask
Download the binary for your platform from the
[releases page](
| linux x86_64 | `php2shell-scanner-linux-amd64.tar.gz` |
| linux arm64 | `php2shell-scanner-linux-arm64.tar.gz` |
| linux armv6 | `php2shell-scanner-linux-arm.tar.gz` |
| macOS intel | `php2shell-scanner-darwin-amd64.tar.gz` |
| macOS apple silicon | `php2shell-scanner-darwin-arm64.tar.gz` |
| windows x86_64 | `php2shell-scanner-windows-amd64.exe` |
curl -fsSLO
curl -fsSLO
sha256sum --check --ignore-missing SHA256SUMS
tar -xzf php2shell-scanner-linux-amd64.tar.gz
sudo install php2shell-scanner /usr/local/bin/
The binaries are statically linked, so there is nothing to install besides the
php2shell-scanner url -u
php2shell-scanner url -u --json --fail-on critical
php2shell-scanner local --webroot /var/www/html
php2shell-scanner ssh -H 10.0.0.5 -U deploy -i ~/.ssh/id_ed25519
php2shell-scanner ssh -H 10.0.0.5 -U deploy --agent --webroot /srv/www
`url` and `ssh` take repeatable targets. They are scanned in parallel
(`--concurrency`, default 5) and the results come back per target.
php2shell-scanner url -u -u -u
php2shell-scanner ssh -H web1 -H web2 -H deploy@web3:2222
A host can be `host`, `host:port` or `user@host:port`. The port from `-H` wins
over `-p`. `local` stays single target, it is the machine the scanner runs on.
A target that cannot be reached does not stop the run: it gets a
`SCAN_FAILED` or `SSH_CONNECT_FAILED` finding and the other targets are still
php2shell-scanner ssh -H web1 -U deploy --password 'secret'
SSH_PASSWORD='secret' php2shell-scanner ssh -H web1 -U deploy
The same credentials are used for every host in the run. Keys, the agent
(`--agent`) and the password can be combined; the server decides what it
| `--json` | all | print the report as JSON |
| `--fail-on ` | all | exit 1 at this severity or worse (default `high`, `none` disables) |
| `--webroot ` | local, ssh | extra document root to scan; the document roots of the web server configuration are detected automatically |
| `--probe` / `--canary` | url | arming probe on by default; `--canary` adds an inert PHP so Suricata/YARA rules fire |
| `--crawl` | url | look for exposed phpinfo pages (on by default) |
| `--insecure` | url | skip TLS verification |
| `--path` | url | extra endpoint to probe, repeatable |
| `--agent`, `--password`, `--passphrase`, `--known-hosts`, `--insecure-host-key` | ssh | authentication and host key handling |
Exit codes: `0` clean, `1` findings at or above `--fail-on`, `2` usage or runtime error.
| ≤ 8.1 | EOL, no routine updates (7.4/8.0/8.1 backports exist) | treated as affected |
A build below the fix of its branch is reported as **critical**; 8.1 and older are
reported as **critical** end-of-life branches that need a vendor package or the
* PHP version from `X-Powered-By`, the `Server` header, the body, or an exposed
phpinfo page (found with the `?=PHPB8B5F2A0-...` easter egg).
* SAPI: `Apache 2.0 Handler` means mod_php, which is the worst case for
CVE-2026-109618 because request cleanup runs even when startup failed, so the
exploit lands before any application code and application-level authentication
* phpinfo values that decide exploitability: `session.upload_progress.enabled`,
`memory_limit`, `fatal_error_backtraces` (8.5), `session.use_only_cookies`,
* Arming probe: one multipart POST with a `PHPSESSID` field and a
`PHP_SESSION_UPLOAD_PROGRESS` field and no cookie. That is the request shape
the Suricata rules in [php2shell-lab](
key on, so the probe doubles as a correlation point. The probe never contains
a PHP open tag unless `--canary` is given.
The OOM corruption path is **not** probed: proving it means exhausting the
target's memory, which is a denial of service. The scanner reports the
configuration that makes the path reachable instead.
* PHP interpreters and versions (`php`, `php8.x`, `php-fpm`), plus the SAPI in
use: mod_php module present, php-fpm processes, web server processes.
* `session.upload_progress.enabled`, `memory_limit`, `fatal_error_backtraces`,
`session.serialize_handler`, `session.use_only_cookies` from the interpreter
**and** from the ini files, FPM pools and web server configuration on disk.
The CLI ini is not the web ini, so both are read.
* `session.upload_progress.enabled=0` is only a mitigation when it is set in the
configuration that applies before request parsing; a `.user.ini` value is
* `session.serialize_handler` mismatch between files that one
`session.save_path` (the configuration half of the session primitive).
* Session directory scan for artefacts of the content injection: a `sess_*`
file that carries `upload_progress_` to a PHP open tag is **critical**
(poisoned session file), a bare marker is an attempt indicator, and an
aborted serialization is the write-denial primitive.
* Document root scan for artefacts of the content injection and for PHP
configuration. The roots come from the `DocumentRoot` and `root` directives of
the Apache and Nginx configuration; `--webroot` adds one more. Files carrying
the `upload_progress_` marker are reported, and `.htaccess`/`*.ini` files are
read for the same directives as the system configuration.
* `session.upload_progress.cleanup` decides whether an arming attempt leaves a
file behind: with cleanup off the progress entry stays in the session file,
with cleanup on it is removed when the request ends and a later session
* Post-exploitation tell: a PHP or web server process with a shell as child.
The scanner was verified against the offline lab in
[mikensec/php2shell-lab]( and against
### Running the local mode inside a container
`local` scans the machine it runs on, so point it at a container by putting the
binary in it. Build on the host, copy, run:
# 1. build for linux (native arch of this host)
For a container with the other architecture:
GOOS=linux GOARCH=amd64 go build -o dist/p2s-amd64 . # x86_64 container
GOOS=linux GOARCH=arm64 go build -o dist/p2s-arm64 . # aarch64 container
Copy to `/usr/local/bin`, not `/tmp`: the lab victim mounts `/tmp` as tmpfs, so
a file copied there disappears. Add `--webroot` only when the document root is
# in the lab: make the victim vulnerable (the lab defaults to a floating tag)
cp .env.example .env # pins PHP_TAG=8.3.35-apache
./lab.sh version # -> [VULNERABLE] 8.3.35 /index.php
`lab/` holds a throwaway SSH target (PHP 8.3.35 with an SSH server) so the `ssh`
mode can be tested without touching a real host:
ssh-keygen -t ed25519 -f ./id_ed25519 -N ''
ssh-keyscan -p 2222 127.0.0.1 >> ./known_hosts
../php2shell-scanner ssh -H 127.0.0.1 -p 2222 -U root \
-i ./id_ed25519 --known-hosts ./known_hosts --webroot /var/www/html
The blackbox arming probe and the host-side session scan are two halves of the
same check: the probe writes a `sess_` file into the session
directory, and the host scan finds it. With `--canary` the marker contains an
inert `` , which is what makes the Suricata rule
`30260002` and the YARA rule `PHP_UploadProgress_Session_Poison` match.
* The lab pins `PHP_TAG=8.3.35-apache` in `.env`. Without that pin the compose
file resolves a floating tag, which is already patched (8.3.36) and reports
* `session.upload_progress.enabled=0` closes the confirmed session paths only.
`memory_limit=-1` removes the predictable memory-limit failure for the OOM
path. Neither is a substitute for the patch.
* The version table is the one from the PHP advisories of 2026-10-21. Confirm
the range against php.net before acting on an end-of-life branch.
Go 1.27+. One dependency: `golang.org/x/crypto/ssh` (SSH transport only).
`.github/workflows/release.yml` builds the release binaries. It runs when a
release is published, and it can also be started by hand with
`workflow_dispatch` and a tag, in which case it creates the release when it
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
