Si administras una aplicación en PHP, esta semana toca revisar algo más que sus dependencias. PHP2Shell reúne dos vulnerabilidades del propio intérprete que pueden permitir ejecutar código en el servidor sin autenticarse. Ambas recibieron una puntuación CVSS v4.0 de 9.2. El riesgo merece atención incluso si tu aplicación está actualizada. [1]
Este artículo recoge información disponible hasta el 24 de octubre de 2026. Los ejemplos de código explican el manejo de memoria y las comprobaciones de configuración. Los modelos internos están simplificados para poder seguirlos sin conocer el código fuente completo del intérprete.
Una semana con dos avisos distintos
Según la cronología de watchTowr, el 20 de octubre circularon detalles antes de la divulgación prevista. La publicación se adelantó al 21, cuando llegaron las versiones corregidas. Ese contexto ayuda a entender por qué aparecieron recomendaciones y pruebas de concepto que fueron cambiando durante la semana. Conviene mirar la fecha de actualización de cada análisis. [1]
Los dos identificadores originales son CVE-2026-109618 y CVE-2026-109619. Puedes consultar sus registros en NVD, enlazados por separado para facilitar el seguimiento. La explicación técnica que sigue se apoya en los avisos publicados por PHP. [4] [5]
El aviso del 23 de octubre corresponde a CVE-2026-115560, un problema adicional de sesiones. [7]
Qué se rompe dentro de PHP
CVE-2026-109618: quedarse sin memoria en un mal momento
PHP usa longjmp() para abandonar operaciones cuando se queda sin memoria, una situación conocida como out of memory u OOM. Ese salto puede dejar estructuras internas a medio inicializar. La limpieza posterior puede usarlas como si fueran válidas y permitir ejecutar código con los permisos del proceso web. [2]
La investigación de Calif muestra el fallo de orden: se actualizan contadores internos antes de terminar de preparar el elemento al que apuntan. Si una reserva de memoria falla en medio, la limpieza posterior confía en esos contadores. El error está en cómo el intérprete maneja esa interrupción; no hace falta encontrar una llamada peligrosa en el código de tu aplicación para que exista el problema. [6]
Podemos representar ese orden con este pseudocódigo de elaboración propia. Los nombres son inventados para explicar el mecanismo; no es un fragmento de php-src ni un programa ejecutable:
// Modelo del fallo: se publica un elemento incompleto.
posicion = tabla.usados;
tabla.usados += 1;
// Una reserva puede abortar antes de terminar estas líneas.
tabla.elementos[posicion].clave = reservar_cadena(entrada);
tabla.elementos[posicion].valor = VALOR_INICIAL;
// La limpieza posterior confía en el contador.
for (i = 0; i < tabla.usados; i++) {
destruir_elemento(tabla.elementos[i]);
}
Supón que usados valía 4. Después del incremento, la limpieza recorrerá 5 elementos. Si la reserva interrumpe la operación, el quinto todavía no está completo. El contador y el contenido de la tabla dejaron de describir el mismo estado. Esta lectura de datos sin inicializar pertenece a la familia de errores que MITRE explica en CWE-457. [16]
Para diseñar una estructura de este tipo, conviene preparar el elemento antes de hacerlo visible al resto del programa:
// Modelo conceptual: construir antes de publicar.
// construir_elemento devuelve un resultado o un error controlado.
resultado = construir_elemento(entrada);
if (resultado.es_error) {
return ERROR_SIN_MEMORIA;
}
tabla.elementos[tabla.usados] = resultado.elemento;
tabla.usados += 1;
Cada elemento contado debe estar listo para usarse o destruirse. El arreglo real de PHP retrasa el error OOM suave hasta un punto seguro de la máquina virtual y termina el proceso si se supera un segundo umbral de memoria. Los modelos anteriores solo explican el orden de las operaciones. [2]
CVE-2026-109619: una referencia sobrevive a su memoria
Aquí el problema es el estado global que un proceso conserva entre peticiones. Algunas referencias pueden seguir apuntando a memoria de una petición que ya terminó y fue liberada. Una petición posterior, atendida por ese mismo proceso, puede usarlas antes de que se reinicialicen. Es un caso de use after free: usar memoria que ya dejó de pertenecer al objeto original. [3]
PHP confirmó rutas relacionadas con el progreso de subida de archivos y con las trazas de errores fatales de PHP 8.5. [3]
Este segundo modelo muestra qué significa que una referencia sobreviva a su memoria:
// Pseudocódigo: dos peticiones dentro del mismo proceso.
// Petición A
estado_global = peticion.datos;
liberar(peticion.datos);
// Falta retirar la referencia de estado_global.
// Petición B, atendida más tarde por ese proceso
if (estado_global != NULL) {
leer(estado_global); // apunta a memoria ya liberada
}
liberar() devuelve la memoria al administrador de memoria, pero no borra todas las variables que guardaban su dirección. Por eso comprobar != NULL no demuestra que el objeto siga vivo. Ese espacio podría contener otros datos cuando llegue la siguiente petición. MITRE describe ese ciclo de vida en CWE-416. [17]
En un diseño con un único propietario, la limpieza debe retirar las referencias persistentes y liberar el recurso dentro de un flujo controlado:
// Modelo de limpieza, no es el parche de PHP.
datos_a_liberar = estado_global;
estado_global = NULL;
liberar(datos_a_liberar);
El ejemplo solo cubre una referencia. Si hay más alias, todos deben respetar el mismo ciclo de vida; poner una variable en NULL deja pendientes los demás punteros. También hay que cubrir las salidas por error. Esa es la parte que una prueba del flujo normal puede pasar por alto. [17]
La forma de ejecutar PHP también cuenta
La investigación original demostró explotación en aplicaciones reales, incluido WordPress. En Apache con mod_php, parte del procesamiento vulnerable ocurre antes de ejecutar el código de la aplicación. Por eso un login o una validación dentro del sitio pueden llegar demasiado tarde para esa ruta. [6]
PHP-FPM también requiere revisión: el alcance depende de las fases del ciclo de vida en que se alcance el fallo. [2] [3]
Qué versiones corrigen los dos CVEs originales
Estas son las primeras versiones corregidas de cada rama mantenida que aparecen en los avisos de PHP y en el seguimiento de NCSC-NL: [2] [9]
| Rama de PHP | Primera versión corregida |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
Una distribución puede incorporar el arreglo como backport, conservando una numeración anterior de PHP. En ese caso necesitas el aviso de seguridad y la versión del paquete de tu proveedor para confirmar el parche. El repositorio de NCSC-NL ayuda a seguir productos y proveedores, incluidos servicios que llevan PHP integrado y que quizá no aparecen en tu inventario habitual. [9]
PHP publicó parches mínimos de código fuente para 7.4, 8.0 y 8.1. Esas ramas siguen fuera del mantenimiento normal; comprueba con tu proveedor si su paquete incorpora el arreglo. [7]
Cómo revisar tu entorno sin confundir la consola con la web
Puedes empezar con estos comandos en un sistema donde tengas acceso autorizado:
php --ini
php -r 'printf("PHP=%s\nSAPI=%s\n", PHP_VERSION, PHP_SAPI);'
El primer comando muestra la configuración cargada por ese binario. El segundo imprime su versión y su SAPI, la interfaz con la que se ejecuta PHP. Si aparece cli, estás inspeccionando la consola. La web puede usar otro binario, otra versión y otro php.ini. El manual distingue interfaces como fpm-fcgi y apache2handler. Comprueba esos datos en la configuración del servicio o del contenedor que atiende las peticiones. [10] [15]
Incluye contenedores, servidores detrás del balanceador y productos que empaquetan PHP. Tras instalar el arreglo, reinicia los procesos correspondientes y confirma el paquete o imagen en ejecución en cada nodo. [9]
Si necesitas una medida temporal mientras actualizas
Para bloquear las rutas confirmadas de sesiones de CVE-2026-109619, PHP recomienda desactivar el seguimiento del progreso de las subidas:
; En php.ini o en la configuración del servidor o del pool FPM
session.upload_progress.enabled = 0
Debe aplicarse antes de analizar la petición y debes reiniciar todos los procesos que la atienden. Para esta mitigación, PHP indica que no te apoyes en ini_set() ni en un .user.ini de FPM. Desactivar esta opción solo bloquea las rutas de sesiones confirmadas; no corrige toda la vulnerabilidad. [3]
La función sirve para registrar cuánto lleva subido un archivo. El manual explica que esa información se escribe en la sesión antes de ejecutar los scripts. Esa secuencia es la razón por la que cambiar el ajuste desde tu código puede llegar tarde. [11]
Para CVE-2026-109618, las medidas sobre memoria y tamaño de peticiones son parciales. Incluso memory_limit=-1 deja abiertos fallos de reserva de memoria y exige revisar la capacidad del servicio. El parche sigue siendo necesario. [2]
El aviso del 23 de octubre: CVE-2026-115560
Este último problema requiere que coincidan estas condiciones: la configuración inicial usa session.serialize_handler=php_binary o php_serialize; la aplicación lo cambia a php mediante ini_set() antes de llamar a session_start(); y el progreso de subida de sesiones sigue habilitado. [7]
Al revisar tu aplicación, busca cambios como este. Es un patrón para auditar, no una configuración recomendada:
<?php
// Contexto inicial: php_binary o php_serialize en la configuración.
// Este cambio, con upload progress activo, forma parte del caso descrito.
ini_set('session.serialize_handler', 'php');
session_start();
Encontrar solo session_start() no basta para identificar este caso. Necesitas comprobar el valor inicial, el cambio en tiempo de ejecución y si el progreso de subida permanece habilitado. Mantén separada esta revisión del arreglo de los dos CVEs originales. [8]
PHP recomienda mantener session.upload_progress.enabled=0 hasta la próxima versión regular con el arreglo. Si ya lo desactivaste, conserva esa medida. El comunicado también aclara que las aplicaciones que nunca cambian el serializador en tiempo de ejecución pueden mantener la función habilitada para este caso concreto. [7]
Al cierre de este artículo, PHP prevé la corrección en el siguiente ciclo regular. The Hacker News recoge ese anuncio como otra falla pendiente de parche. [7] [8]
Qué sabemos de los ataques
Unit 42 documentó una intrusión en una organización gubernamental europea vinculada a estas vulnerabilidades. Según su investigación, los atacantes obtuvieron hashes de contraseñas y secretos de MFA, e intentaron avanzar hacia Active Directory. El informe muestra que el impacto puede extenderse más allá del sitio web una vez conseguido el acceso inicial. Es el caso que describe ese equipo, no una afirmación sobre todos los servidores expuestos. [12]
Al revisar tu entorno, correlaciona las peticiones sospechosas con los registros de aplicación y la actividad del proceso PHP: archivos nuevos o modificados, procesos inesperados y conexiones salientes que no encajen con su funcionamiento habitual. Son pistas para investigar, no una firma que atribuya por sí sola un evento a PHP2Shell. El propio material de búsqueda de Unit 42 requiere contexto y puede producir falsos positivos. [12]
VulnCheck también distingue entre intentos y explotación conseguida. En una de sus observaciones, un atacante intentó usar un archivo como webshell, pero obtuvo una respuesta 404. Ver una petición con aspecto de exploit en un log no demuestra que haya ejecutado código. Tampoco basta con actualizar para descartar una intrusión anterior: si encuentras indicios, conserva la evidencia y abre una investigación. [13]
Cómo leer los PoCs que están circulando
Una prueba de concepto demuestra una ruta bajo determinadas condiciones. La versión, la arquitectura, la SAPI y la disposición de memoria pueden cambiar el resultado. VulnCheck fue actualizando sus conclusiones al validar más combinaciones durante la semana. Si un PoC falla en tu entorno, ese resultado no confirma que tengas el parche. [13]
Entre los repositorios públicos está el de eness, incluido en las referencias como material de terceros. Este artículo no incluye una validación propia de ese PoC. Las versiones corregidas se contrastaron con los avisos de PHP y el seguimiento de NCSC-NL. [14] [9]
Qué puedes aplicar para prevenir errores parecidos
Si desarrollas componentes nativos, revisa cuándo una estructura se considera válida y quién es dueño de cada reserva de memoria. Prueba las rutas de error, además de las operaciones exitosas. El análisis estático puede detectar ciertos usos de variables sin inicializar; el fuzzing permite explorar entradas y estados inesperados. Son herramientas para descubrir defectos, no una garantía de que no queden otros. [16]
En una aplicación web, esa lección se traduce en mantener también el intérprete y sus extensiones dentro del proceso de actualizaciones. Documenta dónde corre PHP y cómo verificas que un despliegue sustituyó todos sus procesos. Una lista de dependencias de la aplicación no cubre por sí sola ese inventario. [9]