Ilustración: verificación del parche, cuatro capas de log y una rotación que nunca se ejecutó

Este informe está anonimizado. Describe un incidente real sin nombres de host, direcciones ni datos de clientes. El foco no es la vulnerabilidad, sino la cadena de prueba: ¿qué se comprueba cuando hay que decidir en horas si un sistema ha sido comprometido — y con qué facilidad se lee mal el resultado?

Punto de partida

En un solo día el proyecto PHP publicó dos vulnerabilidades críticas explotables sin autenticación (CVE-2026-109618 y CVE-2026-109619, CVSS 9,2 cada una, denominadas en conjunto «PHP2Shell»). Afectan a instalaciones PHP que atienden peticiones web. El host en cuestión ejecutaba un proxy inverso, una instancia de Nextcloud sobre PHP-FPM y varias webs estáticas. La pregunta del operador era sencilla:

¿Estamos afectados? ¿Está activo el parche? ¿Y muestran los logs que alguien haya explotado la falla?

Tres preguntas, tres problemas de prueba muy distintos. Ahí estaba la dificultad.

Paso 1 — probar la afectación en lugar de suponerla

El primer reflejo («usamos PHP, luego estamos afectados») es demasiado grueso. Lo decisivo es la SAPI: solo están expuestas las instalaciones que procesan peticiones web. La prueba fue igual de concreta:

Paso 2 — prueba del parche: paquete y tiempo de ejecución

Una actualización de seguridad instalada aún no es un parche activo. Entre «el paquete está en disco» y «el proceso en ejecución lo usa» hay un reinicio del maestro FPM. Por eso se comprobó dos veces:

ComprobaciónResultadoQué prueba
Versión del paquete8.5.4-0ubuntu1.5 (fix de Ubuntu, USN-8948-1/-2)El fix está instalado
Momento de instalación2026-10-22 06:12 (actualizaciones automáticas)Cuándo llegó el fix
Reinicio de FPM2026-10-22 06:13, maestro activo desde entoncesEl fix se ejecuta
Integridad de archivosverificación de paquetes sin desviaciónSin binarios manipulados

Estas cuatro líneas son la prueba real del parche. Todo lo demás es interpretación de logs.

Paso 3 — cuatro capas de log, cuatro preguntas distintas

Un solo log no puede probar un incidente. Cada capa responde a una pregunta distinta — y tiene su propio punto ciego:

CapaRespondePunto ciego
Log de acceso del proxy¿Qué peticiones llegaron, con qué estado y tamaño de cuerpo?Sin contenido; la rotación limita la retrospectiva
Log de la aplicación (Nextcloud)¿Qué hizo la aplicación internamente — errores, avisos, inicios de sesión?Con nivel info solo eventos, sin registros de acceso
Log de FPMCaídas de workers, agotamiento de memoria, errores fatalesSolo nivel PHP, sin contexto HTTP
Journald / kernelOOM kills, segfaults, core dumpsSin semántica de aplicación

Además: Fail2ban (fuerza bruta SSH) y el gestor de paquetes (momento del parche). Solo la combinación sostiene una afirmación — y solo si se nombran los puntos ciegos.

El hallazgo falso — «la aplicación no escribe log»

La búsqueda del log de la aplicación recorrió los directorios habituales (/srv, /var, /home, profundidad limitada) y no encontró nada. La conclusión era obvia: no hay log. Era falsa.

Se habían pasado por alto dos cosas:

  1. El directorio de datos estaba fuera del espacio de búsqueda. La instancia no guardaba sus datos en el directorio de programa, sino en un volumen propio. La búsqueda fue minuciosa — solo que no allí.
  2. El logger tenía un fallback. Si el archivo de log configurado no existía y su directorio no era escribible, la aplicación escribía en un archivo sustituto dentro del directorio de datos. Es un comportamiento correcto — pero oculta una mala configuración.

El defecto real estaba una capa más abajo: el trabajo de rotación lee la ruta del log desde la configuración. Como esa ruta no existía, comparaba el tamaño de un archivo inexistente — y nunca rotaba. Resultado: un único archivo de 438 MB en lugar de rotación horaria.

