
PHP CVE-2026-109618/109619 — RCE non autenticato self-contained su PHP 8.5.11 stock (divulgazione in memoria in-band PS(id)→cookie + catena UAF), scale di controllo complete, controllo negativo su versione corretta in 8.5.12
Pacchetto di riproduzione in laboratorio e di ricerca sulle vulnerabilità per la classe php2shell di PHP 8.5.11 (CVE-2026-109618, CVE-2026-109619), ottobre 2026. Stato: Dimostrato — RCE self-contained (quattro richieste HTTP in chiaro, nessun debugger, nessun accesso locale), più i risultati su amplificazione, lacune nelle patch e stato obsoleto di FPM riportati di seguito.
Contenuti: Risultati · Catene · Amplificazione · Lacune nelle patch · Stato obsoleto di FPM · Bypass WAF · Ambiente · Avvio rapido · Disciplina della dimostrazione · Struttura · Contesto pubblico · Dichiarazione
| # | Risultato | Classificazione |
|---|---|---|
| 1 | RCE self-contained end-to-end su PHP 8.5.11 — quattro richieste HTTP in chiaro, reperimento degli indirizzi in-band tramite divulgazione PS(id)→cookie, entrambe le basi da un'unica fuga di 512 byte di PHPSESSID; 12/12 esecuzioni riuscite, uid=33, 8 generazioni ASLR | Dimostrato — RCE self-contained |
| 2 | Amplificazione di memoria 52x tramite header multipart duplicati da 6 byte — aggira post_max_size (un corpo da 2,57 MB causa OOM su un limite di 128M) su PHP 7.4–8.5.11 | Dimostrato — laboratorio, tutte e quattro le versioni |
| 3 | Tre percorsi di exploit sopravvivono alla patch di sicurezza 8.5.12 (UAF da rientro in var_destroy a n=1015, mancata corrispondenza del gestore di serializzazione, iniezione dell'avanzamento di upload) | Dimostrato — ASAN su 8.5.12 verificato con GPG |
| 4 | Stato obsoleto BG(unserialize) in FPM: il worker sopravvive a un OOM di decodifica della sessione, lo stato obsoleto viene consumato dalla finestra rfc1867 della richiesta successiva, un consumo modellato sull'heap causa SIGSEGV del worker | Dimostrato — meccanismo + DoS; RCE non dimostrata |
| 5 | Bypass del separatore Content-Type (multipart/form-data, boundary=X) — ogni firma WAF pubblicata basata su Content-Type è aggirabile | Dimostrato — comportamento del parser SAPI.c |
| 6 | Entrambe le catene CVE sono inefficaci sulla 8.5.12 corretta (configure identico a livello di token, sorgente verificato con GPG) | Dimostrato — controllo negativo |
| 7 | La teoria pubblica della "zval in cache per session.upload_progress" — confutata su 4 versioni stock di PHP + 3 build del fornitore LiteSpeed, in tutti gli angoli di pulizia/concorrenza | Confutata — esaustivamente |
Due catene indipendenti di corruzione della memoria raggiungono chiamate indirette controllate dall'attaccante sull'immagine stock php:8.5.11-apache; il wrapper end-to-end combina il meccanismo della Catena B con una nuova divulgazione in-band per un'esecuzione completamente remota.
| Catena A — bucket obsoleto | Catena B — riferimento pendente | |
|---|---|---|
| Meccanismo | Il bailout per OOM lascia un bucket di HashTable pubblicato ma non inizializzato; lo shutdown lo consuma come zval fittizia → array fittizio → pDestructor | Un OOM pre-RINIT lascia PS(http_session_vars) con un zend_reference pendente; una stringa viva di 31 byte lo recupera; riferimento contraffatto → pDestructor di zend_hash_destroy |
| Allocatore | tracked/system (USE_ZEND_ALLOC=0) | Zend MM predefinito stock |
| Terminale | system() di libc (indirizzi calibrati con gdb) | zif_system @ libphp+0x5996e0 (senza libc) |
| Debugger al momento dell'esecuzione | solo calibrazione | nessuno — FIRE_GDB=0 |
| Riproducibilità | 2 generazioni parent randomizzate | ripetuta, anche tra esecuzioni diverse |
Leak-A arma un PS(id) pendente (la 1001ª variabile fa scattare max_input_vars →
headers_sent=1; il fallimento di reset_id con cookie vuoto mantiene send_cookie=1;
un campo da 24 MiB causa OOM prima del RINIT). Leak-B piazza un array di 257 elementi il cui
zend_array si sovrappone a PS(id)−0x10: ZSTR_LEN legge nTableSize=512 e il
cookie codificato in URL trasporta pDestructor (→ base libphp) più nove puntatori
heap arData (voto di maggioranza → base del chunk). Il delta misurato chunk−libphp
non è costante tra le generazioni (−0x1a00000 / +0x3600000 / −0x1c00000),
quindi la divulgazione è genuinamente portante. Il fire viene quindi eseguito con gli
indirizzi divulgati. Prerequisiti: display_errors=On, post_max_size superiore
al campo OOM, memory_limit inferiore ad esso, prefork a worker singolo.
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)
Ricevuta completa e scala di controllo: docs/END-TO-END.md.
Amplificazione ottimizzata tramite header duplicati: header corti X: a (6 byte sulla rete)
sfruttano la dimensione fissa di 256 byte di SMART_STR_START_SIZE di PHP per ogni header salvato
(~314 B di memoria per 6 B sulla rete). Misurato tramite bisezione esatta della soglia OOM su
8.5.11; identico su 7.4.33 / 8.1.34 / 8.4.26 (51–52x):
| memory_limit | corpo minimo per OOM | amplificazione |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Tutti i corpi rientrano in un post_max_size=8M predefinito — la barriera pratica
all'upload viene aggirata su ogni versione non corretta. Il framing chunked
aggira inoltre il pre-controllo su Content-Length (8.85 MB in chunked analizzati
e mandati in OOM dove lo stesso corpo con Content-Length veniva rifiutato). La
patch 8.5.12 chiude entrambi i vettori (filtro degli header sconosciuti + applicazione
di post_max_size in streaming). La forma "bomba" ad array annidati non
si riproduce come esponenziale (max 9.6x, 1.16 MiB — non può mandare in OOM nemmeno 8M).
Dettagli: docs/RELEASE-RECONCILIATION-2026-10-21.md.
Verificato su una build ASAN 8.5.12 verificata con GPG e identica per token — le due CVE sono corrette, ma tre percorsi distinti verso l'impatto sopravvivono alla patch:
var_unserializer.c:906:67); le guardie
ZEND_HAS_OOM della patch sono inerti su questo percorso; BG(serialize_lock)
non viene ancora acquisito attorno al dispatch di __destruct.| scritta da
php_serialize e decodificata dall'handler php dopo una session_start() in fase applicativa
istanzia il gadget sia su mod_php php_value sia su FPM .user.ini;
la guardia incomplete_classes_only della patch è limitata alla sola finestra di upload
pre-RINIT.