- Correct HTDATA_OFFSET for PHP 8.3.35/bookworm from 0x690D8 to 0x700D8
(different struct sizes shift the heap layout by 7 pages)
- Verified RCE: 8.4.26 and 8.3.35 both succeed on first attempt
- Document the display_errors=On requirement for the info leak stages
- Explain why stock PHP and stock WordPress-on-PHP are still vulnerable
(display_errors defaults to On; WordPress ini_set runs after parsing)
- Add docker configs for display_errors=Off test containers
PHP checks Content-Length against post_max_size before parsing multipart
bodies, but Transfer-Encoding: chunked requests have no Content-Length.
The full chunked body is processed without any size check, allowing the
exploit to send 140MB+ to exhaust memory_limit=128M despite post_max_size=8M.
This removes the last non-default configuration requirement. The exploit
now achieves full RCE against stock php:*-apache Docker images with zero
configuration changes (4/4 confirmed on php:8.4.26-apache).
Changes:
- post() auto-switches to chunked TE for bodies over 8MB
- Default --huge-mb raised from 12 to 140 (exceeds stock 128M limit)
- README rewritten to document the working three-stage RCE chain
- docker-compose.yml adds stock variant for testing
Type confusion via var_access confirmed: R:N; references resolve stale
entries.data[0] without crashing, returning the attacker's own session
data from the cached ZendMM chunk (pages retained, not zeroed).
Key finding: filler spray and type confusion are mutually exclusive.
var_tmp_var reads last_dtor (struct offset 16) BEFORE var_access reads
entries (offset 56). Spray corrupts offset 16, crashing before the
type confusion can execute.
Built-in class audit: SplFileObject and PDO have function pointer calls
in free_obj, but constructing fake objects requires libphp.so base (ASLR).
Patch audit (8.5.12): all three defense layers are solid, no bypass found.
Five RCE blockers: no info leak, filler/type-confusion mutual exclusion,
GC header overlap, no usable classes without ASLR bypass, session key
validation.
Rewrote the exploit to correctly avoid GET requests between OOM
trigger and crash verification (RINIT zeros BG state on every
request). Filler field spray (100 x 8200B = 300 pages) reliably
reclaims the stale struct pages. Crash confirmed on first attempt.
Updated README and FINDINGS to accurately describe:
- The 8216-byte struct (3 pages), not the earlier 64-byte bin
- GC header overlap that blocks controlled RCE (offsets 0-23)
- Three independent patch layers in PHP 8.3.36/8.4.27/8.5.12
- Honest exploitation status: UAF crash confirmed, RCE not achieved
The original exploit put the file upload before the field flood,
so the progress handler's serialize completed before OOM fired.
This meant BG(serialize) was not left stale.
The corrected flow:
Phase 1: Create ~500KB session file via large upload progress value
Phase 2: Fill memory with fields, then file upload triggers session
re-init which OOMs during unserialize of the large session
Phase 3: Verify stale BG(unserialize) state on the surviving worker
Confirmed on PHP 8.3.6, Apache 2.4.58: error log shows
"tried to allocate 503808 bytes" during session handling,
followed by "Session ini settings cannot be changed when a
session is active" on subsequent requests.
Pure memory corruption path: OOM during multipart parsing leaves stale
BG(serialize) state in Apache mod_php prefork workers. Heap spray
reclaims the freed 64-byte slot, giving control of a HashTable's
pDestructor for arbitrary function call.
No LFI, no framework, no non-default PHP settings required.
Tested on PHP 8.3.6 + Apache 2.4.58 (prefork MPM). Confirmed:
- OOM via field flood, worker survival, session file persistence
- Stale BG(serialize).level across requests
- Heap spray targeting ZendMM 64-byte bin
New exploit at fcgi/exploit_fcgi.py combines the session upload progress
write primitive (CVE-2026-109618/109619) with direct FastCGI access to
PHP-FPM. A slow-drip multipart upload holds one FPM worker in the
multipart parser while a second FastCGI request includes the poisoned
session file via auto_prepend_file.
Works on default PHP configuration, no LFI, no framework, no libraries.
security.limit_extensions does not block auto_prepend_file inclusion.
php_admin_value in pool config does not block PHP_VALUE from raw FastCGI.
Tested and confirmed RCE on:
PHP 8.3.6 (local, pre-patch)
PHP 8.3.35 (Docker, pre-patch)
PHP 8.3.36 (Docker, patched)
PHP 8.4.27 (Docker, patched)
Mitigated by session.upload_progress.enabled=0 or blocking FPM access.
Also includes nolfi/ exploit (served save_path + pathinfo routing) from
previous work, and updated README with mitigation matrix for all vectors.
Exploits the interaction between php_serialize and php session handlers:
when one endpoint writes sessions with php_serialize (Laravel, Symfony
default) and another reads with the php handler (PHP default), injected
pipe characters cause arbitrary deserialization at session_start().
12 built-in POP chains covering current and legacy library versions:
- monolog3-fw: Monolog 3.0+ (tested 3.12.1, latest)
- laravel10-cmd/fw: illuminate 10.0-13.x+ (tested v13.38.0, latest)
- monolog-cmd/fw: Monolog 1.17.2-2.x
- laravel-cmd/fw: illuminate 5.4-8.x
- guzzle-fw: Guzzle 4.0-7.x
- symfony-cmd/fw: Symfony 2.6-4.4.18
- wordpress-cmd/fw: WordPress 6.4.0-6.4.1
All chains tested end-to-end through the full HTTP exploit flow on
PHP 8.3. Accepts custom phpggc chains via --chain-file.