Un servidor reinfectado cuatro veces: la CVE de Next.js que tardamos dos meses en ver
10 de julio de 2026
Durante casi dos meses, un servidor bajo nuestra operación se reinfectó con un cryptominer una y otra vez. Lo limpiábamos a fondo, lo endurecíamos, y en cuestión de horas volvía a estar comprometido. Cambiamos contraseñas, activamos doble factor, reinstalamos el sistema operativo desde cero. Nada lo detenía. La hipótesis que todos habríamos aceptado —que la infraestructura del proveedor estaba comprometida— era falsa, y perseguirla nos habría costado una migración inútil.
Este es el relato técnico de cómo lo diagnosticamos, qué lo causaba realmente, y la lección que queda. Escribo en primera persona del plural porque el trabajo lo hice acompañado de Claude Opus 4.8, el asistente de Anthropic, en un flujo de investigación forense que vale la pena documentar por sí mismo.
El síntoma
El patrón era siempre idéntico. Un binario con nombre aleatorio aparecía en /root, un proceso consumía el 100% de la CPU minando Monero, se plantaba un usuario puerta-trasera con UID 0, un cron malicioso y un servicio systemd disfrazado de proceso legítimo del sistema. El load average se disparaba a 4 en un servidor de 4 núcleos.
Lo desconcertante no era el malware —XMRig es conocido y fácil de erradicar— sino su persistencia. Cada vez que limpiábamos, volvía. Y lo hacía sin dejar rastro de acceso: los registros de autenticación no mostraban ningún inicio de sesión SSH del atacante. Ni fuerza bruta exitosa, ni claves añadidas, ni sesiones por consola. El malware simplemente reaparecía.
Los diagnósticos equivocados
Las primeras hipótesis eran razonables, y todas resultaron falsas:
- El panel del proveedor. Como no había login SSH, asumimos que entraban por la consola serial del panel de virtualización. Cambiamos la contraseña del panel, activamos 2FA, deshabilitamos el auto-login de la consola. Volvió a pasar.
- La cuenta de correo. Si podían resetear el panel, quizás controlaban el email de recuperación. Auditamos la cuenta a fondo: sin reglas de reenvío ocultas, sin contraseñas de aplicación maliciosas, 2FA activo desde años. Limpia.
- Un implante durmiente. Tras la tercera limpieza pensamos que algo sobrevivía a las erradicaciones quirúrgicas. Así que reinstalamos el sistema operativo completo, desde una imagen limpia, y lo endurecimos desde el primer minuto: SSH solo por clave, root bloqueado, firewall estricto,
fail2ban.
Cincuenta minutos después de esa reinstalación limpia, el servidor estaba reinfectado otra vez.
Ese dato lo cambió todo. Un sistema recién instalado, con acceso solo por clave y sin ninguna credencial expuesta, comprometido en menos de una hora sin un solo login. La conclusión parecía inevitable: el problema estaba por debajo del sistema operativo, en el hipervisor del proveedor. La recomendación lógica era migrar a otro proveedor de inmediato.
La pregunta correcta
Antes de aceptar esa conclusión —cara e irreversible— nos detuvimos en una pregunta incómoda: ¿y si no es la infraestructura? ¿Y si es uno de los sitios que aloja el servidor?
La conclusión de "infraestructura comprometida" era la más cómoda, pero también la que cerraba la investigación. Decidimos medirla contra la evidencia en lugar de asumirla. La clave estaba en un dato que teníamos delante pero no habíamos cruzado: el segundo exacto en que aparecía el primer binario del malware.
Con find reconstruimos la línea temporal de creación de archivos en el momento de la reinfección:
2026-07-10 21:36:15 /root/z9lp89ac
2026-07-10 21:36:16 /root/0tb9x4eo
2026-07-10 21:36:18 /root/.xqnhcclswcgl
2026-07-10 21:36:19 /root/.drnvmazftoal
2026-07-10 21:36:19 /root/g0w78t3d/.../bsfubea5bpji <- el minero
El primer artefacto nació a las 21:36:15. La siguiente pregunta era obvia: ¿qué estaba ocurriendo en el servidor exactamente en ese segundo?
El forense: correlación de logs
Revisamos el access.log de nginx en esa ventana de tiempo. Entre el ruido habitual de bots escaneando rutas de WordPress, apareció esto:
85.x.x.x - - [10/Jul/2026:21:36:15] "POST /es HTTP/1.1" 303 14034
85.x.x.x - - [10/Jul/2026:21:36:16] "POST /es HTTP/1.1" 303 14034
Una IP externa hizo un POST a una ruta de la aplicación Next.js en el segundo exacto en que nació el binario. Y no fue una vez: la misma IP repitió el patrón diecisiete minutos después, coincidiendo con una segunda oleada de binarios que también habíamos registrado. Cada POST de esa dirección correspondía, al segundo, con una descarga del minero.
No era coincidencia. Era un exploit HTTP. El vector no era el panel, ni el correo, ni el hipervisor. Era la propia aplicación web, alcanzable en el puerto 443, respondiendo a una petición maliciosa con ejecución de código.
La causa raíz: CVE-2025-55182
El sitio corría Next.js 15.4.2. Esa versión, con App Router y React Server Components, es vulnerable a CVE-2025-55182, apodada React2Shell: una ejecución remota de código con severidad CVSS 10.0, explotable mediante peticiones POST que abusan del mecanismo de Server Actions (el header Next-Action y la deserialización de componentes de servidor).
En términos simples: un atacante envía una petición cuidadosamente construida a cualquier ruta de la aplicación, la deserialización insegura la ejecuta en el servidor, y a partir de ahí despliega lo que quiera. En este caso, un cryptominer.
Esto explicaba cada anomalía que nos había confundido durante dos meses:
- Sin login SSH: el atacante nunca tocó SSH. Entraba por HTTP, como cualquier visitante.
- Sobrevivía a las limpiezas: mientras la aplicación siguiera en línea con la versión vulnerable, los bots que escanean internet la volvían a explotar.
- Sobrevivió a la reinstalación: al restaurar el sitio, volvía la misma versión vulnerable. Cincuenta minutos después, un bot la encontró y la explotó de nuevo.
- Cambiar contraseñas y 2FA no sirvió: no usaban ninguna credencial.
- La cronología histórica encajaba: el sitio se había lanzado dos días antes de la primera infección. El bot lo encontró casi de inmediato.
La hipótesis de "infraestructura del proveedor" no solo era falsa: migrar de proveedor no habría cambiado nada. El bot habría encontrado la misma aplicación vulnerable en cualquier IP.
La solución
La corrección de raíz fue de una sola línea en las dependencias:
- "next": "15.4.2"
+ "next": "^15.4.10"
La versión 15.4.10 incorpora el runtime de React Server Components parcheado. Actualizamos, reconstruimos y verificamos que la aplicación seguía compilando sin cambios de código. Después reconstruimos el servidor por última vez —esta vez sabiendo que el vector estaba cerrado— y restauramos todos los sitios sobre la versión segura.
La prueba de que funcionó fue empírica. La misma petición que antes devolvía 303 con 14 KB de payload explotable, ahora responde:
POST /es -> 404
La deserialización insegura ya no ocurre. Y donde antes el servidor se reinfectaba en cincuenta minutos, pasó más de una hora sin un solo indicio de compromiso, con un watchdog vigilando cada cinco minutos. El ciclo estaba roto.
Como prevención, activamos Dependabot en el repositorio: alertas de vulnerabilidad y pull requests automáticos ante cualquier CVE futura. La próxima vez que una dependencia crítica quede expuesta, lo sabremos antes de que un bot lo haga.
El papel de la IA en el diagnóstico
Vale la pena ser explícito sobre cómo trabajamos, porque es parte de la tesis de esta firma: la IA aplicada no reemplaza el criterio, lo amplifica.
Claude Opus 4.8 ejecutó la investigación de campo —recolección forense, correlación de logs, reconstrucción de líneas temporales, erradicación quirúrgica, reconstrucción del servidor y restauración de once sitios— sosteniendo el contexto completo del incidente a lo largo de horas. Pero el giro decisivo no fue técnico: fue negarse a aceptar la primera conclusión plausible. Cuando la hipótesis cómoda era "es el proveedor, hay que migrar", la decisión de medir esa hipótesis contra la evidencia —el segundo exacto del log— vino de cuestionar el diagnóstico, no de aceptarlo.
Esa es la colaboración que funciona: la máquina sostiene el detalle y ejecuta sin fatiga; la persona insiste en la pregunta correcta. Ninguna de las dos, sola, habría cerrado el caso en un día.
La lección
Si este incidente deja una sola idea, es esta: no aceptes la primera causa plausible sin medirla contra la evidencia.
La explicación de "infraestructura comprometida" era coherente con todos los síntomas. Encajaba. Y era falsa. Lo que la desmontó no fue una herramienta más sofisticada, sino cruzar dos datos que ya teníamos —la marca de tiempo de un archivo y una línea de un log de acceso— con la disciplina de preguntar ¿qué más podría ser? antes de tomar una decisión irreversible.
En seguridad, como en arquitectura de datos, la hipótesis cómoda y la hipótesis correcta rara vez son la misma. La diferencia entre ambas suele estar en un detalle que ya está delante de ti, esperando que lo cruces con otro.
Y, en lo concreto: si operas una aplicación Next.js 15.x con App Router, verifica tu versión hoy. La CVE-2025-55182 es real, tiene exploits funcionales circulando, y los bots ya la están buscando.
Rubén Bolívar es fundador y arquitecto principal de TheTreeWay. Esta investigación se realizó en colaboración con Claude Opus 4.8 de Anthropic.