Diseccionando PHP2Shell (CVE-2026-109618 y CVE-2026-109619): Análisis Técnico de RCE Pre-Autenticación en el Núcleo de PHP, Mitigación y Parches
Análisis técnico profundo de PHP2Shell (CVE-2026-109618 y CVE-2026-109619), las vulnerabilidades críticas CVSS 9.2 de ejecución remota de código pre-autenticación en el núcleo de PHP, incluyendo causa raíz en C, reglas WAF y guía de parcheo.
Cuando aparece una vulnerabilidad en un plugin popular de WordPress o en un paquete de Laravel, el radio de impacto se limita a los sitios que ejecutan ese componente específico. Pero cuando se descubre una vulnerabilidad de corrupción de memoria pre-autenticación dentro del propio intérprete central de PHP —específicamente en la tubería de inicio de solicitud que se ejecuta antes de que comience tu script—, el radio de impacto abarca prácticamente cualquier servidor PHP sin parchear en Internet.
Conocidas colectivamente como PHP2Shell, las vulnerabilidades CVE-2026-109618 y CVE-2026-109619 representan una de las divulgaciones más críticas en el motor de PHP desde el fallo de inyección de argumentos CGI (CVE-2012-1823) y el fallo de iconv en Windows (CVE-2024-4577). Con una puntuación de 9.2 (Crítica) en la escala CVSS v4.0, los fallos residen en la intersección del analizador de flujos multipart/form-data RFC 1867 (main/rfc1867.c), el subsistema de progreso de carga de sesiones (ext/session/session.c) y el manejo de estados de error del motor Zend.
Tras una filtración prematura en redes sociales y foros antes de la fecha de divulgación coordinada, la Fundación PHP emitió una alerta preventiva y publicó actualizaciones de seguridad de emergencia el 21 de octubre de 2026 a las 14:00 UTC. En este análisis técnico profundo, examinamos la causa raíz a nivel de código C de PHP2Shell, explicamos por qué las mitigaciones iniciales de configuración resultaron insuficientes por sí solas y detallamos una guía completa de parcheo, mitigación perimetral y caza de amenazas.
1. Por Qué PHP2Shell es Tan Peligroso: Ejecución Pre-Script en la Fase RINIT
Un error común entre desarrolladores y administradores es pensar: "Mi aplicación no utiliza sesiones de PHP ni acepta subida de archivos, así que no soy vulnerable".
Esa suposición es falsa. Para comprender por qué, debemos observar el ciclo de vida interno de una solicitud en la Server API (SAPI) de PHP. Cuando llega una petición HTTP a Nginx + PHP-FPM o Apache + mod_php dirigida a cualquier archivo .php (incluso un sencillo healthcheck.php de una línea), el intérprete ejecuta varias fases internas antes de compilar o ejecutar el script:
Petición HTTP (Content-Type: multipart/form-data)
│
├─► 1. php_request_startup() [Fase RINIT]
│ ├─► sapi_activate()
│ │ └─► sapi_read_post_data()
│ │ └─► rfc1867_post_handler() [main/rfc1867.c]
│ │ │
│ │ ├─► Analiza límites (boundaries) y fragmentos multipart
│ │ └─► Invoca php_rfc1867_callback()
│ │ └─► php_session_rfc1867_callback() [ext/session/session.c]
│ │ └─► ⚠️ LA VULNERABILIDAD SE DISPARA AQUÍ
│ │
│ └─► zend_activate_modules()
│
└─► 2. php_execute_script() [Código PHP de usuario — NUNCA SE ALCANZA SI SE EXPLOTA]Dado que el análisis de multipart/form-data bajo RFC 1867 ocurre dentro de sapi_activate() durante la fase de inicialización de la petición (RINIT), PHP procesa el cuerpo multipart e invoca los hooks registrados antes de que se ejecute el código de la aplicación, los middlewares del framework, los controles de autenticación o los tokens CSRF. Cualquier ruta .php accesible públicamente puede actuar como punto de entrada.
2. Causa Raíz Técnica: Diseccionando CVE-2026-109618 y CVE-2026-109619
PHP2Shell no es un único error aislado, sino un par de vulnerabilidades estrechamente vinculadas en el código C del núcleo de PHP que, combinadas, permiten una Ejecución Remota de Código fiable y sin autenticación:
- CVE-2026-109618: Agotamiento de memoria y manipulación de estado en el análisis de flujos multipart RFC 1867 (
main/rfc1867.c) en su intersección con el seguimiento de cargas de sesión. - CVE-2026-109619: Use-After-Free (UAF) en el heap mediante punteros colgantes (dangling pointers)
zval *enext/session/session.cy corrupción del estado global tras condiciones de error fatal (bailout).
Anatomía del Puntero Colgante en ext/session/session.c
Desde PHP 5.4, la función session.upload_progress viene habilitada por defecto (session.upload_progress.enabled = 1). Cuando una petición multipart/form-data incluye un campo que coincide con session.upload_progress.name (por defecto, PHP_SESSION_UPLOAD_PROGRESS), rfc1867_post_handler() emite una secuencia de eventos hacia php_session_rfc1867_callback():
MULTIPART_EVENT_START: Inicializa la estructura de seguimiento (php_session_rfc1867_progress).MULTIPART_EVENT_FORMDATA: Captura la clavePHP_SESSION_UPLOAD_PROGRESSe inicializa la sesión.MULTIPART_EVENT_FILE_START: Crea el arreglo de metadatos del archivo dentro de$_SESSION["upload_progress_..."].MULTIPART_EVENT_FILE_DATA: Actualiza los contadores de bytes a medida que llegan fragmentos de datos.MULTIPART_EVENT_FILE_ENDyMULTIPART_EVENT_END: Finaliza y guarda el estado de la sesión.
Para optimizar el rendimiento durante subidas de múltiples fragmentos, la implementación vulnerable de php_session_rfc1867_progress almacenaba en caché punteros crudos en C (zval *) que apuntaban directamente a los buckets internos (arData) de la tabla hash dinámica (HashTable) de $_SESSION:
/* Patrón de estructura vulnerable en ext/session/php_session.h (Pre-Parche) */
typedef struct _php_session_rfc1867_progress {
size_t sname_len;
zend_string *key;
zval sid;
zval data;
zval files;
zval *post_bytes_processed; /* ⚠️ Puntero crudo hacia bucket de HashTable */
zval *current_file;
zval *current_file_bytes_processed; /* ⚠️ Puntero crudo hacia bucket de HashTable */
/* ... */
} php_session_rfc1867_progress;En C y en el motor Zend, conservar un puntero crudo zval * hacia una HashTable mutable a través de límites de entrada/salida de red viola una regla fundamental de seguridad de memoria: una HashTable puede realojar, redimensionar o liberar su búfer interno arData en cualquier momento cuando se insertan elementos, se recalcula el hash o se reinicia el estado.
HashTable se libera o se mueve en el heap de Zend (efree / erealloc). Sin embargo, progress->post_bytes_processed y progress->current_file_bytes_processed nunca se invalidan: quedan como punteros colgantes (dangling pointers) apuntando a memoria liberada.Del Puntero Colgante a la Ejecución Arbitraria de Código
Una vez que esos punteros quedan colgando en el Zend Memory Manager (ZMM), el atacante controla los siguientes pasos:
- Reasignación controlada en el Heap (Heap Grooming): Nuevos campos o encabezados dentro del mismo flujo HTTP alojan estructuras internas de Zend (como instancias de
zend_string,zend_arrayozend_object) que ocupan el bloque exacto de memoria recién liberado al que aún apuntaprogress->post_bytes_processed. - Escritura Controlada mediante Actualizaciones de Progreso: A medida que llegan más datos de archivo (disparando
MULTIPART_EVENT_FILE_DATA), el callback ejecuta:Z_LVAL_P(progress->post_bytes_processed) = data->post_bytes_processed;
Como el atacante controla exactamente cuántos bytes envía en el cuerpo HTTP, el intérprete sobrescribe un entero de 64 bits controlado por el atacante directamente sobre los campos internos de la nueva estructura. - Corrupción de Estado ante Error Fatal (CVE-2026-109619): Al encadenar esta escritura con estados de error forzado (
zend_bailout) o alterar etiquetas de tipozvaly punteros destructores (pDestructordentro de unzend_array), la rutina de limpieza al finalizar la petición ejecuta un puntero a función controlado por el atacante (comosystem/zend_call_function).
3. Cómo Soluciona el Fallo el Parche Oficial
En las versiones de seguridad publicadas el 21 de octubre de 2026, el equipo central de PHP rediseñó la forma en que ext/session/session.c registra el progreso de carga.
En lugar de mantener punteros crudos zval * apuntando a buckets mutables de una HashTable entre eventos multipart, el código parcheado almacena los contadores como enteros primitivos por valor (zend_long) directamente dentro de la estructura php_session_rfc1867_progress y los sincroniza mediante una función dedicada (php_session_rfc1867_sync_counters()) únicamente al volcar el estado:
/* Diseño parcheado en ext/session/php_session.h y session.c */
typedef struct _php_session_rfc1867_progress {
/* Se eliminan los punteros crudos zval* y se reemplazan por valores primitivos */
zend_long post_bytes_processed;
zend_long current_file_bytes_processed;
/* ... */
} php_session_rfc1867_progress;
static void php_session_rfc1867_sync_counters(php_session_rfc1867_progress *progress) {
/* Busca y actualiza las claves de la HashTable por valor bajo demanda,
eliminando cualquier desreferencia de punteros colgantes entre fragmentos */
if (Z_TYPE(progress->data) == IS_ARRAY) {
add_assoc_long_ex(&progress->data, "bytes_processed",
sizeof("bytes_processed") - 1, progress->post_bytes_processed);
}
if (Z_TYPE(progress->current_file) == IS_ARRAY) {
add_assoc_long_ex(&progress->current_file, "bytes_processed",
sizeof("bytes_processed") - 1, progress->current_file_bytes_processed);
}
}4. Versiones Parcheadas Oficiales y Matriz de Ramas
La Fundación PHP solo publica parches oficiales para las ramas activas soportadas de PHP 8.x. Si ejecutas una versión al final de su vida útil (EOL) como PHP 8.1, 8.0, 7.x o 5.x, el proyecto oficial no publicará parches upstream para tu versión: debes actualizar a una rama soportada, usar backports empresariales de tu distribución (Ubuntu Pro, Debian LTS, RHEL) o aplicar mitigaciones estrictas de inmediato.
| Rama de PHP | Estado de Soporte | Versiones Vulnerables | Versión Parcheada |
|---|---|---|---|
| PHP 8.5 | Soporte Activo | < 8.5.12 | 8.5.12+ |
| PHP 8.4 | Soporte Activo | < 8.4.27 | 8.4.27+ |
| PHP 8.3 | Soporte de Seguridad | < 8.3.36 | 8.3.36+ |
| PHP 8.2 | Soporte de Seguridad | < 8.2.35 | 8.2.35+ |
| PHP 8.1 / 8.0 / 7.x / 5.x | Fin de Vida (EOL) | Todas las versiones 5.4+ | Sin parche oficial (Requiere actualizar o backport LTS) |
Comandos de Actualización Paso a Paso
1. Ubuntu / Debian (usando ppa:ondrej/php o repositorios Sury):
sudo apt update
sudo apt install --only-upgrade "php*"
# Fundamental: Reiniciar PHP-FPM o Apache para que los workers carguen el nuevo binario
sudo systemctl restart php8.4-fpm # Ajustar según tu versión (8.2, 8.3, 8.4, 8.5)
sudo systemctl restart apache2 # Si utilizas mod_php2. RHEL / AlmaLinux / Rocky Linux / Fedora (Repositorio Remi):
sudo dnf clean all
sudo dnf upgrade "php*" -y
sudo systemctl restart php-fpm httpd3. Contenedores Docker y Kubernetes:
docker build --pull --no-cache -t mi-app:parcheada .
kubectl rollout restart deployment/mi-app-php5. Por Qué session.upload_progress.enabled = 0 No Basta por Sí Solo
En las primeras horas tras la filtración, numerosos boletines sugirieron desactivar session.upload_progress.enabled = 0 en php.ini como solución definitiva. Posteriormente, los investigadores de seguridad advirtieron que este ajuste por sí solo no garantiza protección total por tres motivos:
- Dos CVEs Distintos: Aunque desactivar
session.upload_progress.enabledbloquea la ruta principal haciaphp_session_rfc1867_callback, CVE-2026-109618 y CVE-2026-109619 también abarcan el análisis multipart RFC 1867 y la corrupción de estado global tras errores fatales. - Sobrescritura por Directorio (
INI_PERDIR): La directiva tiene permisosPHP_INI_PERDIR. Si un atacante o usuario en hosting compartido puede modificar un archivo.user.inio.htaccess, puede reactivar la directiva a menos que esté bloqueada conphp_admin_flag. - Workers en Memoria: Modificar
php.inino surte efecto en los procesos hijos dephp-fpmhasta que el servicio maestro se reinicia.
6. Mitigación en Profundidad y Parcheo Virtual (WAF / Nginx)
A. Bloqueo Fijo en Pools de PHP-FPM y php.ini
; En /etc/php/8.x/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[session.upload_progress.cleanup] = on
; En php.ini — limitar recursos multipart y funciones peligrosas
session.upload_progress.enabled = 0
max_file_uploads = 5
max_input_time = 15
disable_functions = exec,passthru,shell_exec,system,proc_open,popen,pcntl_execB. Mitigación en Proxy Inverso Nginx
Si tu aplicación no requiere subidas públicas de archivos, bloquea las solicitudes multipart/form-data en el borde y reduce los tiempos de espera del cuerpo HTTP para impedir pausas deliberadas a mitad del flujo:
# 1. Reducir timeouts de cuerpo para frustrar pausas de explotación UAF
client_body_timeout 10s;
client_header_timeout 10s;
# 2. Si el sitio NO requiere subida pública de archivos, rechazar multipart/form-data:
if ($content_type ~* "^multipart/form-data") {
return 403;
}C. Reglas WAF para ModSecurity / Coraza / Centinela
# Bloquear cualquier campo multipart o parámetro que invoque PHP_SESSION_UPLOAD_PROGRESS
SecRule ARGS_NAMES|FILES_NAMES|MULTIPART_PART_HEADERS "(?i)PHP_SESSION_UPLOAD_PROGRESS" \
"id:1009618,phase:2,deny,status:403,log,t:none,t:urlDecodeUni,msg:'PHP2Shell (CVE-2026-109618/109619) Intento de Explotacion Session Upload Progress'"
# Bloquear peticiones multipart con anomalías estructurales
SecRule MULTIPART_STRICT_ERROR "!@eq 0" \
"id:1009619,phase:2,deny,status:403,log,t:none,msg:'Error Estricto Multipart - Posible Explotacion CVE-2026-109618'"7. Caza de Amenazas (Threat Hunting) y Respuesta a Incidentes
Dado que PHP2Shell se filtró antes del lanzamiento del parche del 21 de octubre, el escaneo oportunista comenzó de inmediato. Aplicar el parche cierra la vulnerabilidad, pero no expulsa a un atacante que ya haya instalado una web shell. Ejecuta estas verificaciones en todos tus servidores PHP expuestos:
Auditar registros del kernel en busca de fallos de segmentación (SIGSEGV) en php-fpm:Los intentos fallidos de corrupción de memoria UAF suelen provocar caídas con señal 11 (SIGSEGV) en los procesos trabajadores de PHP antes de lograr la ejecución:
dmesg -T | grep -i -E "php-fpm|segfault"
journalctl -u "php*-fpm" --since "7 days ago" | grep -E "SIGSEGV|SIGABRT|exited on signal"Buscar archivos PHP creados o modificados recientemente:
find /var/www -type f -name "*.php" -mtime -7 -lsInspeccionar procesos hijos anómalos de PHP-FPM / Apache:
ps auxf | grep -E -A 5 "php-fpm|apache2|httpd"Conclusión
PHP2Shell (CVE-2026-109618 y CVE-2026-109619) demuestra una vez más que el código C heredado que se ejecuta durante la fase de arranque de peticiones HTTP constituye una superficie de ataque crítica. Debido a que rfc1867.c y ext/session/session.c procesan los flujos multipart antes de que comience el script PHP de la aplicación, ningún middleware de autenticación ni framework en espacio de usuario puede proteger a un intérprete sin actualizar.
Actualiza tus entornos PHP a las versiones 8.5.12, 8.4.27, 8.3.36 u 8.2.35 de inmediato, bloquea session.upload_progress.enabled mediante php_admin_flag, filtra el tráfico multipart/form-data no solicitado en el borde y audita tus registros en busca de caídas de workers o artefactos de post-explotación.