WordPress on stock PHP (no php.ini) is vulnerable with the same heap
profile parameters as plain PHP. Tested 5/5 first-attempt RCE against
php:8.4.26-apache + WordPress 6.8.1 with no configuration changes.
Simplified stage 3 retry loop: removed the HTDATA offset scanning that
was added during WordPress debugging (unnecessary, stock offsets work).
Stage 3 now uses the same retry count as stages 1 and 2.
- Expand patch section: five independent defenses, not three
- Defense 1 (rinit_session_globals at MULTIPART_EVENT_START) alone
is sufficient to block the entire exploit
- Document post_max_size enforcement that blocks chunked TE bypass
- Add display_errors=Off impossibility analysis with technical detail
- Confirm PHP 8.3.35 RCE working (not just stages 1+2)
- Note 8.4.27 tested and confirmed blocked
- 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