,

Cómo aplicamos un parche de seguridad crítico a PHP 7.4 sin actualizar de versión

·

Cómo aplicamos un parche de seguridad crítico a PHP 7.4 sin actualizar de versión

Cuando sale una vulnerabilidad crítica de PHP, actualizar suele ser tan fácil como ejecutar el actualizador de la distribución. Pero ¿qué pasa cuando el servidor afectado todavía depende de una versión de PHP que ya no recibe parches oficiales en forma de paquete, y la aplicación que corre en él no es compatible con ninguna versión moderna?

En este artículo explicamos, de forma genérica y reproducible, cómo corregimos dos vulnerabilidades críticas de PHP (ejecución de código remoto sin autenticación) en un servidor que tenía que seguir funcionando con PHP 7.4, compilando el parche oficial desde el código fuente y sustituyendo solo la pieza vulnerable.

Aviso: compilar y sustituir componentes de PHP a mano es el último recurso, para casos en los que no hay paquete oficial y no se puede migrar de versión a corto plazo. Siempre que exista un paquete oficial, usad el paquete. Y siempre, siempre, probadlo antes en una copia y tened una vía de marcha atrás.

El contexto del problema

El escenario era este:

  • Una aplicación antigua (un CRM heredado) que solo funciona con PHP 7.4 y que no se puede migrar a PHP 8 a corto plazo.
  • PHP 7.4 está en fin de vida: el equipo de PHP y los repositorios de terceros ya no publican paquetes nuevos para esta rama.
  • Ante una vulnerabilidad crítica, el equipo de seguridad de PHP sí publicó el código fuente del parche para las versiones antiguas, pensado precisamente para quien compila desde el código.

El objetivo: aplicar ese parche al servidor en producción con el mínimo riesgo.

La idea clave: sustituir solo el módulo, no reinstalar PHP

En una instalación típica con Apache y mod_php, PHP se ejecuta dentro de Apache como un módulo: un único fichero (libphp7.4.so). La vulnerabilidad estaba en el núcleo de PHP, que está dentro de ese módulo.

Por tanto, no hace falta recompilar todo PHP ni tocar las extensiones (base de datos, imágenes, correo, etc.), que son ficheros separados. Basta con compilar un módulo nuevo con el parche y sustituirlo, manteniendo el resto igual.

Esquema de Apache con mod_php: solo se sustituye libphp7.4.so; las extensiones y la configuración se mantienen
Los siete pasos del proceso: receta, entorno aislado, código oficial, compilar, probar, desplegar y verificar
El proceso completo. El trabajo pesado se hace en un contenedor; el servidor solo se toca al principio y al final.

Paso 1: recoger la «receta» de la instalación actual

Antes de nada, hay que saber exactamente cómo está compilado el PHP instalado, para reproducirlo de forma idéntica. Los datos importantes son:

  • La versión exacta y los números de API de PHP.
  • Si está compilado con Thread Safety o sin él (NTS).
  • Las opciones de compilación (./configure).
  • Qué extensiones van dentro del núcleo y cuáles son ficheros externos.

La versión y la arquitectura se pueden consultar así:

php -i | grep -E "^(PHP Version|PHP API|Thread Safety|extension_dir)"

Las opciones de compilación, si tenéis el paquete -dev instalado:

php-config --configure-options

Y haced una copia del módulo actual, que servirá de referencia y de marcha atrás:

cp /usr/lib/apache2/modules/libphp7.4.so  /root/libphp7.4.so.original

Paso 2: compilar en un entorno aislado e idéntico

Compilar directamente en el servidor de producción es una mala idea: consume recursos y deja herramientas de desarrollo instaladas. La solución limpia es compilar en un contenedor con el mismo sistema operativo que el servidor. Así el resultado es compatible, porque dentro del contenedor reproducimos las mismas bibliotecas que tiene el servidor.

# Contenedor con el mismo SO que el servidor
docker run -it --name build -v "$PWD":/out ubuntu:XX.XX bash

Dentro del contenedor, instalamos las herramientas de compilación y las bibliotecas de desarrollo que PHP necesita (XML, SSL, zlib, etc.):

apt-get update
apt-get install -y build-essential autoconf bison re2c pkg-config git \
  apache2-dev libxml2-dev libssl-dev libpcre2-dev zlib1g-dev \
  libargon2-dev libsodium-dev

Paso 3: descargar el código fuente oficial con el parche

El punto más importante de todo el proceso: el código tiene que venir de la fuente oficial. El equipo de seguridad de PHP publica el parche en el repositorio oficial de PHP, normalmente en una rama específica para la vulnerabilidad, y siempre lo enlaza desde el aviso oficial de php.net.

git clone --depth 1 -b <rama-oficial-del-parche> https://github.com/php/php-src.git
cd php-src

Verificad que el código es el que tiene que ser. Comprobad la diferencia respecto a la versión oficial que tenéis instalada: solo deberían cambiar los ficheros del parche, no cientos de ficheros.

git fetch --depth 1 origin tag php-7.4.XX
git diff --stat php-7.4.XX HEAD

