Skip to content

About

SAPI differential research on CVE-2026-109618/109619: FPM session-residue patch verification (29/29 vs 0/31), mod_php clean-room PoC, full A/B labs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

php2shell-sapi-differential

Differential research on CVE-2026-109618 / CVE-2026-109619 across PHP SAPIs. Everything below ran on stock Docker images, PHP 8.4.26 (vulnerable) vs 8.4.27 (fixed, released 2026-10-21). No ASAN builds for the headline numbers, no lab-only tuning where it matters, every claim backed by a command you can run.

Short version:

  • Apache mod_php 8.4.26: our clean-room memory-chain PoC reaches RCE in 10/10 fresh containers. On 8.4.27 the same chain fails 10/10, at the first stage.
  • PHP-FPM 8.4.26: a controlled startup OOM kills the worker (exit code 70) and leaves an upload_progress_* key with done=false behind in the session file. We hit this in 29 of 29 triggered runs.
  • PHP-FPM 8.4.27: same OOM, same exit code, same 502 to the client, but the session file stays empty. 0 of 31 triggered runs.

That third line is the useful one. One multipart request plus a look at the session file tells you whether an FPM host carries the stale-state behaviour the patch removed. No exploit code involved.

TL;DR for defenders

  • If you run FPM, the process boundary already contains the worst of it: the worker dies on the OOM bailout, the master respawns it in about 0.1 s. This is architecture, both versions do it.
  • The residue is the difference. On unpatched 8.4.x the aborted upload leaves a session entry behind. On patched builds it does not. We use it as a patch verification check that needs no memory exploitation.
  • The residue only appears with the default php session serialize handler. Under php_serialize both builds stay clean (0/6 and 0/6), so include your session INI in any exposure assessment.
  • If you run mod_php, update. The memory chain is documented and public, and it works deterministically on 8.4.26.

Quick start

git clone https://github.com/<you>/php2shell-sapi-differential.git
cd php2shell-sapi-differential/fpm-lab
docker compose up -d
docker restart fpm26-nginx fpm27-nginx   # nginx caches FPM DNS at start
python3 ../fpm_diff/fpm_diff.py h6

h6 is the deterministic 15-round A/B series. It prints one line per round:

[H6-r0] oom+residue: exit70 18->19, trigger_resp='HTTP/1.1 502 Bad Gateway', file=b'upload_progress_kkkk...'
[H6-summary] oom=15/15, oom_with_residue=15/15: build=26
[H6-summary] oom=15/15, oom_with_residue=0/15: build=27

Ports: 18210 serves 8.4.26, 18220 serves 8.4.27, both bound to 127.0.0.1.

What a residue looks like

After a triggered OOM on 8.4.26:

$ docker exec fpm26 head -c 60 /tmp/sess_00…982
upload_progress_kkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkk

Same procedure on 8.4.27:

$ docker exec fpm27 head -c 60 /tmp/sess_00…9f1
<no file>

The file on the vulnerable build holds the upload progress array of a request that never completed, with done=false. The patch (commit 506e87fa7d, "Avoid state corruption via bailout") restores session globals on error unwind, and the entry is gone.

What we measured, in full

Experiment 8.4.26 8.4.27
H1: startup OOM via multipart, worker lifecycle exit 70, respawn ~0.1 s exit 70, respawn ~0.1 s
H2: application-level OOM (fatal in userland) soft, worker survives soft, worker survives
H4: session residue after startup OOM 7/7 0/9
H6: same, deterministic series, 3 s settle 15/15 0/15
H5: same, serialize_handler=php_serialize 0/6 0/6
H7: same on an ASAN build 0/8, no ASAN reports n/a
Apache: own_memchain.py RCE, fresh containers 10/10 0/10

Cumulative residue totals across all FPM series: 29/29 (8.4.26) vs 0/31 (8.4.27), each residue verified by reading the session file inside the container, not just via HTTP.

Exit code taxonomy

