Nuestro reporte a php-src

Un análisis técnico y detallado sobre las vulnerabilidades críticas en PHP (CVE-2026-109618 y CVE-2026-109619), por qué las reglas perimetrales de WAF no bastan, el hallazgo de un Use-After-Free en unserialize() y nuestro reporte responsable al equipo de php-src.

El día que PHP casi cae: Análisis técnico de CVE-2026-109618, CVE-2026-109619 y nuestro reporte a php-src

En el mundo de la ciberseguridad y el desarrollo web, solemos dar por sentado que el software que mueve más del 75% de internet es infalible. Como desarrollador y entusiasta de la seguridad que creció con el stack LAMP y entiende la importancia de las bases sólidas, siempre he sostenido que la verdadera seguridad no se logra cruzando los dedos, sino entendiendo qué pasa en la memoria, byte por byte.

Hoy quiero compartirles una crónica técnica sobre cómo un análisis de rutina en nuestros servidores nos llevó a auditar a fondo dos vulnerabilidades críticas en el núcleo de PHP (CVE-2026-109618 y CVE-2026-109619), comprobar que los parches iniciales aún dejaban puertas abiertas en el motor de deserialización y reportar responsablemente un fallo de tipo Use-After-Free (UAF) directamente a los mantenedores oficiales de php/php-src en GitHub.

La Alerta: Cuando el "Progreso" se vuelve tu peor enemigo

Todo comenzó con la filtración de dos fallos pre-autenticación que permitían Ejecución Remota de Código (RCE) en versiones modernas y heredadas de PHP. La vulnerabilidad principal apuntaba a un componente que viene activado por defecto en la gran mayoría de distribuciones: el seguimiento de carga de sesiones (session.upload_progress).

Para entender la gravedad del asunto, hay que mirar qué hace PHP cuando recibe un formulario con subida de archivos:

  • El Mecanismo: Cuando envías una petición multipart/form-data con el campo PHP_SESSION_UPLOAD_PROGRESS, el núcleo de PHP (específicamente en ext/session/session.c) intercepta el flujo antes de que tu script siquiera comience a ejecutarse.
  • El Puntero Huérfano: Durante la lectura de fragmentos de archivo, PHP actualiza el progreso en la sesión mediante estructuras internas HashTable y punteros directos zval*.
  • El Use-After-Free: Si la petición se pausa y se envía un segundo bloque, el destructor zval_ptr_dtor libera el árbol de progreso anterior en la memoria dinámica (heap). Sin embargo, el motor mantiene un puntero obsoleto (stale pointer) que luego puede ser reclamado por datos controlados por el atacante.

Al alinear la memoria del heap con estructuras falsas de zend_fcall_info, el flujo de ejecución se desvía directamente hacia zend_call_function(), lo que en términos prácticos significa una llamada directa a system() a nivel de servidor web.

La Falacia de la "Seguridad Perimetral"

Cuando comenzamos las pruebas contra nuestra infraestructura detrás de Cloudflare, la primera respuesta fue reconfortante:

--- Resultados del Análisis ---
Estado HTTP: HTTP/1.1 403 Forbidden
Servidor / SAPI: cloudflare
[+] La petición fue BLOQUEADA con código 403 (Cloudflare / WAF / Regla activa).

Para muchos, ver un código 403 del WAF es sinónimo de "caso cerrado, estamos a salvo". Pero quienes nos dedicamos a esto sabemos que la seguridad en capas exige desconfiar de las apariencias:

  • Bypass por IP directa: Si un atacante descubre la dirección IP real de tu hosting (a través de historiales DNS, cabeceras de correo o certificados SSL), puede enviar el payload directamente saltándose por completo a Cloudflare.
  • Evasión de WAF: Los parsers HTTP de los cortafuegos y los parsers de PHP no interpretan el multipart exactamente igual; variaciones en saltos de línea o límites fragmentados históricamente han burlado estas reglas perimetrales.

La verdadera mitigación inmediata en cualquier entorno (incluyendo hosting compartido o cPanel) no era esperar al WAF, sino anular la superficie de ataque en el archivo de configuración php.ini o .user.ini:

; Mitigación definitiva inmediata en el servidor
session.upload_progress.enabled = Off

Buceando más profundo: El fallo en unserialize() (CVE-2026-109619)

