Curated security research, vulnerabilities, advisories and tools for practitioners.

CVE-2026-109618/109619: Two Independent Pre-Auth RCE Chains on PHP 8.5.11 (Lab Reproduction & PoC)

Summary

# PHP RFC 1867 lab — two independent RCE chains on PHP 8.5.11 Private, embargoed reproduction package for the PHP 8.5.11 Apache lab RCE work, 18–21 October 2026. **Status: Proven — lab-assisted...

Why it matters

Provides definitive lab evidence and control ladders clarifying the actual exploitability of php2shell, refuting circulating unworkable cached-zval UAF theories while proving concrete RCE vectors on stock PHP 8.5.11.
CVE
CVE-2026-109618
Vendor
PHP
Product
PHP
Affected versions
<= 8.5.11 (fixed in 8.5.12, 8.4.27, 8.3.36, 8.2.35)
CVSS
9.2
Published
Collected

📦 Full-Text Content Snapshot

R2 Archive · Full Article Preserved212 lines · 13.0 KB
📄 Raw .md⬇️ Download .md

Author: dinosn · Published: October 21, 2026 · Source: github.com/dinosn/php-cve-2026-10961x-lab-poc

Lab reproduction and vulnerability-research package for the PHP 8.5.11 php2shell class (CVE-2026-109618, CVE-2026-109619), October 2026. Status: Proven — self-contained RCE (four plain HTTP requests, no debugger, no local access), plus amplification, patch-gap, and FPM stale-state findings below.


Results at a Glance

#FindingClassification
1End-to-end self-contained RCE on PHP 8.5.11 — four plain HTTP requests, in-band address sourcing via PS(id)→cookie disclosure, both bases from one 512-byte PHPSESSID leak; 12/12 fires, uid=33, 8 ASLR generationsProven — self-contained RCE
252x memory amplification via duplicate 6-byte multipart headers — defeats post_max_size (2.57 MB body OOMs a 128M limit) on PHP 7.4–8.5.11Proven — lab, all four versions
3Three exploit paths survive the 8.5.12 security patch (var_destroy re-entry UAF at $n=1015$, serialize-handler mismatch, upload-progress injection)Proven — ASAN on GPG-verified 8.5.12
4FPM stale BG(unserialize): worker survives a session-decode OOM, stale state is consumed by the next request's rfc1867 window, heap-shaped consumption SIGSEGVs the workerProven — mechanism + DoS; RCE unproven
5Content-Type separator bypass (multipart/form-data, boundary=X) — every published Content-Type-anchored WAF signature is bypassableProven — SAPI.c parser behavior
6Both CVE chains dead on patched 8.5.12 (token-identical configure, GPG-verified source)Proven — negative control
7The public "cached-zval session.upload_progress" theory — refuted on 4 stock PHP versions + 3 LiteSpeed vendor builds, all cleanup/concurrency cornersRefuted — exhaustive
8The same stale-globals class on PHP 7.2 (EOL): both bases leak from one in-band cookie, zif_system dispatch runs as uid=33 — 6/6 under stock post_max_size=8M, incl. 5-worker poolsProven — lab, 7.2.34 + 7.2.21
9The 129 MiB producer is replaceable by a 1.41 MB header-dense part (470k × x:\n) with zero recalibration — 3/3 full chain, identical basesProven — lab

Chains

Two independent memory-corruption chains reach attacker-controlled indirect calls on the stock php:8.5.11-apache image; the end-to-end wrapper combines Chain B's mechanism with a novel in-band disclosure for a fully remote fire.

FeatureChain A — stale bucketChain B — dangling reference
MechanismOOM bailout leaves a HashTable bucket published but uninitialized; shutdown consumes it as a fake zval → fake array → pDestructorPre-RINIT OOM leaves PS(http_session_vars) holding a dangling zend_reference; a live 31-byte string reclaims it; forged reference → zend_hash_destroy pDestructor
Allocatortracked/system (USE_ZEND_ALLOC=0)stock default Zend MM
Terminallibc system() (gdb-calibrated addresses)zif_system @ libphp+0x5996e0 (no libc)
Debugger at fire timecalibration onlynone — FIRE_GDB=0
Reproducibility2 randomized parent generationsrepeated, incl. cross-run

End-to-end receipt (12/12 fires, 8 ASLR generations, no debugger, no local access)

