El problema no terminaba al cambiar las contraseñas

La intervención comenzó después de detectar publicaciones publicitarias no autorizadas en una plataforma de producción formada por WordPress y Moodle sobre un servidor Linux administrado con Plesk. Habían desaparecido plugins legítimos, parte del diseño y de los formularios había dejado de funcionar y el contenido malicioso continuaba apareciendo incluso después de cambiar varias contraseñas.

Eso indicaba que no bastaba con recuperar archivos. Había que tratar el entorno como un incidente completo: preservar evidencias, reconstruir la cronología, localizar persistencia, recuperar las aplicaciones y reducir la superficie de ataque sin interrumpir servicios legítimos.

La investigación relacionó accesos administrativos, publicaciones REST, marcas de tiempo, objetos SQL y registros antimalware. Se pudo demostrar que una cuenta válida había sido utilizada para instalar código no autorizado y crear contraseñas de aplicación de WordPress. Esas credenciales REST seguían funcionando aunque cambiara la contraseña normal del usuario.

Delimitar antes de tocar producción

El servidor alojaba más de una aplicación y compartía servicios de base de datos, certificados, correo y copias. Antes de modificar nada se inventariaron:

  • dominios, suscripciones y usuarios de Plesk;
  • bases y privilegios MariaDB;
  • versiones de WordPress, Moodle, PHP, plugins y temas;
  • tareas programadas, servicios, certificados y renovaciones;
  • administradores, sesiones, tokens y métodos de autenticación;
  • uso de disco, copias y directorios de datos.
Los archivos sospechosos se trasladaron a una cuarentena fuera del directorio público. Cada operación destructiva quedó precedida por una copia, un manifiesto o una comprobación de exclusividad. La fecha de la intrusión no se presentó como un hecho cuando la evidencia solo permitía demostrar la primera actividad observable.

Retirar la persistencia, no solo los síntomas

La contención localizó mecanismos distintos que podían mantener el acceso:

  • cuatro contraseñas de aplicación no autorizadas;
  • sesiones administrativas todavía activas;
  • plugins y gestores de archivos no legítimos;
  • webshells y código implantado en distintas zonas de WordPress;
  • un trigger MariaDB capaz de recrear una cuenta administrativa;
  • privilegios SQL que la aplicación no necesitaba.
Se revocaron las credenciales REST, se destruyeron sesiones, se retiraron cuentas y código no autorizados y se eliminó el trigger. El usuario SQL perdió capacidades globales como FILE, TRIGGER, EVENT y gestión de rutinas. Un mu-plugin bloqueó XML-RPC y la autenticación Basic remota contra la API REST donde no existía una integración legítima que la necesitara.

La decisión importante fue no aplicar un bloqueo genérico a todas las instalaciones. Otra web del servidor utilizaba integraciones que podían depender de REST, por lo que los controles se adaptaron al inventario real de cada aplicación.

Recuperar WordPress desde componentes verificables

El núcleo y los plugins públicos se comprobaron contra sus fuentes oficiales. Los componentes comerciales se recuperaron desde paquetes autorizados y una copia manipulada se mantuvo fuera de servicio.

La restauración de Flamingo demostró que código ausente no implica datos perdidos. El plugin había desaparecido, pero sus estructuras seguían en MariaDB. Tras guardar configuración y base de datos, se instaló una versión oficial y volvió a reconocer 1.694 contactos, 3.851 mensajes y 92.086 metadatos sin importar ni sobrescribir el histórico.

El editor integrado de PHP quedó bloqueado, mientras que los administradores autorizados conservaron la capacidad de instalar y actualizar plugins. También se forzó HTTPS en administración, se rotaron salts y secretos, se revisaron cuentas y se bloqueó la ejecución de PHP dentro de los directorios de cargas.

Recuperar y actualizar Moodle mediante una ruta soportada

Moodle 3.10.4 estaba fuera de soporte y respondía con errores sobre el runtime disponible. Su directorio de datos, de unos 39 GB, se movió fuera del webroot y quedó protegido con permisos restrictivos. Como contención se revocaron 16 tokens, se desactivaron servicios web no utilizados y se cerraron sesiones.

La actualización no se hizo en un salto directo. Primero se probaron las operaciones sobre una base clonada y se prepararon tres niveles de reversión. La ruta aplicada fue:

text
MariaDB 10.3 -> MariaDB 10.11
471 tablas utf8mb3 -> utf8mb4_unicode_ci
Moodle 3.10.4 -> Moodle 4.1.22 -> Moodle 4.5 LTS

Se conservaron únicamente plugins necesarios y compatibles. Las referencias huérfanas en base de datos no se tomaron como motivo suficiente para reinstalar código antiguo.

Plesk también conserva estado

El endurecimiento incluyó Nginx, Apache, handlers PHP, ModSecurity, Imunify360, firewall, TLS, cron, backups y MariaDB limitada a loopback. Siempre que Plesk ofrecía una vía soportada se utilizó su panel o CLI, evitando editar archivos que el propio panel podía regenerar después.

Al retirar una segunda web que ya había sido migrada, se demostraron primero las relaciones exclusivas entre dominio, directorio, base, usuario SQL, correo, certificado y logs. Después de eliminar el dominio persistían órdenes ACME, notificaciones y generaciones de certificados dentro de SSL It!. La limpieza retiró solo los objetos del dominio desaparecido y verificó que la tarea real de renovación no los recreara.

Saneamiento de identidades y copias

La retirada de un antiguo responsable técnico se abordó de forma transversal. Se revisaron WordPress, Moodle, formularios, SMTP, Plesk, copias, cron, certificados, OAuth y configuraciones serializadas. Se eliminaron identidades y destinatarios operativos, pero se conservaron registros históricos cuando eran necesarios como evidencia y no otorgaban acceso.

Después de crear y comprobar una copia saneada se retiraron backups antiguos que podían reintroducir cuentas o secretos. La compresión de un dump no se consideró cifrado: los traslados quedaron condicionados a canales y almacenamiento protegidos.

Depurar Moodle sin borrar por la fecha equivocada

Para eliminar cursos antiguos se eligió startdate, no timecreated, porque respondía a la intención funcional del corte. Antes de borrar se midieron 11 cursos, 409 actividades, 70 matrículas y 9.201 referencias de archivo.

Los cursos se eliminaron mediante la API de Moodle y no con SQL directo. Se desactivó temporalmente la papelera para no duplicar varios gigabytes, se conservaron objetos todavía compartidos y se purgaron únicamente 3.083 archivos sin referencias. El resultado liberó aproximadamente 3,61 GiB y protegió siete cursos que habían sido creados antes del corte pero comenzaban después.

No se ejecutó OPTIMIZE TABLE en producción: el espacio interno recuperable era pequeño frente al coste y los posibles bloqueos sobre cientos de tablas.

Validación y resultado

La validación combinó comprobaciones funcionales, integridad, permisos, servicios y análisis antimalware:

  • 145.680 archivos y 130.425 recursos de base de datos sin nuevos hallazgos;
  • 29.990 archivos de Moodle y 127.925 recursos de base de datos limpios en el escaneo final;
  • core, plugins, cron, login, cookies, SMTP y acceso a moodledata revisados;
  • certificados, HTTPS, firewall, backups y privilegios MariaDB comprobados;
  • reglas temporales de administración retiradas al terminar.
La conclusión no fue que el sistema resultara imposible de comprometer. La afirmación defendible fue que se eliminaron los mecanismos de persistencia identificados, no quedaron hallazgos activos en las comprobaciones realizadas y la superficie de ataque se redujo de forma medible.

Qué dejó el proyecto

Además de recuperar las aplicaciones, se creó un runbook para reconstruir la arquitectura desde cero sin trasladar usuarios, contenidos, cursos ni secretos potencialmente contaminados. Se prepararon paquetes reinstalables de componentes autorizados, con análisis PHP, revisión de permisos, hashes y manifiestos.

La IA se utilizó como apoyo para inventarios, consultas de diagnóstico, automatización y documentación, siempre con objetivos acotados, copias previas, precondiciones verificables y revisión humana antes de acciones sensibles.

Lo que aprendí

  • Cambiar contraseñas no elimina persistencia en código, base de datos, tokens o cron.
  • Una plataforma compartida exige demostrar dependencias antes de borrar.
  • Las APIs oficiales conocen relaciones que un DELETE SQL puede ignorar.
  • Plesk y sus extensiones mantienen estado más allá de los archivos visibles.
  • Una copia posterior al saneamiento evita restaurar identidades y secretos retirados.
  • Documentar límites de validación es más profesional que prometer seguridad absoluta.