Every pre-application fatal of the shape Allowed memory size ... in Unknown on line 0 was followed by worker exit 70: 35 of 35 across both builds. Every userland fatal (our oom_fatal.php) was followed by a normal shutdown: 15 of 15. We have not seen this correlation written down anywhere, it makes FPM log triage for this CVE class trivial: grep for exited with code 70.

Why ASAN gives you nothing here

We built 8.4.26 from source with -fsanitize=address and ran the same procedure. Result: 8/8 OOM rounds, exit 70 as usual, zero AddressSanitizer reports, and the residue disappeared. The bug is a logic bug, the bailout skips cleanup through longjmp(), and nothing violates the heap in a way ASAN sees. Sanitizer builds also change the timing enough to suppress the stale state. If you want to reproduce the residue measurements, use stock builds. The asan/ directory has the Dockerfile and the config quirks we hit (ASAN_OPTIONS as image env, no = inside FPM env[] values, stock main-fpm.conf plus pool include instead of -y with a single file).

What is new here compared to public work

Project Has Missing that we add
9bie/php2shell-bypass-aslr-exp mod_php RCE, ASLR bypass, 150-worker setup no FPM, no patch A/B
xuehuolll/php2shell-CVE-2026-109618-109619 5 exploit paths, patch analysis, FPM source reading (child exits) no FPM measurements
EngOREOO/php2shell-...-PoC portability patch, precondition checks, A/B on mod_php no FPM
corelight/php2shell-detect network detection (Zeek/Suricata) host-side patch verification
VKKKV/php2shell-test FPM measurements on 8.5.11/8.5.12, serialize variant no 8.4.x, no unpatched-vs-patched FPM A/B, no residue level

The residue differential on FPM 8.4.26 vs 8.4.27 is, to our knowledge, not published anywhere. We claim it as a new observation with a reproducible test, not as a new exploit. If you find prior art, tell us and we will credit it.

Scope and non-claims

  • We did not build an FPM RCE. The advisory says one exists via an application-level producer. We have not reproduced that path and make no claim about its difficulty.
  • The residue on unpatched FPM is a state artefact. We have not tried to escalate it. If you do, responsible disclosure applies.
  • The Apache PoC is included (apache-poc/) because it is the mod_php half of the differential. It is a clean-room implementation against a patched CVE, guarded by a version check, and it writes a marker file with id output. It is not a weapon, and it only talks to 127.0.0.1.
  • All lab listeners bind to 127.0.0.1. Nothing here targets third-party systems.

Layout

fpm-lab/            docker compose, php.ini, fpm pool, nginx, webroot
fpm-lab/php-serialize.ini + docker-compose.serialize.yml   H5 variant
fpm_diff/fpm_diff.py  test driver (h1, h2, h4x, h4x2, h5, h6, h7)
asan/               source-build Dockerfile, pool config, notes
apache-poc/         own_memchain.py, the mod_php half, 10/10 vs 0/10
results/            raw JSON of every run recorded here
docs/               SAPI matrix, patch coverage mapping

Patch coverage in brief

Commit 506e87fa7d (single fix commit between the tags, 402 files) does four things that map directly onto the measurements:

  • zend_alloc: soft vs hard memory limit split. Soft limit queues an error at a VM safe point, hard limit _exit()s without cleanup. This kills the mod_php chain, our 8.4.27 counterproof fails at stage B1, 10/10.
  • session RSHUTDOWN restores globals after error unwind. This is the residue fix on FPM, 29/29 vs 0/31.
  • PS(in_rfc1867_update) guard. Orders the upload progress update path.
  • Fatal backtrace global cleanup.

Full mapping with regression test names: docs/PATCH_COVERAGE.md.

Credits and prior work

  • Wiz research post that named php2shell
  • hubertf's writeup and the 9bie reference implementation
  • The PHP security team, fix released 2026-10-21 in 8.2.35, 8.3.36, 8.4.27, 8.5.12
  • The public repos listed above for calibration cross-checks

License

MIT. See LICENSE.

About

SAPI differential research on CVE-2026-109618/109619: FPM session-residue patch verification (29/29 vs 0/31), mod_php clean-room PoC, full A/B labs

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages