Detects exposure to the PHP core RCEs CVE-2026-109618 and CVE-2026-109619 ("PHP2Shell", published 2026-10-21) in three modes:
| mode | what it does |
|---|---|
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
for the inert canary with --canary.
Authorized use only. Run this against systems you own or have prior written permission to test. It is provided with no warranty and no liability for damages. Read Disclaimer and responsibility before you use it.
Download the binary for your platform from the releases page:
| platform | asset |
|---|---|
| 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 https://github.com/aancw/php2shell-scanner/releases/latest/download/php2shell-scanner-linux-amd64.tar.gz
curl -fsSLO https://github.com/aancw/php2shell-scanner/releases/latest/download/SHA256SUMS
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 file itself.
# blackbox
php2shell-scanner url -u http://target/index.php
php2shell-scanner url -u https://target/ --json --fail-on critical
# this machine
php2shell-scanner local
php2shell-scanner local --webroot /var/www/html
# remote host over ssh
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/wwwurl and ssh take repeatable targets. They are scanned in parallel
(--concurrency, default 5) and the results come back per target.
php2shell-scanner url -u http://a/ -u http://b/ -u http://c/
php2shell-scanner ssh -H web1 -H web2 -H deploy@web3:2222A 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
scanned.
php2shell-scanner ssh -H web1 -U deploy --password 'secret'
SSH_PASSWORD='secret' php2shell-scanner ssh -H web1 -U deployThe 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
accepts.
Useful flags:
| flag | modes | meaning |
|---|---|---|
--json |
all | print the report as JSON |
--fail-on <severity> |
all | exit 1 at this severity or worse (default high, none disables) |
--webroot <dir> |
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 comment 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 |
--user-agent <string> |
url | User-Agent header; none is sent by default |
--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.
| branch | fixed in | last vulnerable |
|---|---|---|
| 8.5 | 8.5.12 | 8.5.11 |
| 8.4 | 8.4.27 | 8.4.26 |
| 8.3 | 8.3.36 | 8.3.35 |
| 8.2 | 8.2.35 | 8.2.34 |
| ≤ 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 source backports.
- PHP version from
X-Powered-By, theServerheader, the body, or an exposed phpinfo page (found with the?=PHPB8B5F2A0-...easter egg). - SAPI:
Apache 2.0 Handlermeans 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 cannot stop it. - phpinfo values that decide exploitability:
session.upload_progress.enabled,memory_limit,fatal_error_backtraces(8.5),session.use_only_cookies,session.serialize_handler. - Arming probe: one multipart POST with a
PHPSESSIDfield and aPHP_SESSION_UPLOAD_PROGRESSfield 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--canaryis given. Every request fromurlmode carries noUser-Agentunless--user-agentis set, so match on the request shape rather than on the header.
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_cookiesfrom 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=0is only a mitigation when it is set in the configuration that applies before request parsing; a.user.inivalue is reported as too late.session.serialize_handlermismatch between files that share onesession.save_path(the configuration half of the session primitive).- Session directory scan for artefacts of the content injection: a
sess_*file that carriesupload_progress_next 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
DocumentRootandrootdirectives of the Apache and Nginx configuration;--webrootadds one more. Files carrying theupload_progress_marker are reported, and.htaccess/*.inifiles are read for the same directives as the system configuration. session.upload_progress.cleanupdecides 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 directory scan misses it.- 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
a plain php:8.3.35-apache 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)
mkdir -p dist
GOOS=linux go build -o dist/p2s .
# 2. copy it in
C=php2shell-target
docker cp dist/p2s $C:/usr/local/bin/p2s
# 3. run it
docker exec $C /usr/local/bin/p2s localFor 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 containerCopy 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
not in the web server configuration.
# 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 up
./lab.sh version # -> [VULNERABLE] 8.3.35 < 8.3.36
# blackbox against the victim
php2shell-scanner url -u http://<target-ip>/index.phplab/ 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:
cd lab
ssh-keygen -t ed25519 -f ./id_ed25519 -N '' -C '' # -C '' keeps user@host out of the key comment
cp ./id_ed25519.pub ./authorized_keys
docker compose up -d --build
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/htmlThe blackbox arming probe and the host-side session scan are two halves of the
same check: the probe writes a sess_<injected id> file into the session
directory, and the host scan finds it. With --canary the marker contains an
inert <?php /* ... */ ?> comment, 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-apachein.env. Without that pin the compose file resolves a floating tag, which is already patched (8.3.36) and reports as patched. session.upload_progress.enabled=0closes the confirmed session paths only.memory_limit=-1removes 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.
This tool is for authorized security testing only. Running it against a system without the owner's prior written permission can be an offence under computer misuse and unauthorized access laws, and knowing which of them apply is your responsibility. Own the target or hold a signed scope, statement of work or equivalent authorization that covers exactly these checks, and stay inside it.
- A detector, not an exploit. There is no exploit payload, no code execution
primitive and no post-exploitation action. The tool fingerprints versions and
configuration and, unless the probe is disabled, sends one benign
session.upload_progressrequest. The OOM corruption path is deliberately never probed. - It still changes target state. The
urlprobe runs against every target, not only vulnerable ones: on any host withsession.upload_progress.enabledandsession.upload_progress.cleanupoff it can leave asess_*entry behind, patched or not, and its POST shows up in access logs and can trip IDS rules.localandsshmode read files, ini values and session directories with the privileges you hand them, so paths, configuration and any credentials stored in those files end up in the report. - Heuristics, not proof. Findings can be false positive or false negative. Confirm a critical result by hand before you patch, act on or disclose it.
- No warranty. The tool is provided "as is", without warranty of any kind, express or implied, including merchantability, fitness for a particular purpose and non-infringement.
- No liability. The authors and contributors are not liable for any claim, damage or other liability — service disruption, denial of service, data loss, legal action, regulatory penalty — arising from the use or misuse of this tool, even if advised of the possibility.
- Handle reports as sensitive. They carry hostnames, versions, file paths and configuration detail from the scanned environment; treat them with the same care as the target's own vulnerability data.
- Independent tool. Not endorsed by, affiliated with or maintained by the PHP Group, or by any vendor, distribution or hosting provider named above. All product and project names belong to their respective owners.
go build -o php2shell-scanner .
go vet ./...
go test ./...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
does not exist yet.