Guías de seguridad

Qué comprobar tras una actualización urgente de PHP: PHP web frente a PHP de línea de comandos, set -e y códigos de salida de diff, y una revisión de intrusión

Tras un parche urgente, el PHP web y el CLI pueden diferir. Cómo comprobar ambos, set -e con diff y la revisión de intrusión.

Publicado 2026-11-01 Actualizado 2026-11-01 Última verificación 2026-11-01 14 min de lectura
En esta página

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.

  1. Tras el anuncio

    Activar la página de mantenimiento y esperar la versión corregida
  2. Tras la publicación

    Instalar la versión corregida y retirar la página de mantenimiento
  3. Comprobació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ó
  4. 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ó
  5. 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

En el mismo servidor, el PHP que usa la web y el que usa la línea de comandos pueden ser programas separados con versiones distintas. Tras el parche, compruebe ambos.

Compruebe web y CLI por separado

1

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.

2

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.

3

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.

4

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).

5

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»

ComandoSignificado de 1Significado de 2Fuente
diffDiferencias encontradasProblemaGNU diffutils: diff
cmpDiferencias encontradasProblemaGNU diffutils: cmp
grepNo se seleccionó ninguna línea (sin coincidencia)Se produjo un errorGNU 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 b sin más bajo set -e (se detiene solo porque hay diferencias)
  • diff a b || true (traga tanto las «diferencias» como un error real (2))
  • Usar || true en grep para 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 ;;
esac

set -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.

ElementoQué buscarComando de ejemplo
Claves de accesoClaves públicas que no recuerda haber registradoAbra authorized_keys
UsuariosUsuarios desconocidos, cambios en grupos de administracióngetent passwd, getent group sudo
Archivos modificados de paquetesArchivos instalados por un paquete que han cambiado desde que se distribuyódebsums -s (familia Debian), rpm -Va (familia RHEL)
Ejecutables sin paquete propietarioEjecutables nuevos en /usr/local, /opt o en el PATH que ningún paquete poseedpkg -S <archivo> (familia Debian) o rpm -qf <archivo> (familia RHEL); busque archivos sin paquete propietario
SUIDArchivos nuevos que se ejecutan con los privilegios de su propietariofind / -xdev -perm -4000 -type f
cron y systemdTareas y temporizadores programados desconocidoscrontab -l, /etc/crontab, /etc/cron.d, systemctl list-timers
/tmpArchivos ejecutables colocados allífind /tmp /var/tmp -type f -perm /111
Conexiones salientesConexiones a destinos que no reconocess -tunp
Dentro de contenedoresProcesos en ejecución y archivos escritos tras el iniciodocker 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.

1

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.

2

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.

3

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.

4

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

  1. Si puede iniciar sesión en el servidor, ejecute php -v y which php, y escriba la ruta completa a PHP en sus tareas de cron
  2. 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)
  3. Localice en los scripts con set -e las llamadas a diff, grep o cmp, y decida cómo se gestiona el estado de salida 1
  4. Guarde una copia de lo que publicó fuera del servidor, y compárela tras las actualizaciones
  5. Haga idéntica la lista de revisión de intrusión en todos los servidores y recórrala elemento por elemento

Fuentes

Lectura siguiente

FAQ

Q¿Puede seguir PHP en una versión antigua después de actualizarlo?
A

Sí. El PHP que gestiona las peticiones web (PHP-FPM o un módulo de Apache) y el PHP de línea de comandos (CLI) usado desde una shell o cron pueden ser programas separados con versiones distintas. php.net llama SAPI a la forma en que se ejecuta PHP y señala que el SAPI de CLI difiere de los demás en muchos aspectos. El lado web puede estar parcheado mientras el PHP que usa cron sigue siendo antiguo.

Q¿Cómo compruebo qué versión de PHP se está ejecutando realmente?
A

Para la línea de comandos, ejecute php -v. Para la web, compruébelo desde el lado web; en WordPress, es Salud del sitio en la pantalla de administración. Si usa una página phpinfo(), bórrela en cuanto termine. which php muestra qué ejecutable se llama y, en sistemas tipo Debian con varias versiones, update-alternatives --display php muestra a qué apunta el comando php. En las tareas de cron, escribir la ruta completa a PHP elimina cualquier duda sobre cuál se ejecuta.

Q¿Por qué se detiene un script de shell con set -e cuando uso diff?
A

diff devuelve el estado de salida 1 cuando encuentra diferencias. El manual de GNU define 0 como sin diferencias, 1 como diferencias y 2 como problema. set -e (errexit) detiene el script ante un estado distinto de cero, así que el simple hecho de encontrar una diferencia lo detiene. Coloque el comando en una condición if, o guarde el estado de salida en una variable y gestione 0, 1 y 2 por separado. grep (1 significa sin coincidencias) y cmp (1 significa distinto) se comportan igual.

Q¿Cómo decido si me han vulnerado cuando una entrada del registro parece sospechosa?
A

El registro por sí solo no basta para decidirlo. Un registro muestra un intento que llegó, no necesariamente un éxito. Compare sus archivos públicos activos con la copia que guardó en el momento del despliegue, y compruebe que coinciden y que no existen archivos PHP adicionales. Una coincidencia permite decir que no se encontró alteración de archivos. No es prueba de seguridad, así que combínela con las otras comprobaciones.