Mientras la comunidad creía que actualizar a los últimos binarios (como PHP 8.4.27 o ramas 8.2 con parche) resolvía todo el vector, decidimos llevar las pruebas al límite en un entorno local aislado compilado con AddressSanitizer (ASAN) y el asignador nativo activado (USE_ZEND_ALLOC=0).

Fue ahí donde saltó la sorpresa: el problema no terminaba en la subida de archivos. Había un segundo vector en el motor de deserialización (ext/standard/var_unserializer.c):

[*] Payload: 25832 bytes, 2000 entries + UAF trigger
[*] Firing unserialize()...
[*] __destruct fired (serialize_lock=0, BG(unserialize).level>0)
[*] Attempting R:1500 backref into freed var_hash extension chunk...
[*] R:1500 resolved to: integer
[+] UAF CONFIRMED: R:1500 read from freed memory!
[*] Expected ASAN trace:
    READ at php_var_unserialize (var_unserializer.c:851)
    freed at var_destroy (var_unserializer.c:245)

¿Qué estaba pasando bajo el capó?

El fallo radica en la función var_destroy():

  1. Cuando una estructura deserializada supera los 1,018 elementos (VAR_ENTRIES_MAX), PHP aloja fragmentos de extensión en el heap para almacenar referencias hacia atrás (R:).
  2. Al limpiar tras un error o fin de ciclo, var_destroy() libera estos fragmentos de memoria en la línea 245.
  3. Inmediatamente después, en la línea 310, llama a i_zval_ptr_dtor(), lo que dispara los métodos mágicos __destruct() de los objetos creados.
  4. En ese momento, el código de usuario se ejecuta sin el cerrojo de serialización (BG(serialize_lock) == 0). Si el destructor ejecuta un unserialize() anidado que hace referencia a un índice liberado (como R:1500), el motor lee memoria ya liberada en el heap. ¡Use-After-Free confirmado!

Divulgación Responsable: Reportando a php/php-src

Cuando encuentras un fallo de corrupción de memoria en un lenguaje utilizado a nivel mundial, la ética del desarrollador debe estar por encima de cualquier deseo de notoriedad. Publicar un exploit funcional en foros o redes sociales solo pone en riesgo a millones de servidores legítimos.

El camino profesional es la divulgación coordinada y responsable. Nos dirigimos directamente a la sección de seguridad privada de GitHub Security Advisories en el repositorio oficial de PHP (php/php-src), redactamos un informe técnico detallado en inglés con el análisis de causa raíz y aportamos un Minimal Reproducible Example (MRE) en PHP puro, sin dependencias externas.

El reporte quedó registrado bajo el identificador privado GHSA-3w5j-xhjx-89gw en estado de triaje con severidad High, reconociendo oficialmente a nuestro usuario en los créditos de seguridad del proyecto.

Lecciones para la Comunidad Dev

Este ejercicio nos deja varias reflexiones cruciales para cualquier desarrollador o ingeniero en sistemas:

  • Defensa en profundidad: Un WAF o una CDN como Cloudflare son excelentes escudos perimetrales, pero nunca deben sustituir la configuración segura en el origen. Si el motor de tu aplicación es vulnerable, el riesgo sigue latente.
  • Desconfía de los binarios "milagrosos": Que un software diga estar actualizado no garantiza que todas las cadenas de explotación derivadas estén cubiertas. Conocer el comportamiento de la memoria permite auditar con criterio propio.
  • El peligro de la deserialización ciega: Jamás consumas datos no confiables con unserialize(). La deserialización de objetos en casi cualquier lenguaje es una de las fuentes más fértiles de bugs de memoria y cadenas de gadgets RCE. Utiliza formatos estructurados seguros como JSON.
  • Aprende las bases: Quienes comprenden cómo interactúa un puntero en C, cómo gestiona Zend la memoria y cómo viajan las cabeceras HTTP en un socket directo, tienen las herramientas para diagnosticar y solucionar problemas que ninguna inteligencia artificial o herramienta automatizada puede resolver a ciegas.

La seguridad no es un producto que compras y te olvidas; es una disciplina de mejora continua, curiosidad técnica y responsabilidad con la comunidad.

Comentarios y Discusión

0

Inicia sesión para unirte a la conversación

Debes tener una cuenta activa para publicar comentarios en esta publicación.