Leak-A arms a dangling PS(id) (1001st variable trips max_input_vars → headers_sent=1; the empty-cookie reset_id failure keeps send_cookie=1; a 24 MiB field OOMs pre-RINIT). Leak-B lands a 257-element array whose zend_array overlaps PS(id)−0x10: ZSTR_LEN reads nTableSize=512 and the URL-encoded cookie carries pDestructor (→ libphp base) plus nine arData heap pointers (majority vote → chunk base). The measured chunk−libphp delta is not constant across generations (−0x1a00000 / +0x3600000 / −0x1c00000), so the disclosure is genuinely load-bearing. The fire then runs with the leaked addresses. Prerequisites: display_errors=On, post_max_size above the OOM field, memory_limit below it, single-worker prefork.

text
A status='HTTP/1.1 200 OK'   → OOM fatal, worker survives, PS(id) armed
B Set-Cookie: PHPSESSID=<512-byte encoded disclosure>
FIRE → MARKER=uid=33(www-data) gid=33(www-data) groups=33(www-data)
FIRE_OK   (×12 across 8 distinct ASLR layouts)

Full receipt and control ladder: docs/END-TO-END.md.


52x Memory Amplification

Optimized duplicate-header amplification: short X: a headers (6 bytes wire) exploit PHP's fixed 256-byte SMART_STR_START_SIZE per saved header (~314 B memory per 6 B wire). Measured by exact OOM-threshold bisection on 8.5.11; identical on 7.4.33 / 8.1.34 / 8.4.26 (51–52x):

memory_limitmin OOM bodyamplification
8M153 KB54.8x
32M636 KB52.8x
128M2.57 MB52.3x
256M5.14 MB52.2x

