PHP2Shell: las RCE de PHP que explotaron miles de PoC falsos
El martes 20 de octubre, un martes cualquiera, los identificadores de dos vulnerabilidades nuevas aparecieron en foros y redes sociales. Sin detalles técnicos, pero con una hora de release equivocada y un CVSS de 10.0 que después resultó ser 9.2. En cuestión de horas, GitHub se llenó de repositorios llamados “PoC” para lo que se convirtió en PHP2Shell: CVE-2026-109618 y CVE-2026-109619, dos Remote Code Execution sin autenticación en el intérprete de PHP, con CVSS v4.0 de 9.2 según el equipo de PHP.
Eso es precisamente lo peligroso. Lo interesante es lo que pasó después. Mientras medio mundo del software revisaba el diff, miles de “pruebas de concepto” aparecieron — y la mayoría no funcionaba. No funcionaban contra un PHP limpio: funcionaban solo si antes modificabas el servidor para dejarlo vulnerable de otra forma. Ese detalle lo cambia todo, porque es la diferencia entre una RCE real y un spectacle de repositorios con muchas estrellas.
Este post es doble: primero qué es PHP2Shell y cómo cierras el ciclo de patching en tus servidores; segundo —y es la parte que me importa más— por qué el ecosistema de exploits públicos se llenó de basura en cuestión de horas, y cómo no te engañas tú tampoco cuando un LLM te “confirme” que tienes un exploit entre manos.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 15-20 minutos para inventario + update + verificación en un servidor con PHP
- Requisitos: acceso root o sudo al host, y saber qué SAPI usas (mod_php, PHP-FPM, php-cgi, LiteSpeed o FrankenPHP)
- Resultado: PHP en 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12, workers reiniciados, y un criterio para distinguir un exploit real de uno inventado
- Troubleshooting: si usas PHP 7.4, 8.0 u 8.1 no hay binario oficial; si usas 7.3 o anterior (incluido PHP 5) no hay fix y el punto no es negociable
Qué es PHP2Shell y por qué importa
PHP2Shell es el nombre que se le dio al par de vulnerabilidades CVE-2026-109618 y CVE-2026-109619, publicadas el 20 de octubre de 2026 y corregidas el 21. Ambas permiten ejecución remota de código sin autenticación y ambas están calificadas con 9.2 en CVSS v4.0 por los mantenedores de PHP.
Lo que las hace especiales no es la puntuación: es dónde viven. Las dos están en las piezas que toca prácticamente cada request — gestión de memoria y manejo de errores. Eso es exactamente el tipo de bug que no se puede parchear sin arriesgar la compatibilidad, hurtando el rendimiento, o forzando cambios en el ecosistema de extensiones de terceros que nadie quiere coordinar.
Y ese detalle técnico explica por qué la historia previa importa tanto. Durante la auditoría externa de PHP de 2024 (financiada por la Sovereign Tech Agency, ejecutada por Quarkslab), un desarrollador del core encontró los primeros indicios del problema de out-of-memory detrás de CVE-2026-109618. En 2024 ya se teorizó un posible exploit, pero no se encontraron vectores concretos. El equipo de seguridad desarrolló varios enfoques en competencia — cada uno con sus trade-offs: romper compatibilidad, abrir nuevas Denial of Service, o obligar a cambiar muchas extensiones. El proceso de decisión se estancó y se reinició durante meses. La PHP Foundation lo reconoce textualmente en su comunicado: fue un fallo estructural.
Los dos bugs, por separado
CVE-2026-109618 (GHSA-w8hw-w9fx-j79w) es corrupción por out-of-memory. Si un request llega al límite de memory_limit en el momento exacto equivocado, PHP puede dejar datos internos parcialmente inicializados que después el código de limpieza trata como válidos. Sobre Apache con mod_php, un request sin autenticar puede disparar esto antes de que corra una sola línea del código de tu aplicación, y se demostró ejecución remota completa con la configuración por defecto. También se confirmó RCE vía PHP-FPM a través de rutas de código a nivel de aplicación.
CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp) es estado global obsoleto. Un error fatal en el punto equivocado puede dejar el estado interno apuntando a memoria que pertenecía a un request anterior. El siguiente request que tome ese worker de PHP puede actuar sobre esos datos liberados. Se confirmó un use-after-free con crash del worker, y quien reportó el issue demostró RCE contra una aplicación real corriendo en Apache con mod_php.
Nótese la diferencia: el primero necesita timing preciso y límite de memoria; el segundo necesita que el worker sobreviva al request anterior. Los dos dependen de condiciones específicas, y ahí está el nudo de por qué la mayoría de los “exploits” circulados no funcionaron.
Inventariar: qué versión de PHP corre dónde
Antes de actualizar nada, necesitas saber qué tienes. Este es el error más común en estos días: actualizar el servidor principal y olvidar el contenedor, el panel de hosting, o el PHP embebido en otro servicio.
php -v
php -i | grep -E "^(Server API|Loaded Configuration File)"
El segundo comando es el que importa: te dice si estás en fpm-fcgi, apache2handler, cgi-fcgi o litespeed. El riesgo es mayor sobre mod_php, donde el ataque puede tener éxito antes de que corra tu framework. Si tienes varios hosts, automatiza el inventario en lugar de SSH uno por uno:
for host in web01 web02 panel01; do
echo "=== $host ==="
ssh "$host" 'php -v'
ssh "$host" 'php -r "echo php_sapi_name(), PHP_EOL;"'
done
En contenedores, php -v dentro del host no dice nada sobre la imagen que corre tu app. Toca revisar los Dockerfiles y los-compose files por el tag de PHP.
grep -rn "php:[0-9]" --include="Dockerfile*" --include="*.yml" --include="*.yaml" .
Si eso te devuelve un php:7.4 o un php:5.6, ya sabes que la conversación va a ser más larga que un simple update.
Actualizar a la versión corregida
Estas son las versiones que corrigen ambos CVEs. Nota que el salto no es uniforme — cada rama tiene su propio número:
| Rama | Vulnerable | Corregida |
|---|---|---|
| PHP 8.5.x | menor a 8.5.12 | 8.5.12 |
| PHP 8.4.x | menor a 8.4.27 | 8.4.27 |
| PHP 8.3.x | menor a 8.3.36 | 8.3.36 |
| PHP 8.2.x | menor a 8.2.35 | 8.2.35 |
| PHP 8.1, 8.0, 7.4 | todas | sin binario oficial — EOL |
| PHP 7.3 y anteriores (incluido 5) | todas | sin fix — hay que migrar |
La actualización en Debian/Ubuntu normalmente llega por el paquete de la distribución, que recibió el patch antes del release público:
sudo apt update
sudo apt list --upgradable 2>/dev/null | grep -i php
sudo apt install --only-upgrade php8.3-cli php8.3-fpm
En CentOS/RHEL el camino es dnf con los repos de PHP, aunque los paquetes de la distribución pueden tener algún retraso:
dnf list --upgrades 'php*'
sudo dnf upgrade php php-cli php-fpm
php -v | head -1
Después del update hay un paso que mucha gente salta y que es _el_ que importa: reiniciar todos los workers. Un PHP-FPM con procesos viejos en memoria sigue corriendo el código viejo aunque el binario en disco ya esté parcheado.
sudo systemctl restart php8.3-fpm
sudo systemctl restart apache2
ps aux | grep -c "[p]hp-fpm" # confirma que hay workers nuevos
Si usas contenedor, el reinicio no basta — hay que reconstruir la imagen, porque el código viejo vive en la capa.
docker compose build --no-cache app
docker compose up -d --force-recreate app
docker compose exec app php -v
Por qué casi todos los PoC públicos eran falsos
Esta es la parte que vale la pena del episode. Una observación de Elias Taylor publicada el 21 de octubre: si revisas GitHub, hay miles — literalmente miles — de “PoC” para PHP2Shell. Una cosa tienen en común casi todos: no funcionan contra una instancia de PHP vanilla.
Levanta un servidor PHP en Docker que tú creas vulnerable, y corre el PoC tú mismo. No le pidas a tu LLM que lo corra — córralo tú. Los exploits no funcionan.
El patrón más común: la mayoría de estos PoC dependen de LFI (Local File Inclusion) para completar la cadena. En no-PHP: el exploit requiere que el servidor ya tenga una vulnerabilidad grave a nivel de código, una donde el usuario pueda leer /etc/passwd. Que una app PHP esté comprometida de alguna forma no es raro, pero exigir eso como precondición para un “exploit universal contra cualquier PHP” es absurdo. Si necesitas eso, no es un bug severo — es un generador de tickets.
El troubleshooting honesto: levanta tu propio laboratorio
Si realmente quieres verificar un PoC, la forma seria es levantar un target vulnerable tú, de forma aislada:
docker run --rm -d --name php-vuln -p 8080:80 php:8.2-apache
sleep 3
curl -sI http://localhost:8080/ | head -1
Nunca corras un PoC de terceros contra tu servidor de producción — y menos si viene de un LLM que “lo validó” por ti. Aíslalo, restringe el puerto a localhost o a una red de pruebas, y lee el código antes de ejecutarlo. Un exploit es código arbitrario: ejecutarlo es confiarle tu servidor al atacante.
Lo que la historia dice sobre LLM y seguridad
Lo que el caso revela —y es la parte que me parece más importante— no es PHP. Es lo que los fracasos generalizados de los PoCs dicen sobre cómo usamos los modelos hoy.
La suposición mayoritaria era que los LLMs iban a igualar el terreno: leer el diff, construir la cadena de exploit al instante, y convertir algo “más difícil que Heartbleed” en algo trivial. La realidad fue otra. La mayoría de las cadenas dependían de una precondición ridícula, o —peor— el propio PoC modificaba el servidor contra el que se probaba para hacerlo explotable. Eso es hacer trampa, y es exactamente el error de juicio que se reparte de forma masiva cuando miles de personas le ceden la decisión a un modelo sin evaluarla.
Y esto tiene nombre en el análisis de decisiones: Decision Schelling Points. Cuando un método de resolver un problema está sobrerrepresentado no porque sea correcto, sino porque es lo que el modelo hace por defecto, toda la población se mueve en la misma dirección sin haber evaluado nada.
La lección operativa para un sysadmin es simple y no cambia con el CVE que toque:
- El parche no espera. PHP dice que ningún cambio de configuración mitiga completamente estas vulnerabilidades. Actualizar es la única opción segura. Si no puedes actualizar hoy, restringe el acceso a internet del sistema PHP.
- Un PoC que necesita que configures tu servidor para funcionar no es un PoC. Si el repo de exploit incluye un
setup.shque mete una vulnerabilidad en tu target, no estás probando el CVE — estás fabricando tu propio incidente. - Verifica contra fuentes primarias. El advisory oficial de PHP y los GHSA son la referencia; no el blog de un investigador que se apoya en lo que otros afirman, ni un LLM que te dice “sí, es explotable” sin mostrar el output real.
- Evalúa el riesgo por lo que PHP dice, no por el score filtrado. El CVE se filtró con un 10.0 que era 9.2. El número no era el problema; el ruido alrededor sí.
Detalles de la línea de tiempo que vale la pena saber
El disclosure coordinado se rompió dos días antes de la fecha objetivo, y eso explica la tempestad de PoCs. El 20 de octubre ~15:00 UTC los identificadores CVE empezaron a circular con hora equivocada y CVSS 10.0. PHP adelantó su pre-anuncio a las 19:30 UTC y movió el release del 22 de octubre 14:00 UTC al 21 de octubre 14:00 UTC. A las 07:41 UTC del 21 —más de seis horas antes del release coordinado— Chainguard rompió el embargo unilateralmente y publicó imágenes construidas con el fix, permitiendo que cualquier atacante estudiara el diff en forma binaria mientras el resto del mundo PHP seguía esperando.
Hubo intentos de explotación antes del disclosure público: AWS Threat Intelligence identificó un intento del 17 de octubre en telemetría MadPot, cuatro días antes; Cloudflare reportó signos tempranos y desplegó reglas WAF temporales como mitigación de stopgap. watchTowr observó explotación en la wild el 21 vía su honeypot Attacker Eye. CISA, en cambio, no había observado explotación al cierre de ese mismo día y no listó ningún CVE en su catálogo KEV.
En resumen: el equipo de seguridad de PHP revisó los PoC que aparecieron tras la filtración y no identificó ninguno funcional al momento del release. El ruido era real; la amenaza inmediata, no.
Resumen
PHP2Shell es serio y hay que parchear ya — no por el score 9.2, sino porque vive en el core de gestión de memoria que toca cada request. Pero es más difícil de explotar de lo que el ruido indica, y la razón por la que el ruido señalaba algo equivocado es que la mayoría de los PoC públicos eran falsos: requerían que tú mismo dejaras el servidor vulnerable de otra forma primero.
La segunda lección es más duradera que el CVE: la próxima vez que un repo de exploit te parezca convincente, verifica el pre-requisito. Levanta un target limpio, corre el script tú mismo, y mira qué toca antes de dar por buena la cadena. Y cuando la actualización esté hecha, el restart de los workers no es opcional.
Preguntas frecuentes
¿Entonces está o no explotado en la wild?
Las dos cosas. watchTowr observó explotación en la wild el 21 de octubre vía su red de honeypots, y AWS vio un intento el 17. Pero CISA no observó explotación al cierre del 21 y ningún CVE entró a KEV. El equipo de seguridad de PHP revisó los PoC circulados y no halló ninguno funcional al momento del release.
¿Sirve una configuración para evitarlo si no puedo actualizar?
No como solución. PHP es explícito: ningún cambio de configuración mitiga por completo estas vulnerabilidades, y actualizar es la única opción segura. Lo único aceptable a corto plazo es restringir el acceso a internet al sistema PHP hasta que puedas actualizar.
¿Qué hago si corro PHP 7.4, 8.0 u 8.1?
Esas ramas están EOL. Hay backports mínimos de código fuente para 7.4, 8.0 y 8.1, pero sin binarios oficiales. Tus opciones reales son el paquete de tu distribución o el de un proveedor de LTS, o migrar a una rama soportada (8.2+). PHP 7.3 y anteriores no tienen fix alguno.
¿Cómo sé si un PoC de GitHub es real o falso?
Mira el pre-requisito. Si el repo incluye un setup que configura LFI o mete una vulnerabilidad en el target, no estás probando el CVE: estás fabricando tu propio incidente. La prueba honesta es levantar un PHP vanilla en Docker y correr el script tú mismo — si necesita cambios previos, no es un exploit universal.
¿Actualizar el paquete es suficiente o necesito reiniciar algo más?
Necesitas reiniciar todos los workers, no solo instalar el paquete. En PHP-FPM y Apache, los procesos antiguos siguen en memoria con el código viejo aunque el binario esté parcheado. En contenedores hay que además reconstruir la imagen sin caché, porque el código viejo vive en una capa.
Lee también
- Despliegue Seguro y Rápido de Parches: Desafíos y Soluciones para Sysadmins Linux
- BlockStrap (CVE-2026-103849): RCE CVSS 10.0 en Magento y Adobe Commerce, y cómo auditar tu tienda
- CVE-2026-105445 en Docker Hub: el bug GhostPAT que reasignaba tokens personales
- Guía Definitiva para Sysadmins: Configuración de AppArmor para Proteger tus Aplicaciones en Linux
