PHP2Shell: qué hacer si tu web usa PHP o WordPress
PHP2Shell: qué versiones de PHP están corregidas, cómo saber cuál sirve tu web, si estuviste expuesto y qué revisar y rotar después.

El 21 de octubre de 2026 el proyecto PHP publicó dos avisos de seguridad críticos, CVE-2026-109618 y CVE-2026-109619, a los que se conoce como PHP2Shell por la publicación de Wiz Research, uno de los equipos que los descubrieron. Los dos permiten ejecutar código en el servidor sin usuario ni contraseña, con la cuenta con la que corre la web, y los dos tienen una nota CVSS de 9,2 sobre 10 (aviso de CVE-2026-109618, aviso de CVE-2026-109619).
Lo que los hace distintos de los avisos de cada semana es dónde están: en PHP, no en WordPress, ni en un plugin, ni en tu código. En el caso confirmado con Apache y mod_php, el fallo salta mientras PHP prepara la petición, «antes de que empiece a ejecutarse ningún código de la aplicación», según el aviso de CVE-2026-109618. Da igual que uses WordPress, Drupal, Laravel o un PHP escrito a mano.
En jarroba.com nos llegó la alerta el 23 de octubre. Lo primero fue situar el fallo: una petición a una web pasa por varias capas antes de llegar a los datos, y PHP2Shell está en la de PHP, justo antes de WordPress. Por eso nada de lo que pongas en WordPress lo frena.
Qué versiones están corregidas
| Rama de PHP | Versión con el arreglo |
|---|---|
| 8.5 | 8.5.12 |
| 8.4 | 8.4.27 |
| 8.3 | 8.3.36 |
| 8.2 | 8.2.35 |
| 8.1 y anteriores | ninguna oficial (ver debajo) |
Las versiones salen de los dos avisos, y el proyecto PHP publicó las cuatro el 21 de octubre, con un aviso propio que resume el problema y las medidas.
Las ramas 8.1 y anteriores ya no tienen soporte, y no habrá versión nueva para ellas. Por la gravedad del fallo, el equipo de seguridad de PHP sí publicó el parche en código fuente para 7.4, 8.0 y 8.1, pero solo sirve a quien compila PHP. Si tu PHP viene de una distribución de Linux, puede que ya esté corregido (lo explica la sección siguiente); si no, toca cambiar de rama.
Cuidado
Los dos avisos coinciden: actualizar es la única protección completa. Las medidas de configuración que proponen reducen el riesgo, pero no lo eliminan.
La versión que de verdad sirve tu web
Parece fácil: miras la versión de PHP y la comparas con la tabla. En la práctica hay tres trampas.
La cabecera no siempre lo dice todo. PHP anuncia la versión completa en la cabecera X-Powered-By (SAPI_PHP_VERSION_HEADER en su código), pero hay alojamientos que la recortan o la quitan. La nuestra decía X-Powered-By: PHP/8.3, y con eso no se sabe si es la 8.3.31 o la 8.3.36.
La consola puede ser otra. En un alojamiento compartido, el php -v que ves por SSH no tiene por qué ser el que ejecuta la web: en muchos paneles cada sitio elige su versión, y la consola puede usar otra.
El número de una distribución no es el de PHP. Algunas distribuciones de Linux corrigen sus paquetes sin cambiar de versión. En Ubuntu 24.04, php -v sigue diciendo 8.3.6, una versión que en la tabla sale vulnerable, y el paquete está corregido desde 8.3.6-0ubuntu0.24.04.13; en Ubuntu 22.04, el 8.1 lo está desde 8.1.2-1ubuntu2.28 (seguimiento de Ubuntu). Si tu PHP viene de una distribución, lo que manda es la versión del paquete y el aviso de seguridad de esa distribución, no la tabla de arriba.
Lo más fiable, en orden:
- En WordPress, Herramientas → Salud del sitio → Información → Servidor: es la versión con la que corre la web.
- En el panel del alojamiento, la versión configurada para ese sitio.
- Por SSH, la versión y la fecha de compilación (la primera línea de la salida):
$ php -v
PHP 8.3.36 (cli) (built: Oct 21 2026 14:05:40) (NTS)
- Si usas Docker, la del contenedor que está sirviendo la web, no la de tu máquina ni la de la imagen que tengas descargada (
docker exec):
$ docker exec <contenedor> php -v
Una etiqueta como php:8.3 o latest no es una versión: hay que volver a descargar la imagen y reconstruir.
¿Estuve expuesto antes del parche?
Saber que ahora estás bien no responde a la pregunta que importa: qué versión tenía tu web los días anteriores. El proveedor no te lo dice, y los registros de acceso no guardan la versión de PHP.
En WordPress hay una forma de saberlo sin tocar nada. Cada vez que alguien abre el escritorio (o Salud del sitio), WordPress comprueba si su versión de PHP está al día y guarda la respuesta una semana, en una opción cuyo nombre lleva el MD5 de la versión (wp_check_php_version(), en wp-admin/includes/misc.php). Si la versión cambió en esa semana, quedan las dos.
$ wp transient list --network --search='php_check_*' --fields=name,expiration
$ php -r 'foreach (["8.3.31", "8.3.36"] as $v) echo $v, " ", md5($v), PHP_EOL;'
El primer comando lista las comprobaciones guardadas y su caducidad; el segundo calcula el MD5 de las versiones que sospeches, para reconocerlas. La caducidad sale en segundos desde 1970 (date -u -d @<número> la traduce a UTC), y restándole siete días se sabe cuándo se hizo cada comprobación. Así supimos lo nuestro: una comprobación con 8.3.31 del 17 de octubre y otra con 8.3.36 del 21 a las 16:40 UTC. Nuestro proveedor actualizó PHP por su cuenta el mismo día del parche; los días de antes, la web corrió con una versión vulnerable.
Solo funciona si alguien abrió el escritorio o Salud del sitio esos días, y solo informa de esas fechas. Tampoco sirve si la web usa una caché de objetos persistente (Redis o Memcached): con ella, la comprobación no se guarda en la base de datos, y wp-cli avisa de que solo lista lo que hay ahí. Y hay que mirarlo pronto: cada comprobación caduca a la semana, y WordPress borra las caducadas una vez al día. Cuando volvimos a mirarlo el 26, la de 8.3.31 ya no estaba. Aun así, fue el único registro de la versión de esos días que encontramos.
Qué hacer ya
- Alojamiento compartido. El proveedor es quien actualiza PHP. Comprueba la versión como arriba y, si no está corregida, pide el cambio por escrito y vuelve a mirarla después. Si estás en 8.1 o anterior, sube de rama: nosotros lo hicimos desde el panel, y WordPress, los plugins y nuestro código siguieron funcionando sin avisos (el único salió en wp-cli, la herramienta de consola).
- Servidor propio. Actualiza PHP y reinicia PHP-FPM o Apache: los procesos que ya estaban arrancados siguen con el código viejo hasta que se reinician.
- Docker. Reconstruye la imagen con
docker build --pull, que vuelve a descargar la base («Always attempt to pull all referenced images»), y vuelve a desplegar. - Si no puedes actualizar ya, los avisos proponen medidas que reducen el riesgo sin eliminarlo:
session.upload_progress.enabled=0, que corta uno de los dos casos confirmados de CVE-2026-109619. El aviso recomienda dejarlo desactivado incluso después de parchear si no usas esa función, porque es poco usada y poco probada, y es donde más gente va a buscar fallos nuevos. El núcleo de WordPress no la usa;- en PHP 8.5,
fatal_error_backtraces=0, que corta el otro caso confirmado; memory_limit=-1impuesto por el administrador, contra CVE-2026-109618, y nunca conRLIMIT_ASniRLIMIT_DATA, que según el aviso provocan justo el fallo que se quiere evitar.
Las tres se ponen en la configuración que se carga al arrancar (php.ini, el pool o grupo de procesos de FPM, o la del servidor web), y después hay que reiniciar todos los procesos. No sirven ini_set() ni .user.ini: según el aviso de php.net, se aplican después de que PHP procese la petición, y el fallo salta antes.
Lo que no te protege
Los plugins que limitan intentos de login, el doble factor, los captchas y actualizar WordPress son buenas medidas, pero aquí no sirven: el ataque no pasa por el login, y ocurre antes de que WordPress arranque. Lo decía el propio correo que recibimos, de un fabricante de plugins de login, y se agradece la sinceridad.
Tampoco te avisará un escáner que solo mire ficheros si lo que se llevaron fue memoria (se explica más abajo).
¿Me han entrado? Lo que revisamos
Lo hicimos por SSH con wp-cli; casi todo se puede hacer también con el gestor de ficheros del panel, con más paciencia. La idea es buscar lo que deja quien consigue ejecutar código: ficheros PHP nuevos, cuentas nuevas y tareas que se repiten.
- WordPress y los plugins, contra los originales:
$ wp core verify-checksums --include-root 2>&1 | grep -E 'should not exist: .*(\.ph|\.user\.ini)|Success|Error'
$ wp plugin verify-checksums --all
Lee los avisos, no solo la última línea: un fichero que sobra solo da un Warning: File should not exist, y la orden acaba en Success igualmente. Con --include-root también mira la carpeta raíz de la web, y avisa de todo lo que no es WordPress; el grep deja solo los ficheros PHP, los .user.ini y el resultado.
No cubren los ficheros de traducción .l10n.php, que también son PHP, ni un tema propio, ni los plugins que no vienen de wordpress.org (los de pago o los propios): esos se saltan con un aviso, y el resultado sigue siendo Success. Compara el tema y esos plugins con tu repositorio si lo tienes.
- PHP nuevo o cambiado desde antes de la ventana, en todo tu espacio, no solo en la carpeta de la web:
$ cd ~ && find . -newerct 2026-10-15 -type f \
\( -name '*.php*' -o -name '*.phtml' -o -name '*.phar' -o -name '.user.ini' \) \
-not -path '*/wp-admin/*' -not -path '*/wp-includes/*' -not -path '*/wp-content/cache/*'
-newerct mira cuándo cambió el fichero en el disco, una fecha que, según el manual de touch, no se puede poner a mano, al contrario que la de modificación. wp-admin y wp-includes ya los comprobó el paso 1. Y se empieza con cd ~ porque en algunos alojamientos la carpeta personal es un enlace, y find ~ no entra en ella: no encuentra nada y no da ningún error. Nos pasó al comprobar las órdenes de este artículo.
Saldrán muchas cosas legítimas (actualizaciones, copias restauradas, cambios de permisos): a nosotros, miles. Para verlas por carpetas, añade al final | cut -d/ -f1-4 | sort | uniq -c | sort -rn, y mira las que no sepas explicar.
- Escondites habituales: PHP dentro de
wp-content/uploads, la carpetawp-content/mu-plugins(lo que hay ahí se carga siempre, no se puede desactivar desde el escritorio y sale aparte de la lista normal de plugins, según la documentación de WordPress),.htaccesscambiados,.user.ininuevos (conauto_prepend_filecargan un PHP en cada petición sin tocar WordPress, en servidores con FPM o CGI) y, si tienes SSH,~/.ssh/authorized_keys: una clave que no reconoces es una puerta trasera. - Cuentas y accesos:
$ wp user list --role=administrator --fields=ID,user_login,user_registered
$ wp user list --orderby=registered --order=DESC --number=20 --fields=ID,user_login,user_registered
$ wp user application-password list <usuario> --fields=name,created,last_used,last_ip
$ wp cron event list --fields=hook,next_run_relative
Administradores que no conoces, altas recientes (la segunda orden pone primero las últimas), contraseñas de aplicación que nadie creó (la tercera, para cada administrador; también están en su perfil) y tareas programadas con nombres raros. created y last_used salen en segundos desde 1970, como la caducidad de antes.
- Los registros de errores. El aviso de CVE-2026-109619 pide buscar errores fatales, avisos repetidos antes de que se ejecute el script y
Maximum execution time exceeded. Pero también avisa de que no encontrar nada en los registros no demuestra que estés a salvo. En muchos alojamientos compartidos los registros de errores no están a mano: hay que activar su descarga en el panel. Hazlo antes de necesitarlos.
En nuestro caso no apareció nada: los ficheros coincidían con los originales y con nuestro repositorio, y no había cuentas nuevas ni tareas raras.
Por qué rotar los secretos aunque no encuentres nada
El aviso de CVE-2026-109619 incluye entre los efectos confirmados la filtración de memoria: lo que pasó por el servidor en otras peticiones. En un WordPress eso incluye la contraseña de la base de datos y las claves con las que se firman las cookies (se cargan en cada petición), las claves de servicios externos y las cookies de sesión de quien estuviera conectado. Leer memoria no deja ficheros ni cuentas: la revisión de arriba no lo detecta.
Si estuviste expuesto, cambia:
- la contraseña de la base de datos (en el panel del alojamiento y en
wp-config.php, justo después); - las claves de servicios externos que estén en
wp-config.php(captcha, correo, APIs); - las contraseñas de aplicación que usen tus integraciones;
- las sales de WordPress, lo último, porque cierran todas las sesiones y tus usuarios tendrán que volver a entrar:
$ wp config shuffle-salts
Antes de cada cambio, copia wp-config.php fuera de la carpeta de la web (por ejemplo, a tu carpeta personal) y borra la copia al terminar. Un error en ese fichero deja la web en blanco, y una copia con otro nombre dentro de la web (wp-config.php.copia) no la ejecuta PHP, así que el servidor puede entregarla como texto, con la contraseña de la base de datos que estás cambiando.
Si te han entrado
La guía de WordPress para webs atacadas tiene como primer paso algo que parece obvio y no lo es: apuntarlo todo. Qué ves, desde cuándo, qué cambiaste esos días. Después, según esa guía:
- pregunta a tu proveedor: en un alojamiento compartido el ataque puede afectar a más que tu web;
- revisa si los buscadores o los antivirus han marcado tu web, en Google Search Console y Bing Webmaster;
- cambia todos los accesos, de todas las personas: SFTP o FTP, el panel de WordPress, el del alojamiento y la base de datos;
- y analiza también tu propio ordenador: a veces el robo de contraseñas empieza ahí.
Por experiencia, añadiríamos una cosa más: restaurar desde una copia anterior a la ventana de exposición, no desde la de anoche, que puede llevar dentro lo que dejó el atacante. Para eso hacen falta copias de varios días, guardadas fuera del servidor, y haber probado alguna vez a restaurar una. Nosotros lo hicimos en un entorno aparte: la base de datos y los ficheros levantaron la web en menos de un minuto, con los mismos artículos, usuarios y comentarios que la real. Lo que de verdad lleva tiempo es rehacer wp-config.php si no está en la copia (hay quien lo deja fuera a propósito, porque guarda las contraseñas): conviene tener apuntado qué lleva y de dónde sale cada valor.
Lo que aprendimos
PHP2Shell nos encontró con lo básico hecho, y eso hizo que revisarlo fuera cuestión de una mañana. También nos enseñó lo que faltaba: tener los registros de errores a mano antes de necesitarlos, y una vigilancia diaria que avise de los cambios sospechosos, en lugar de mirar solo cuando sale una noticia.
Y una lección sobre el alojamiento compartido. Que el proveedor actualice PHP solo es una ventaja enorme: el nuestro lo hizo el mismo día. Pero hay que comprobarlo, no suponerlo, y hasta ese momento la web estaba expuesta.
Si quieres más
- Los dos avisos del proyecto PHP, con el detalle técnico y las medidas: CVE-2026-109618 y CVE-2026-109619.
- La guía de WordPress para webs atacadas y la de endurecimiento.
«PHP2Shell: qué hacer si tu web usa PHP o WordPress», de Ramón Invarato en jarroba.com, con licencia Creative Commons Reconocimiento-NoComercial-CompartirIgual 3.0. Obra registrada en Safe Creative.

Te puede interesar
LAMP paso a paso en tu servidor
Aprender paso a paso a crear una web dinámica con la arquitectura LAMP completa: Linux, Apache, MySQL o MariaDB, PHP, Perl o Python.
Usar Servidores y crear una web
Instala Apache en un servidor Ubuntu, abre los puertos del firewall y publica una web con HTML, CSS y JavaScript usando Vim, un IDE y FileZilla por SFTP.




Deja un comentario