Si la diferencia son unos pocos ficheros del núcleo (gestión de memoria, procesamiento de peticiones…), es el parche mínimo sobre vuestra versión. Si salen cientos, la rama no corresponde y hay que partir de la versión oficial exacta y aplicarle solo el parche.

Paso 4: compilar con la misma configuración

Se usa la misma receta del paso 1, con estas condiciones:

  • Conservar las rutas de configuración (--with-config-file-path, etc.).
  • Generar el módulo de Apache con --with-apxs2.
  • Mantener el mismo modo de Thread Safety (para Apache prefork, NTS).
./buildconf --force
./configure <las-mismas-opciones-que-el-paquete-original> --with-apxs2=/usr/bin/apxs2
make -j"$(nproc)"

Antes de darlo por bueno, comprobad que la configuración ha dejado las mismas extensiones internas que el módulo original y que no activa Thread Safety si no toca.

Paso 5: probar el módulo antes de ponerlo en producción

Dos comprobaciones valiosas:

a) Las pruebas oficiales del código. El código de PHP incluye miles de pruebas automáticas. Ejecutadlas para confirmar que el módulo compilado es sólido:

make test

b) Una instancia de Apache de prueba. Antes de tocar el servicio real, se puede levantar un Apache de test en un puerto interno que cargue el módulo nuevo con la configuración, las extensiones y los complementos reales, y comprobar que la versión es correcta, que carga todas las extensiones y que conecta con la base de datos. Todo ello sin tocar el servicio en producción.

Paso 6: desplegar y, sobre todo, poder volver atrás

Primero, quitad del módulo la información de depuración para que quede de un tamaño parecido al original:

strip --strip-unneeded libs/libphp7.so -o libphp7.4.so.patched

Copiad el módulo al servidor y comprobad su integridad con la huella (SHA-256), para aseguraros de que ha llegado entero:

sha256sum libphp7.4.so.patched

El despliegue se hace de forma que el actualizador de paquetes no vuelva a sobrescribir nuestro módulo. En sistemas basados en Debian/Ubuntu, esto se consigue con dpkg-divert, que «aparta» el fichero original y deja el nuestro en su sitio:

dpkg-divert --local --rename --add /usr/lib/apache2/modules/libphp7.4.so
install -m 644 libphp7.4.so.patched /usr/lib/apache2/modules/libphp7.4.so
apache2ctl configtest && systemctl restart apache2

Marcha atrás en un minuto, si hace falta:

rm /usr/lib/apache2/modules/libphp7.4.so
dpkg-divert --local --rename --remove /usr/lib/apache2/modules/libphp7.4.so
systemctl restart apache2
Cómo funciona dpkg-divert: el módulo original se aparta, el parche ocupa su sitio y la marcha atrás lo restaura

Paso 7: verificar que el servicio usa el módulo nuevo

El número de versión no cambia (sigue siendo 7.4.XX), así que hay que confirmarlo de otras formas:

  • La fecha de compilación (Build Date de un phpinfo) tiene que ser la de vuestro despliegue, no la original.
  • La huella del módulo instalado tiene que coincidir con la que calculasteis.
  • Ningún proceso del servidor web debe tener el módulo antiguo cargado en memoria (hay que reiniciar el servicio, no solo recargarlo).

Un efecto colateral importante: los escáneres de vulnerabilidades

Como el número de versión no cambia, los escáneres de vulnerabilidades (que a menudo solo leen la versión) seguirán marcando el servidor como vulnerable. Es un falso positivo. Conviene documentar el proceso (origen del código, integridad, huella del módulo desplegado) para poder justificarlo en una auditoría. Es exactamente el mismo criterio con el que se validan los parches retroportados de las distribuciones: no se comprueba el ataque en cada servidor, se verifica que el código corregido es el que se ejecuta.

El escáner ve la versión 7.4.XX y marca vulnerable; la fecha de compilación y la huella demuestran que se ejecuta el parche

Medidas complementarias mientras dure la situación

Compilar el parche resuelve la vulnerabilidad, pero un servidor con PHP y sistema operativo sin soporte sigue siendo una deuda técnica. Mientras no se pueda migrar, vale la pena acompañarlo de:

  • Las mitigaciones oficiales que recomiende el aviso de seguridad (a menudo, desactivar funcionalidades concretas y limitar el tamaño de las peticiones).
  • Reducir la exposición: limitar el acceso a la red interna o por VPN, y cerrar las herramientas de administración sensibles tras una autenticación adicional.
  • Un plan de migración real, porque esta solución es un puente, no un destino.

Conclusión

Cuando una vulnerabilidad crítica afecta a una versión de PHP sin soporte, no estáis atrapados entre «dejarlo abierto» y «romper la aplicación». Si el equipo de PHP publica el código del parche, se puede compilar un módulo corregido, probarlo en una copia y desplegarlo con una vía de marcha atrás clara, manteniendo la versión y la aplicación intactas.

No es el camino ideal (lo ideal es migrar a una versión con soporte), pero es una forma responsable de ganar el tiempo necesario para hacer la migración con garantías, sin dejar un agujero crítico abierto mientras tanto.

Traducción hecha con ayuda de IA a partir del artículo original en catalán.