Skip to content

About

Black-box detector, proof harness and ELF offsets extractor for php2shell (CVE-2026-109618 / CVE-2026-109619), plus a reproducible vulnerable lab. Companion tooling for the eness PoC.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

php2shell-detect

Black-box detection and proof tooling for the php2shell PHP RCE flaws (CVE-2026-109618 / CVE-2026-109619), plus a reproducible vulnerable lab.

These are companion tools for the public proof-of-concept by enes sönmez at eness/php2shell-CVE-2026-109618-CVE-2026-109619-PoC. The exploit itself is not reproduced here — clone that repo for it. This one adds the pieces that were missing when testing against a real target: a detector that distinguishes "not vulnerable" from "could not tell", a proof harness that writes artifacts instead of claims, and the offsets extractor the PoC expects but does not ship.

Authorised testing only. prove.py and the detector's --oom mode exhaust memory_limit and kill PHP workers by design.

Why these exist

Running the upstream PoC against a real server produces HEAP_LEAK_FAILED and nothing else. Its stage 1 returns 0 for three unrelated situations — no Set-Cookie at all, a cookie shorter than 1000 bytes, and fewer than 8 pointers sharing a 2MB alignment — and prints all of them identically. There is no way to express "indeterminate", so every inconclusive run looks like a negative.

The exploit also needs a very specific configuration (memory_limit below post_max_size, max_input_vars=1000, session.use_strict_mode=0, single-worker prefork mod_php) and heap constants measured on one build. Each is its own false-negative source, and none of them mean the target is patched.

The three tools

detect/php2shell_detect.py — is this exposed?

Five signals with different preconditions, and a three-state verdict:

# Signal Impact
S1 PHP version from X-Powered-By / Server vs fixed releases passive
S2 Differential Set-Cookie presence probe harmless
S2b Differential Set-Cookie value probe — is a supplied session id adopted? harmless
S2c session.use_strict_mode, which disambiguates an S2b negative harmless
S3 max_input_vars / post_max_size / memory_limit, from error text or behaviour mixed
S4 Memory disclosure, relaxed criteria + parameter sweep kills workers

S2 confirms the vulnerable path is reachable with no OOM, grooming or corruption, so it survives every config that breaks the exploit. S2b rescues the common case where S2 goes blind because the application calls session_start() itself — it compares the cookie's value instead of its presence. S4 lowers the acceptance bar from the PoC's exploitation thresholds to detection thresholds.

Exit 0 not vulnerable, 1 vulnerable, 2 indeterminate, 3 error. Code 2 is the point: treating it as 0 is the false negative.

detect/prove.py — show me, don't tell me

Mode Proves Impact
precheck which exploit preconditions hold, and names the ones that don't harmless
leak real memory disclosure → raw bytes, hexdump, recovered pointers and the exact parameters, written to out/ kills workers
crash corruption attributable to the upload-progress path, via a byte-size-matched control that omits only the progress field kills workers

crash mode distinguishes corruption from plain resource exhaustion: if both the UAF shape and the size-matched control kill the worker, that is OOM and the tool says so rather than claiming a finding.

Command execution is deliberately absent — it needs zif_system and zval_ptr_dtor for the target's exact build, which cannot be derived over HTTP.

detect/offsets.py — the helper the PoC expects but doesn't ship

poc_109618.py reads out/offsets.env and shells out to docker/offsets.py, which is not in the upstream repo. So --profile bookworm silently falls back to the lab zif/dtor constants, and stage 2 then rejects every leak, because libphp_base_from_cookie validates p & 0xFFF == dtor_offset & 0xFFF.

Two recovery paths, pure stdlib, no pyelftools:

  1. Symbols. zval_ptr_dtor is ZEND_API, so it is in .dynsym even when the binary is stripped. zif_system usually is not.
  2. Function table. PHP's module entry is an array of zend_function_entry { const char *fname; zif_handler handler; ... }. In a PIC object both fields are filled by R_X86_64_RELATIVE relocations, so: find the vaddr of "system\0" in .rodata, find the RELATIVE reloc whose addend is that vaddr (that is &fname), and the reloc at &fname+8 carries zif_system. Candidates are rejected unless the handler lands in an executable section.

Quickstart

Python 3.8+, no dependencies.

# 1. is it exposed? safe, ~8 requests, nothing corrupted
python detect/php2shell_detect.py -u http://target/index.php

# 2. are the exploit preconditions even satisfiable? harmless
python detect/prove.py -u http://target/index.php --mode precheck

# 3. only if precheck green-lights. kills workers.
python detect/prove.py -u http://target/index.php --mode leak --sweep
python detect/prove.py -u http://target/index.php --mode crash --reps 5

# 4. offsets, for driving the upstream PoC
python detect/offsets.py --so /usr/lib/apache2/modules/libphp8.3.so
python detect/offsets.py --container php2shell-target

offsets.py is pure stdlib, so it can simply be copied to the target host and run there against the live module.

Lab

lab/ builds a deliberately vulnerable target pinned to php:8.4.26-apache, the tag the upstream PoC's BOOKWORM_* constants were measured against. Note that php2shell.ini sets memory_limit (8M) below post_max_size (64M) — that inversion is what lets a large body be accepted and only then exhaust the heap, and it is why no default-configured server is exploitable by this chain.

Run the tools against the lab before trusting a negative on a real host: if they do not fire against a deliberately vulnerable target, a clean result elsewhere means nothing. See lab/README.md.

Scope limit, and the one false negative you cannot probe away

Everything here except S1 covers the session upload-progress path only. Public reporting splits php2shell into an OOM-corruption bug and a stale-global-state bug, with disputed CVE-to-component mapping, so a clean result does not clear the non-session path. Version comparison is the only signal covering both — and if expose_php=Off, there is no black-box coverage of the other path at all.

Version comparison has its own blind spot: Debian and RHEL backport security fixes without changing PHP_VERSION. When the Server header names a distro, the detector says so in the verdict, and you have to confirm against the package changelog on the host:

zcat /usr/share/doc/php8.3-common/changelog.Debian.gz \
  | grep -E 'CVE-2026-109618|CVE-2026-109619'

Fixed versions

8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (released 2026-10-21). PHP 8.1 and earlier are EOL and get no official patch. session.upload_progress.enabled = Off is a partial stopgap that closes the session path only — upgrading is the fix.

Testing

Validated against mock targets and synthetic ELF objects, no live host required:

  • detector — 15 scenarios: unpatched/patched × progress on/off, EOL branch, app-managed sessions (S2 blind, S2b rescues), expose_php=Off variants, edge-blocked multipart, use_strict_mode=1, use_only_cookies=0, and observed disclosure on both an unpatched and a nominally patched build
  • prove.py — 9 end-to-end checks including artifact creation and, importantly, that it refuses to claim proof on a closed or healthy target
  • offsets.py — 5 checks covering both recovery paths, a handler outside .text, a non-ELF file and ELF32

offsets.py has not been run against a real libphp.so — no Docker was available where it was written. The ELF parsing is validated; the real-world symbol layout is not.

Credits

Vulnerability research and the original PoC: enes sönmez (@enes_dev) — eness/php2shell-CVE-2026-109618-CVE-2026-109619-PoC.

License

MIT, covering the contents of this repository only. The upstream PoC is a separate work under its own terms.

About

Black-box detector, proof harness and ELF offsets extractor for php2shell (CVE-2026-109618 / CVE-2026-109619), plus a reproducible vulnerable lab. Companion tooling for the eness PoC.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages