
PHP CVE-2026-109618/109619 — eigenständige unauthentifizierte RCE auf Standard-PHP 8.5.11 (In-Band-PS(id)→Cookie-Speicherofflegung + UAF-Kette), vollständige Kontrollleitern, Negativkontrolle mit fester Version auf 8.5.12
Lab-Reproduktion und Schwachstellenforschungs-Paket für die PHP-8.5.11-Klasse php2shell (CVE-2026-109618, CVE-2026-109619), Oktober 2026. Status: Nachgewiesen — eigenständige RCE (vier einfache HTTP-Anfragen, kein Debugger, kein lokaler Zugriff), plus Verstärkung, Patch-Lücken und FPM- Stale-State-Befunde unten.
Inhalt: Ergebnisse · Ketten · Verstärkung · Patch-Lücken · FPM-Stale-State · WAF-Bypass · Umgebung · Schnellstart · Nachweisdisziplin · Aufbau · Öffentlicher Kontext · Erklärung
| # | Befund | Klassifizierung |
|---|---|---|
| 1 | End-to-End eigenständige RCE auf PHP 8.5.11 — vier einfache HTTP-Anfragen, In-Band-Adressermittlung via PS(id)→Cookie-Offenlegung, beide Basen aus einem einzigen 512-Byte-PHPSESSID-Leak; 12/12 Auslösungen, uid=33, 8 ASLR-Generationen | Nachgewiesen — eigenständige RCE |
| 2 | 52-fache Speicherverstärkung durch doppelte 6-Byte-Multipart-Header — umgeht post_max_size (2,57 MB Body führt zu OOM bei einem 128M-Limit) auf PHP 7.4–8.5.11 | Nachgewiesen — Labor, alle vier Versionen |
| 3 | Drei Exploit-Pfade überstehen den Sicherheitspatch 8.5.12 (var_destroy-Re-Entry-UAF bei n=1015, Serialize-Handler-Mismatch, Upload-Progress-Injection) | Nachgewiesen — ASAN auf GPG-verifiziertem 8.5.12 |
| 4 | FPM-Stale-BG(unserialize): Worker überlebt ein Session-Decode-OOM, der veraltete Zustand wird vom rfc1867-Fenster der nächsten Anfrage konsumiert, heap-geformter Konsum führt zu SIGSEGV des Workers | Nachgewiesen — Mechanismus + DoS; RCE nicht nachgewiesen |
| 5 | Content-Type-Separator-Bypass (multipart/form-data, boundary=X) — jede veröffentlichte Content-Type-verankerte WAF-Signatur ist umgehbar | Nachgewiesen — SAPI.c-Parser-Verhalten |
| 6 | Beide CVE-Ketten tot auf gepatchtem 8.5.12 (token-identische Konfiguration, GPG-verifizierte Quelle) | Nachgewiesen — Negativkontrolle |
| 7 | Die öffentliche Theorie des „gecachten zval session.upload_progress" — widerlegt auf 4 Standard-PHP-Versionen + 3 LiteSpeed-Vendor-Builds, alle Cleanup-/Concurrency-Ecken | Widerlegt — erschöpfend |
Zwei unabhängige Speicherkorruptionsketten erreichen angreifergesteuerte indirekte
Aufrufe auf dem Standard-php:8.5.11-apache-Image; der End-to-End-Wrapper kombiniert
den Mechanismus von Kette B mit einer neuartigen In-Band-Offenlegung für eine vollständig entfernte Auslösung.
| Kette A — veralteter Bucket | Kette B — baumelnde Referenz | |
|---|---|---|
| Mechanismus | OOM-Bailout hinterlässt einen HashTable-Bucket veröffentlicht, aber uninitialisiert; Shutdown konsumiert ihn als Fake-zval → Fake-Array → pDestructor | Pre-RINIT-OOM hinterlässt PS(http_session_vars) mit einer baumelnden zend_reference; ein lebender 31-Byte-String beansprucht sie zurück; gefälschte Referenz → zend_hash_destroy pDestructor |
| Allocator | tracked/system (USE_ZEND_ALLOC=0) | Standard-Zend-MM |
| Terminal | libc system() (gdb-kalibrierte Adressen) | zif_system @ libphp+0x5996e0 (kein libc) |
| Debugger zum Auslösezeitpunkt | nur Kalibrierung | keiner — FIRE_GDB=0 |
| Reproduzierbarkeit | 2 randomisierte Parent-Generationen | wiederholt, inkl. über Läufe hinweg |
Leak-A bewirkt ein baumelndes PS(id) (die 1001. Variable löst max_input_vars aus →
headers_sent=1; der Fehlschlag von reset_id mit leerem Cookie hält send_cookie=1 aufrecht;
ein 24-MiB-Feld löst vor RINIT einen OOM aus). Leak-B platziert ein Array mit 257 Elementen, dessen
zend_array PS(id)−0x10 überlappt: ZSTR_LEN liest nTableSize=512, und das
URL-kodierte Cookie trägt pDestructor (→ libphp-Basis) sowie neun arData-
Heap-Zeiger (Mehrheitsentscheid → Chunk-Basis). Das gemessene Chunk−libphp-Delta
ist nicht konstant über die Generationen hinweg (−0x1a00000 / +0x3600000 / −0x1c00000),
daher ist die Offenlegung tatsächlich tragend. Der Fire-Lauf erfolgt dann mit den
geleakten Adressen. Voraussetzungen: display_errors=On, post_max_size über
dem OOM-Feld, memory_limit darunter, Single-Worker-Prefork.
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)
Vollständige Reproduktion und Kontrollleiter: docs/END-TO-END.md.
Optimierte Amplifikation durch doppelte Header: kurze X: a-Header (6 Byte auf der Leitung)
nutzen PHPs feste SMART_STR_START_SIZE von 256 Byte pro gespeichertem Header aus
(~314 B Speicher pro 6 B auf der Leitung). Gemessen durch exakte OOM-Schwellen-Bisektion auf
8.5.11; identisch auf 7.4.33 / 8.1.34 / 8.4.26 (51–52x):
| memory_limit | min. OOM-Body | Verstärkung |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Alle Bodies passen in ein Standard-post_max_size=8M — die praktische Upload-
Barriere ist ausgehebelt auf jeder ungepatchten Version. Chunked-Framing
umgeht zusätzlich die Content-Length-Vorabprüfung (8,85 MB chunked geparst
und OOM'd, während derselbe Body mit Content-Length abgelehnt wurde). Der
8.5.12-Patch schließt beide Vektoren (Filterung unbekannter Header + Streaming-
Durchsetzung von post_max_size). Die verschachtelte Array-„Bomben"-Form lässt sich nicht
als exponentiell reproduzieren (max. 9,6x, 1,16 MiB — kann selbst 8M nicht zum OOM bringen).
Details: docs/RELEASE-RECONCILIATION-2026-10-21.md.
Verifiziert auf einem GPG-verifizierten, token-identischen 8.5.12-ASAN-Build — die beiden CVEs sind behoben, aber drei separate Pfade zur Auswirkung überleben den Patch:
var_unserializer.c:906:67); die ZEND_HAS_OOM-Guards
des Patches sind auf diesem Pfad wirkungslos; BG(serialize_lock) wird
weiterhin nicht um den __destruct-Dispatch herum gesetzt.php_serialize geschriebene |-Key-Session,
die vom php-Handler nach einem session_start() in der App-Phase dekodiert wird,
instanziiert das Gadget sowohl bei mod_php php_value als auch bei FPM .user.ini;
der incomplete_classes_only-Guard des Patches ist nur auf das Pre-RINIT-
Upload-Fenster beschränkt.Im Labor verifiziert auf php:8.5.11-fpm: Eine 24-MB-Session bei memory_limit=32M
läuft innerhalb von php_var_unserialize in einen OOM und der Worker überlebt — die Exit-70-
Barriere gilt nur für OOM beim Startup. gdb zeigt BG(unserialize).level=1
und den veralteten Datenzeiger, der bis in das nächste Request-RINIT-Fenster des rfc1867
fortbesteht; ein 8191-Byte-Multipart-Feld erzeugt ein 8216-Byte-
_emalloc, das den veralteten Zeiger überlappt und den Worker in
var_tmp_var während des Request-Starts mit SIGSEGV beendet. Gemessene Auswirkung: wiederholbarer Worker-
Absturz (DoS) plus ein Speicherkorruptions-Primitiv; RCE über den Absturz hinaus ist
nicht bewiesen. Voraussetzungen: niedriges memory_limit, große angreiferbeeinflusste
Session, ein session_start()-Endpunkt, upload_progress=On und
pm=static max_children=1 für zuverlässige Same-Worker-Paarung. FPM ist
daher für diese Klasse bedingt, nicht kategorisch geschützt.