All bodies fit inside a stock post_max_size=8M — the practical upload barrier is defeated on every unpatched version. Chunked framing additionally bypasses the Content-Length pre-check (8.85 MB chunked parsed and OOM'd where the same body with Content-Length was rejected). The 8.5.12 patch closes both vectors (unknown-header filtering + streaming post_max_size enforcement). The nested-array "bomb" shape does not reproduce as exponential (max 9.6x, 1.16 MiB — cannot OOM even 8M). Details: docs/RELEASE-RECONCILIATION-2026-10-21.md.


Patch Gaps on 8.5.12

Verified on a GPG-verified, token-identical 8.5.12 ASAN build — the two CVEs are fixed, but three separate paths to impact survive the patch:

  1. var_destroy re-entry UAF (CWE-416) — boundary unchanged at $n=1015$, identical to 8.5.11 (fault at var_unserializer.c:906:67); the patch's ZEND_HAS_OOM guards are inert on this path; BG(serialize_lock) is still not taken around __destruct dispatch.
  2. Serialize-handler mismatch — a php_serialize-written |-key session decoded by the php handler after an app-phase session_start() instantiates the gadget on mod_php php_value and FPM .user.ini alike; the patch's incomplete_classes_only guard is scoped to the pre-RINIT upload window only.
  3. Upload-progress injection — attacker-controlled key stored verbatim in the session file.

FPM Stale BG(unserialize)

Lab-verified on php:8.5.11-fpm: a 24 MB session at memory_limit=32M OOMs inside php_var_unserialize and the worker survives — the exit-70 barrier applies only to startup OOM. gdb shows BG(unserialize).level=1 and the stale data pointer persisting into the next request's rfc1867 pre-RINIT window; an 8191-byte multipart field produces an 8216-byte _emalloc that overlaps the stale pointer and SIGSEGVs the worker in var_tmp_var during request startup. Measured impact: repeatable worker crash (DoS) plus a memory-corruption primitive; RCE beyond the crash is unproven. Prerequisites: low memory_limit, large attacker-influenced session, a session_start() endpoint, upload_progress=On, and pm=static max_children=1 for reliable same-worker pairing. FPM is therefore conditionally, not categorically, protected for this class.


WAF Signature Bypass

Content-Type: multipart/form-data, boundary=X (comma separator) is accepted by PHP — SAPI.c trims at ;, ,, and space. Every published php2shell WAF signature anchored on Content-Type: multipart/form-data; is bypassable. Defender guidance in docs/WAF-BYPASS.md.


What This Repository Does Not Prove

  • A single-shot exploit against hardened production deployments (multi-worker prefork reliability, display_errors=Off, and high memory_limit all remain genuine barriers);
  • RCE from the FPM stale-state primitive (crash only, so far);
  • Impact on PHP versions other than those explicitly tested (7.4.33 / 8.1.34 / 8.4.26 / 8.5.11 / 8.5.12 stock, 3 LiteSpeed builds).

Exact Environment

PropertyRequired value
HostNative Linux x86_64
TargetPHP 8.5.11, Apache module (Chain B also on FPM for stale-state work)
Imagephp@sha256:974e3a920309308e1690ed607e6cc3061d6906f90cc8d30fe519daa48bccf101
Chain A allocatorUSE_ZEND_ALLOC=0, USE_TRACKED_ALLOC=1 (intentionally non-default)
Chain B allocatorstock default Zend MM
PHP limits (chains)memory_limit=16M, post_max_size=32M, upload_max_filesize=32M
Apacheprefork, exactly one worker
DebuggerHost GDB with Python support (calibration/receipts only)

Quick Start (Chain A Runner)

Run only on an owned, isolated x86_64 Linux lab:

bash
sudo ./scripts/preflight.sh
sudo ./scripts/reproduce.sh

The runner validates the environment, generates the byte-identical 5,767,505-byte fixture, calibrates a safe type-0x7 slot, then runs fresh-worker negative → abort → marker with independent marker-byte, worker-transition, health, and trace checks, and writes a full SHA-256 manifest.

bash
sudo ./scripts/lab-stop.sh     # stop container, keep state
sudo ./scripts/lab-reset.sh    # remove PoC container + network only

Proof Discipline

CaseExpected effectRCE oracle
CalibrationType-0x7 stale slot observed; worker survivesnone
NegativeFake structures present; no dispatch; same workermarker absent
AbortRefcount decrement; exact abort@plt; SIGABRTmarker absent
MarkerExact target dispatch; command output under /tmpexact unique bytes

An HTTP error, empty response, PHP fatal error, or worker crash is not accepted as command-execution evidence.


Safety Properties

  • Dedicated Docker containers on internal networks; no host port published.
  • Images pinned by digest. PoC builders accept no arbitrary command.
  • Execution payloads are restricted id/printf-class commands writing under /tmp inside the dedicated container only.
  • Negative and controlled-abort cases are mandatory in every session.
  • Host GDB attaches only to the sole worker of the named lab container.

Defender Screening

tools/screen/graded_screen.py is the graded triage for the parser class behind both CVEs, packaged for defenders: banner/branch triage, a safe surface probe, and two controlled parser-heap ladders (unique part headers; small-body empty-filename fast fill).

bash
python3 tools/screen/graded_screen.py https://host/ --dry-run
python3 tools/screen/graded_screen.py https://host/ --max-tier 3 --authorized

Repository Layout

text
app/                    Minimal PHP/Apache target
tools/                  php2shell_test.py (v1+v2) + domain_php2shell_scan.py
tools/screen/           graded_screen.py - defender triage (banner -> ladders)
tools/bombchain.py      1.41 MB amplified-producer variant of the stock chain
tools/php72/            PHP 7.2 chain: drive.py, chain72.py, fullchain.py
docs/SCREENING.md       Screening tiers, controls, verdicts, false positives, pacing
ninebie/                Chain B PoC (send/env scripts, fire entry point, receipts)
docs/                   Proof boundary, port write-ups, reconciliation log
evidence/reference/     Chain A two-generation proof summary + provenance
evidence/validation-*/  Chain A compact evidence, two fresh generations
scripts/                Chain A lab, calibration, body, runner, verifier
tests/                  Deterministic fixture and safety tests
artifacts/, runs/       Generated locally; ignored by Git

SOURCE-SHA256SUMS authenticates the tree: python3 scripts/make_manifest.py --verify.


Disclosure Statement

In an isolated PHP 8.5.11 Apache lab, two independent multipart-triggered chains reached attacker-controlled indirect calls and executed benign commands; a later end-to-end wrapper removed the debugger and local-access dependencies entirely using an in-band disclosure. On patched 8.5.12 both CVE chains are dead, but three separate paths to impact survive the patch. A 52x memory-amplification technique defeats post_max_size on all unpatched versions since 7.4. These results demonstrate code-execution feasibility in the stated configurations; they are not evidence of a universal or unconfigured-deployment exploit.


📦 Full-Text Markdown Snapshot archived by Phying News · Original source: github.com