Público: personas que gestionan sitios PHP y están aplicando una actualización urgente de PHP, o acaban de hacerlo y se preocupan por posibles brechas.
En octubre de 2026 se publicaron versiones corregidas para dos vulnerabilidades críticas de PHP (CVE-2026-109618 y CVE-2026-109619). Un sitio que gestiona el operador de este sitio se actualizó esa misma noche. Esta página convierte lo ocurrido allí, dos suposiciones iniciales que resultaron erróneas y la revisión de intrusión aplicada después a todos los servidores, en pasos generales. Las vulnerabilidades y las versiones corregidas se explican en Dos vulnerabilidades críticas de PHP y las versiones de emergencia corregidas (octubre de 2026).
Qué ocurrió (en un sitio que gestiona el operador de este sitio)
El sitio usa PHP en más de un entorno. Como el equipo de PHP había anunciado cuándo se publicarían las versiones corregidas, el operador activó una página de mantenimiento antes de la publicación y permaneció fuera de línea hasta que la versión corregida estuvo disponible. Después la instaló y retiró la página de mantenimiento.
Tras el anuncio
Activar la página de mantenimiento y esperar la versión corregidaTras la publicación
Instalar la versión corregida y retirar la página de mantenimientoComprobación posterior a la actualización
El lado web se actualizó, pero el PHP de línea de comandos que usaba cron seguía apuntando a una versión antigua, así que se reapuntóEsa misma noche
Un script temporal que desactivaba el modo de mantenimiento se detuvo antes de escribir nada. Causa: set -e combinado con diff. No hubo daños y el script se reconstruyóDespués
La misma lista de revisión de intrusión se aplicó a todos los servidores, y una petición sospechosa que había llegado antes de la actualización se comprobó comparando archivos
La CISA de EE. UU. añadió después ambos CVE a su catálogo de vulnerabilidades explotadas conocidas (KEV) (anuncio de CISA). Eso respalda la decisión de parchear con urgencia, pero un parche solo cierra la puerta. Comprobar que nadie había entrado ya es una tarea aparte.
Suposición 1: «El PHP web está actualizado, así que hemos terminado»
La primera suposición era que, si el sitio funciona con la nueva versión, todo el PHP de ese servidor está actualizado. En realidad, el PHP de línea de comandos que llama cron seguía apuntando a una versión antigua.
PHP puede ser un programa distinto según la forma en que se ejecuta (el SAPI, Server API). php.net indica que el SAPI de CLI difiere de los demás SAPI en muchos aspectos (PHP: Uso de PHP desde la línea de comandos). Con varias versiones instaladas en una misma máquina, la web puede ejecutar una versión nueva mientras la línea de comandos ejecuta una antigua.
Peticiones web → PHP-FPM / módulo de Apache
Ejecuta la versión actualizada (visible en Salud del sitio y pantallas similares)
cron y scripts de shell → PHP de línea de comandos
Sigue apuntando a la versión antigua, que una comprobación desde la web no puede mostrar
Compruebe web y CLI por separado
Compruebe la versión de PHP de línea de comandos
php -v muestra la versión del PHP que llama esta shell. php.net indica que php -v también le dice si PHP es una compilación CGI o CLI. which php muestra la ubicación del ejecutable que realmente se llama.
En sistemas tipo Debian con varias versiones, compruebe a dónde apunta el comando
update-alternatives --display php muestra a qué versión apunta el comando php (manual de update-alternatives). --config lo cambia, pero eso es un cambio de configuración del servidor, así que confirme antes el procedimiento y cómo revertirlo.
Escriba la ruta completa a PHP en las tareas de cron
cron ejecuta las tareas en su propio entorno, separado de una shell de inicio de sesión (crontab(5)). Si escribe solo php, qué PHP se elige depende de ese entorno. Consejo de este sitio: escriba la ruta completa a la versión que parchó, para que la comprobación posterior a la actualización tenga una única respuesta.
Compruebe la versión de PHP web desde el lado web
En WordPress, Salud del sitio en la pantalla de administración la muestra. En código, están disponibles las constantes PHP_VERSION (versión) y PHP_SAPI (cómo se ejecuta PHP: cli para CLI, fpm-fcgi para PHP-FPM, apache2handler para el módulo de Apache) (php_sapi_name).
Borre cualquier página phpinfo() justo después de comprobarla
Un archivo público que llama a phpinfo() muestra la versión, pero también expone la configuración y detalles internos del servidor. Bórrelo en el momento en que termine. No dejarlo publicado es la condición para usar este método.
Suposición 2: «diff o bien tiene éxito o bien falla»
El script temporal que desactivaba el modo de mantenimiento se escribió para comparar el contenido actual con el nuevo mediante diff antes de escribir nada. Tenía activado set -e.
diff devuelve el estado de salida 1 cuando encuentra diferencias. En el manual de GNU, 0 significa sin diferencias, 1 significa que se encontraron diferencias y 2 significa problema (GNU diffutils: diff). Encontrar diferencias era justo lo que el script esperaba. Pero set -e (errexit) detiene la shell ante un estado distinto de cero, salvo en lugares como las condiciones. El script se detuvo porque había diferencias y nunca llegó a la escritura.
Así que el paso que desactivaba el modo de mantenimiento se detuvo sin escribir nada, y no hubo daños. Falló en el lado seguro, pero la combinación contraria, no detenerse donde debía, sería peligrosa. El script se reconstruyó.
Comandos donde el estado de salida 1 no es un «error»
| Comando | Significado de 1 | Significado de 2 | Fuente |
|---|---|---|---|
diff | Diferencias encontradas | Problema | GNU diffutils: diff |
cmp | Diferencias encontradas | Problema | GNU diffutils: cmp |
grep | No se seleccionó ninguna línea (sin coincidencia) | Se produjo un error | GNU grep |
Patrones seguros
El manual de GNU Bash indica que, con set -e, la shell no termina cuando un comando devuelve un valor distinto de cero como parte de una prueba if, en una lista && o || distinta del último comando, o cuando su estado se invierte con ! (GNU Bash: The Set Builtin). Úselo.
Se detiene, o oculta un fallo real
- Escribir un
diff a bsin más bajoset -e(se detiene solo porque hay diferencias) diff a b || true(traga tanto las «diferencias» como un error real (2))- Usar
|| trueengreppara que «sin coincidencia» y «no se pudo leer el archivo» parezcan lo mismo
Gestione los códigos de salida por separado
- Guarde el estado de salida en una variable y ramifique según 0, 1 y 2 (el patrón recomendado)
- Si solo necesita saber si hay diferencias,
if diff -q a b; then …funciona, pero tenga en cuenta que 2 (un error) cae en el mismo lado que «diferencias» - Si usa
|| true, decida primero si es aceptable no detectar un 2 (un error real)
Aquí hay un ejemplo que separa 0, 1 y 2. Un fallo en el lado izquierdo de || está exento de errexit, así que el script no se detiene y rc guarda el estado de salida.
#!/usr/bin/env bash
set -euo pipefail
rc=0
diff -rq deployed/ live/ || rc=$?
case "$rc" in
0) echo "no differences" ;;
1) echo "differences: review before continuing" ;;
*) echo "diff failed" >&2; exit 2 ;;
esacset -e no es una red de seguridad completa. Tiene muchas excepciones que pueden ocultar errores, así que para ramas importantes es más sencillo leer usted mismo el estado de salida y decidir (consejo de este sitio).
Tras el parche: ejecute las mismas comprobaciones en todos los servidores
El parche cierra la vía de entrada. No le dice si alguien entró antes de la actualización. Para las vulnerabilidades de KEV, la CISA pide comprobar si hubo intrusión antes de actualizar (véase «Explotación confirmada y adición al KEV de la CISA» en el artículo sobre PHP). Para que las comprobaciones no varíen de un servidor a otro, el operador aplicó la misma lista a todos los servidores.
| Elemento | Qué buscar | Comando de ejemplo |
|---|---|---|
| Claves de acceso | Claves públicas que no recuerda haber registrado | Abra authorized_keys |
| Usuarios | Usuarios desconocidos, cambios en grupos de administración | getent passwd, getent group sudo |
| Archivos modificados de paquetes | Archivos instalados por un paquete que han cambiado desde que se distribuyó | debsums -s (familia Debian), rpm -Va (familia RHEL) |
| Ejecutables sin paquete propietario | Ejecutables nuevos en /usr/local, /opt o en el PATH que ningún paquete posee | dpkg -S <archivo> (familia Debian) o rpm -qf <archivo> (familia RHEL); busque archivos sin paquete propietario |
| SUID | Archivos nuevos que se ejecutan con los privilegios de su propietario | find / -xdev -perm -4000 -type f |
| cron y systemd | Tareas y temporizadores programados desconocidos | crontab -l, /etc/crontab, /etc/cron.d, systemctl list-timers |
| /tmp | Archivos ejecutables colocados allí | find /tmp /var/tmp -type f -perm /111 |
| Conexiones salientes | Conexiones a destinos que no reconoce | ss -tunp |
| Dentro de contenedores | Procesos en ejecución y archivos escritos tras el inicio | docker top, docker diff |
crontab -l muestra solo las tareas del usuario actual, así que revise por separado /etc/crontab, /etc/cron.d y las colas de cron de cada usuario. ss -tunp necesita root para mostrar el proceso detrás de cada conexión. dpkg -S y rpm -qf indican qué paquete posee un archivo; «sin paquete propietario» puede seguir siendo un archivo que usted colocó, así que compárelo con lo que recuerde haber instalado.
find -perm -4000 coincide con archivos que tienen activos todos los bits indicados (find(1)). docker diff lista los archivos cambiados desde que se creó el contenedor (A: añadido, C: cambiado, D: borrado), y docker top muestra los procesos en ejecución (docker container diff, docker container top). Los contenedores normalmente se reconstruyen en lugar de modificarse, así que trate como inesperado un ejecutable que aparezca en docker diff e investíguelo (consejo de este sitio).
Cómo interpretar cada elemento, y los pasos para hosting compartido, WordPress y un VPS, están en Cómo comprobar si su sitio web o servidor fue hackeado.
Resolver una entrada de registro sospechosa comparando archivos
Durante esta comprobación, el equipo descubrió que, antes de la actualización, un POST había llegado al formulario de contacto desde un nodo de salida de Tor (un punto de salida de una red que oculta al remitente). Un registro solo muestra que la petición llegó. No muestra si se escribió algo como resultado.
Así que compararon los archivos públicos activos con la copia guardada en el momento del despliegue. Coincidían exactamente, y no había archivos PHP adicionales. Esta comprobación no encontró indicios de que se hubiera alterado ningún archivo.
Guarde una copia de lo que desplegó, fuera del servidor
Si lo único contra lo que comparar está en el servidor, un cambio simultáneo pasaría desapercibido. En cada despliegue, guarde el estado publicado fuera del servidor.
Compárelo con los archivos activos
Use diff -rq para encontrar archivos que difieran, o compare listas de hashes por archivo (sha256sum). Si algo difiere, compruebe si es un cambio que usted pretendía.
Busque también archivos añadidos
Aunque los contenidos coincidan, un archivo PHP recién colocado no está en la copia original, así que hay que buscarlo por separado. diff -rq también informa de archivos que existen solo en un lado.
Una coincidencia no es prueba de seguridad
Este método solo demuestra que los archivos comparados no han cambiado. No ve la base de datos, las carpetas de subidas ni los cambios fuera del servidor. Valórelo junto con las otras comprobaciones (consejo de este sitio).
Opinión de este sitio: defina la lista de comprobación antes de la noche de emergencia
En una noche con prisas, lo que se revisa en cada servidor tiende a variar. Si la lista de elementos se decide de antemano, cada servidor recibe las mismas comprobaciones y se ven las lagunas. El PHP de línea de comandos antiguo se encontró esta vez porque la comprobación posterior a la actualización no se limitó a la web.
Qué hacer hoy
- Si puede iniciar sesión en el servidor, ejecute
php -vywhich php, y escriba la ruta completa a PHP en sus tareas de cron - Compruebe la versión de PHP web desde el lado web, por ejemplo en Salud del sitio (y borre cualquier página
phpinfo()justo después de usarla) - Localice en los scripts con
set -elas llamadas adiff,grepocmp, y decida cómo se gestiona el estado de salida 1 - Guarde una copia de lo que publicó fuera del servidor, y compárela tras las actualizaciones
- Haga idéntica la lista de revisión de intrusión en todos los servidores y recórrala elemento por elemento
Fuentes
- PHP: Uso de PHP desde la línea de comandos
- PHP: php_sapi_name
- GNU Bash Reference Manual: The Set Builtin
- GNU diffutils: diff, cmp, GNU grep
- update-alternatives(1), crontab(5), find(1)
- docker container diff, docker container top
- CISA: adds two vulnerabilities to the Known Exploited Vulnerabilities Catalog
Lectura siguiente
- Las vulnerabilidades y las versiones corregidas: Dos vulnerabilidades críticas de PHP y las versiones de emergencia corregidas (octubre de 2026)
- Comprobar intrusiones: Cómo comprobar si su sitio web o servidor fue hackeado
- Preparación: Fundamentos de copias de seguridad y recuperación / Lista de inventario de seguridad
- Términos: ¿Qué es RCE (ejecución remota de código)? / ¿Qué es un IOC (indicador de compromiso)?