flowchart TD C["Configuración:
logfile = dir-programa/data"] --> L{"¿Archivo existe?"} L -->|no| F["El logger usa el fallback:
dir-datos/nextcloud.log"] L -->|sí| W["El logger escribe el archivo configurado"] C --> R["El trabajo de rotación lee
la misma ruta configurada"] R --> N{"¿Archivo ahí?"} N -->|no| X["Sin rotación
(el archivo crece sin límite)"] F --> X

La lección es incómoda porque afecta al propio trabajo: «no encontrado» no es un hallazgo mientras el espacio de búsqueda no esté documentado. Un hallazgo negativo exige el mismo cuidado que uno positivo — incluida la indicación de dónde no se buscó.

El segundo hallazgo falso — el cortafuegos

Un puerto de administración era «accesible solo en la LAN». La comprobación no halló ninguna autorización específica para ese puerto y anotó: no hay autorización, por tanto cerrado desde fuera. También era demasiado corto.

En realidad existía una regla general para toda la subred LAN que permitía cualquier puerto. El puerto estaba abierto en la LAN — la protección venía de la topología de red, no de la regla de puerto. El resultado («no accesible desde internet») era correcto, la justificación no. Y una justificación errónea es el comienzo de la siguiente decisión equivocada.

Regla: un cortafuegos se lee en conjunto — política por defecto, autorizaciones generales, orden. Mirar solo «¿falta una autorización de puerto?» no basta.

Lo que los logs mostraron realmente

Tras corregir la ruta del log, la evaluación fue posible — y el cuadro resultó anodino:

El único hallazgo «caliente» fue un escáner que envió cuatro peticiones de aproximadamente 1 MB al sitio Adminer huérfano — exactamente el patrón que produce un intento de explotación. Las cuatro terminaron en 502 porque el socket PHP no existía. El ataque corrió contra un backend muerto. Eso también es un resultado: el hallazgo más peligroso demostró la eficacia de un endurecimiento (involuntario).

El límite de la prueba — y por qué hay que asegurar la evidencia

Dos reglas de retención limitaban la afirmación:

  1. Logs del proxy: rotación de 5 × 10 MB — unos diez días de retrospectiva. El periodo quedó cubierto por completo, nada más.
  2. Log de la aplicación: la rotación conserva exactamente un archivo anterior. En la siguiente ejecución el periodo evaluado habría sido sobrescrito.

Por eso el periodo se aseguró antes de la siguiente rotación como archivo propio: comprimido, propietario root, permisos 640 (el archivo contiene nombres de usuario y direcciones IP), con suma de verificación documentada. Una prueba que no se puede reproducir vale poco en una auditoría — una suma de verificación convierte un archivo en una afirmación verificable.

flowchart LR A["Log en vivo
(un archivo rotado)"] --> B["Extracto del periodo
2026-10-15 … 2026-10-25"] B --> C["Archivo gzip
root:root 640"] C --> D["SHA-256 en el informe"] D --> E["Reproducción:
gunzip + wc -l"]

Lo que funcionó bien

Lo que haremos distinto la próxima vez

  1. Documentar el espacio de búsqueda antes de buscar. Escribir «no encontrado» solo cuando se nombren rutas y profundidades.
  2. Añadir una contraprueba a cada afirmación de configuración. Ruta del log: no solo leerla, sino verificar que el archivo existe, crece y rota.
  3. Probar la rotación activamente (superar el tamaño artificialmente, demostrar la rotación en el log) — no suponer que funciona.
  4. Leer el cortafuegos en conjunto (política por defecto, reglas generales de subred, orden).
  5. Desmontar de inmediato los sitios huérfanos. Un sitio PHP muerto no es un riesgo, pero es una fuente permanente de ruido en el log y un punto ciego en la valoración.
  6. Asegurar la evidencia antes de la rotación — con propietario, permisos y suma de verificación.
  7. Corregir los hallazgos falsos, no suavizarlos. El informe contiene la corrección con fecha y causa. Un hallazgo falso reparado en silencio destruye más confianza que el propio error.

Transferibilidad

El incidente en sí fue poco espectacular: parcheado, no explotado, sin obligación de notificación. Lo valioso fue el aparato de prueba: afectación mediante inventario, parche mediante paquete y tiempo de ejecución, explotación mediante varias capas de log, valoración con límites de prueba explícitos, evidencia con suma de verificación — y un informe con historial de cambios que también contiene la propia equivocación. Esta estructura se puede versionar en Git y verificarse años después.

Fuentes