
PHP CVE-2026-109618/109619 — RCE no autenticado autocontenido en PHP 8.5.11 de serie (divulgación de memoria PS(id)→cookie en banda + cadena UAF), escaleras de control completas, control negativo de versión corregida en 8.5.12
Paquete de reproducción de laboratorio e investigación de vulnerabilidades para la clase php2shell de PHP 8.5.11 (CVE-2026-109618, CVE-2026-109619), octubre de 2026. Estado: Probado — RCE autocontenido (cuatro solicitudes HTTP simples, sin depurador, sin acceso local), además de los hallazgos de amplificación, brechas de parche y estado obsoleto de FPM que se detallan a continuación.
Contenido: Resultados · Cadenas · Amplificación · Brechas de parche · Estado obsoleto de FPM · Bypass de WAF · Entorno · Inicio rápido · Disciplina de prueba · Estructura · Contexto público · Declaración
| # | Hallazgo | Clasificación |
|---|---|---|
| 1 | RCE autocontenido de extremo a extremo en PHP 8.5.11 — cuatro solicitudes HTTP simples, obtención de direcciones en banda mediante la divulgación PS(id)→cookie, ambas bases a partir de una única filtración de PHPSESSID de 512 bytes; 12/12 ejecuciones, uid=33, 8 generaciones de ASLR | Probado — RCE autocontenido |
| 2 | Amplificación de memoria de 52x mediante cabeceras multipart duplicadas de 6 bytes — derrota post_max_size (un cuerpo de 2.57 MB agota la memoria con un límite de 128M) en PHP 7.4–8.5.11 | Probado — laboratorio, las cuatro versiones |
| 3 | Tres rutas de explotación sobreviven al parche de seguridad 8.5.12 (UAF de reentrada en var_destroy con n=1015, desajuste del manejador de serialización, inyección de progreso de carga) | Probado — ASAN sobre 8.5.12 verificado con GPG |
| 4 | BG(unserialize) obsoleto en FPM: el worker sobrevive a un OOM de decodificación de sesión, el estado obsoleto es consumido por la ventana rfc1867 de la siguiente solicitud, el consumo con forma de heap provoca SIGSEGV en el worker | Probado — mecanismo + DoS; RCE no probado |
| 5 | Bypass del separador de Content-Type (multipart/form-data, boundary=X) — toda firma de WAF publicada anclada a Content-Type es eludible | Probado — comportamiento del analizador de SAPI.c |
| 6 | Ambas cadenas CVE muertas en 8.5.12 parcheado (configure idéntico en tokens, código fuente verificado con GPG) | Probado — control negativo |
| 7 | La teoría pública del "zval en caché session.upload_progress" — refutada en 4 versiones estándar de PHP + 3 compilaciones de proveedor de LiteSpeed, en todos los rincones de limpieza/concurrencia | Refutada — exhaustiva |
Dos cadenas independientes de corrupción de memoria alcanzan llamadas indirectas controladas por el atacante en la imagen estándar php:8.5.11-apache; el envoltorio de extremo a extremo combina el mecanismo de la Cadena B con una novedosa divulgación en banda para una ejecución totalmente remota.
| Cadena A — bucket obsoleto | Cadena B — referencia colgante | |
|---|---|---|
| Mecanismo | El rescate por OOM deja un bucket de HashTable publicado pero sin inicializar; el apagado lo consume como un zval falso → array falso → pDestructor | Un OOM previo a RINIT deja PS(http_session_vars) con una zend_reference colgante; una cadena viva de 31 bytes la reclama; referencia forjada → pDestructor de zend_hash_destroy |
| Asignador | rastreado/del sistema (USE_ZEND_ALLOC=0) | Zend MM predeterminado estándar |
| Terminal | system() de libc (direcciones calibradas con gdb) | zif_system @ libphp+0x5996e0 (sin libc) |
| Depurador en el momento de la ejecución | solo calibración | ninguno — FIRE_GDB=0 |
| Reproducibilidad | 2 generaciones de padres aleatorizadas | repetida, incluso entre ejecuciones |
Leak-A arma un PS(id) colgante (la variable número 1001 desborda max_input_vars →
headers_sent=1; el fallo de reset_id con cookie vacía mantiene send_cookie=1;
un campo de 24 MiB provoca OOM antes del RINIT). Leak-B coloca un array de 257 elementos cuyo
zend_array se solapa con PS(id)−0x10: ZSTR_LEN lee nTableSize=512 y la
cookie codificada en URL transporta pDestructor (→ base de libphp) más nueve punteros
arData del heap (voto por mayoría → base del chunk). El delta medido chunk−libphp
no es constante entre generaciones (−0x1a00000 / +0x3600000 / −0x1c00000),
por lo que la divulgación es genuinamente determinante. El disparo se ejecuta entonces con las
direcciones filtradas. Requisitos previos: display_errors=On, post_max_size superior
al campo que provoca OOM, memory_limit inferior a este, prefork de un solo worker.
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)
Recibo completo y escalera de control: docs/END-TO-END.md.
Amplificación optimizada de cabeceras duplicadas: cabeceras cortas X: a (6 bytes en el cable)
explotan el SMART_STR_START_SIZE fijo de 256 bytes de PHP por cabecera guardada
(~314 B de memoria por cada 6 B en el cable). Medido mediante bisección exacta del umbral de OOM en
8.5.11; idéntico en 7.4.33 / 8.1.34 / 8.4.26 (51–52x):
| memory_limit | cuerpo mínimo para OOM | amplificación |
|---|---|---|
| 8M | 153 KB | 54.8x |
| 32M | 636 KB | 52.8x |
| 128M | 2.57 MB | 52.3x |
| 256M | 5.14 MB | 52.2x |
Todos los cuerpos caben dentro de un post_max_size=8M estándar — la barrera práctica de subida
queda derrotada en todas las versiones sin parchear. El framing por chunks
además omite la comprobación previa de Content-Length (8.85 MB en chunks parseados
y con OOM donde el mismo cuerpo con Content-Length fue rechazado). El
parche 8.5.12 cierra ambos vectores (filtrado de cabeceras desconocidas + aplicación de
post_max_size en streaming). La forma de "bomba" de array anidado no
se reproduce como exponencial (máx. 9.6x, 1.16 MiB — no puede provocar OOM ni siquiera con 8M).
Detalles: docs/RELEASE-RECONCILIATION-2026-10-21.md.
Verificado en una compilación ASAN 8.5.12 verificada con GPG e idéntica en tokens — los dos CVE están corregidos, pero tres rutas separadas hacia el impacto sobreviven al parche:
var_unserializer.c:906:67); las guardas
ZEND_HAS_OOM del parche son inertes en esta ruta; BG(serialize_lock)
sigue sin tomarse alrededor del despacho de __destruct.| escrita por php_serialize
decodificada por el manejador php tras un session_start() en fase de aplicación
instancia el gadget tanto en php_value de mod_php como en .user.ini de FPM;
la guarda incomplete_classes_only del parche está limitada únicamente a la ventana de subida
previa al RINIT.