WordPress hackeado: respuesta a incidentes paso a paso que no lo empeora

¿Te fue útil?

Tu sitio WordPress se comporta de forma extraña. El tráfico se desplomó. Search Console alarma. Clientes envían capturas de pantalla con enlaces a casinos que definitivamente no publicaste. Alguien en Slack dice “simplemente elimina el sitio y restaura desde copia de seguridad”. Otro sugiere “instala un plugin de seguridad”. Ambos quieren ayudar. Ambos pueden empeorar las cosas.

Cuando WordPress es atacado, el problema técnico tiene solución. El problema operacional es más difícil: no destruyas evidencias, no propagues la infección, no te engañes con un escaneo “verde” y no restaures una copia comprometida en producción con la confianza que no te has ganado.

Reglas básicas: estabiliza antes de “arreglar”

La respuesta a incidentes no es una vibra. Es una secuencia. Puedes ir rápido, pero no puedes saltarte pasos sin pagar después—por lo general a las 2 a.m. con un CFO en la llamada.

Aquí están las reglas que te sacan de desastres autoinfligidos:

  • No inicies sesión por wp-admin para “ver qué pasa” si sospechas robo de credenciales. Usa primero acceso al lado del servidor.
  • No ejecutes herramientas de “limpieza” primero. Mutan las evidencias y pueden borrar la única pista sobre cómo entró el atacante.
  • No restaures sobre un host comprometido en ejecución. Si el host está controlado, tu WordPress “nuevo” es solo un nuevo inquilino.
  • Contén primero, luego investiga. Ten en cuenta el impacto empresarial, pero limita el radio de daño antes de empezar a hurgar al oso.
  • Decide tu objetivo: ¿es un sitio de marketing que puedes dejar fuera de línea, o un checkout crítico para ingresos? Tus decisiones de contención difieren.

Una cita para recordar (idea parafraseada): Gene Kranz, director de vuelo de la NASA, promovía “duros y competentes” como estándar operativo—sin excusas, sin pánico, solo acción disciplinada.

Guion de diagnóstico rápido (primero/segundo/tercero)

Esto es el guion de “entras, no tropiezas con tus propios pies”. El objetivo es identificar el cuello de botella: ¿es una compromisión a nivel WordPress, a nivel host, una cadena de suministro (plugin/tema) o simplemente consecuencias de tráfico/SEO?

Primero: confirma el impacto y la prioridad de contención (5–10 minutos)

  • ¿El sitio está sirviendo contenido malicioso ahora? Si sí: contén en el borde (WAF/CDN) o en el servidor web (modo mantenimiento / reglas deny).
  • ¿Hay riesgo de exposición de datos? Si el sitio tiene cuentas de clientes, checkout o inicios de sesión de admin: asume robo de credenciales hasta que se pruebe lo contrario.
  • ¿El host es compartido? Hosting compartido o VM multi-tenant implica movimiento lateral plausible. Contén más agresivamente.

Segundo: identifica la capa de compromiso (10–20 minutos)

  • Signos exclusivos de WordPress: JS inyectado en posts, usuarios admin falsos, archivos de tema modificados, plugins con puertas traseras, eventos cron raros.
  • Signos a nivel host: procesos desconocidos, claves SSH que no reconoces, comportamiento tipo rootkit, conexiones salientes sospechosas.
  • Signos de cadena de suministro: una actualización de plugin/tema alrededor de la detección inicial, o un tema “nulled” en el historial. (Nulled es una palabra elegante para “pirateado con malware”).

Tercero: elige la vía segura más rápida

  • Si es probable compromiso del host: reconstruye el host. No negocies con él.
  • Si es probable compromiso de WordPress: traslada el sitio a un host limpio, restaura desde una copia conocida buena y migra contenido selectivamente.
  • Si no tienes buenas copias de seguridad: haz forense y limpieza quirúrgica, pero planea una reconstrucción de todos modos.

Nueve hechos y un poco de historia (que cambian decisiones)

  1. WordPress comenzó en 2003 como un fork de b2/cafelog. Su éxito lo convirtió en objetivo, porque a los atacantes les gusta el ROI.
  2. La era “TimThumb” (principios de 2010) enseñó una lección dolorosa: un componente popular usado en todos lados se convierte en multiplicador de explotación masiva.
  3. XML-RPC (introducido para publicación remota) se ha abusado repetidamente para fuerza bruta y patrones de amplificación. Muchos sitios no lo necesitan.
  4. wp-cron.php no es un cron real; se dispara por peticiones web. Bajo carga o ataque puede convertirse en un DoS autoinfligido o en un canal de persistencia.
  5. La integridad de archivos es una superpotencia: el core de WordPress es determinista. Si no puedes decir rápido qué cambió, operas a ciegas.
  6. La mayoría de compromisos no son “zero-days”; son plugins/temas desactualizados, contraseñas reutilizadas, credenciales filtradas o archivos con permisos de escritura.
  7. El spam SEO suele ser “condicional”: los atacantes sirven páginas limpias a administradores y páginas maliciosas a bots de búsqueda o agentes específicos para retrasar la detección.
  8. Las puertas traseras aman lugares aburridos: mu-plugins, directorios must-use, directorios de caché, uploads y carpetas “temporales” son escondites comunes porque la gente no los diferencia.
  9. Un escaneo limpio no prueba limpieza: muchos escáneres se basan en firmas. Los atacantes pueden ser más creativos que una expresión regular.

Fase 1: contención sin daño colateral

La contención consiste en detener el daño. No en probar la causa raíz. No en hacer una limpieza perfecta. Detén la hemorragia.

Contén en el borde si puedes

Si tienes un CDN/WAF, cambia el sitio a una página de mantenimiento o bloquea rutas sospechosas. Es rápido, reversible y no muta el host comprometido.

  • Bloquea acceso a /wp-admin y /wp-login.php salvo desde IPs conocidas.
  • Limita por tasa o bloquea peticiones con patrones de exploit que apunten a endpoints vulnerables conocidos.
  • Desactiva temporalmente XML-RPC si no lo necesitas.

Contén en el servidor cuando el borde no esté disponible

Si estás en una VM única sin WAF, aún puedes contener. Hazlo de modo que preserves logs y no destruyas el sistema de archivos.

Broma corta #1: Si tu primera reacción es “chmod -R 777 para que funcione”, felicitaciones—has inventado un servicio de suscripción para malware.

Fase 2: preservar evidencias (seguro barato)

Preservar evidencias no es cosplay forense. Es responder dos preguntas después: “¿Cómo entraron?” y “¿Estamos realmente limpios?” Sin evidencias, adivinarás. Adivinar es caro.

Preserva:

  • Logs del servidor web (access y error).
  • Logs de PHP-FPM si existen.
  • Árbol de directorios de WordPress (o al menos hashes + metadatos).
  • Snapshot de la base de datos.
  • Logs de autenticación del sistema (/var/log/auth.log o equivalente).
  • Lista de procesos y conexiones de red.

Fase 3: triage con comandos (qué significa, qué decides)

Estas tareas están diseñadas para ejecutarse en un host Linux típico (Debian/Ubuntu-ish). Ajusta rutas para tu distro y pila web. Cada tarea incluye: un comando, qué significa la salida típica y la decisión que tomas.

Task 1: Identify the web root and ownership drift

cr0x@server:~$ ps aux | egrep 'nginx|apache2|httpd|php-fpm' | head
root       812  0.0  0.3  56400  7420 ?        Ss   Dec26   0:02 nginx: master process /usr/sbin/nginx
www-data   913  0.0  0.4  59824 10240 ?        S    Dec26   0:11 nginx: worker process
root      1021  0.0  0.6 225320 13240 ?        Ss   Dec26   0:05 php-fpm: master process (/etc/php/8.2/fpm/php-fpm.conf)
www-data  1055  0.0  0.7 225320 15400 ?        S    Dec26   0:40 php-fpm: pool www

Qué significa: Tus workers web/PHP corren como www-data. Si los archivos de WordPress son propiedad de www-data, el proceso web puede modificarlos, lo cual es conveniente para actualizaciones y extremadamente conveniente para atacantes.

Decisión: Si el sitio está comprometido y es propiedad del usuario web, trata la integridad de archivos como no confiable. Planea reconstruir desde fuentes conocidas buenas, no “limpiar en sitio”.

Task 2: Freeze the filesystem view (quick inventory of recent changes)

cr0x@server:~$ sudo find /var/www/example -type f -mtime -3 -printf '%TY-%Tm-%Td %TT %u %g %p\n' | head
2025-12-26 09:12:44.000000000 www-data www-data /var/www/example/wp-content/uploads/2025/12/cache.php
2025-12-26 09:12:50.000000000 www-data www-data /var/www/example/wp-includes/version.php
2025-12-26 09:13:02.000000000 www-data www-data /var/www/example/wp-content/plugins/hello/hello.php

Qué significa: Archivos del core como wp-includes/version.php no deberían ser modificados por actividad de uploads. cache.php dentro de uploads es una señal clásica de “PHP en uploads”.

Decisión: Si ves modificaciones de archivos del core fuera de actualizaciones planeadas, escala a “manipulación del core”. Eso suele significar reemplazo completo del código.

Task 3: Check for PHP execution in uploads (configuration reality check)

cr0x@server:~$ grep -R --line-number -E 'location\s+~\s+\*?\s*\^?/wp-content/uploads|deny\s+all|php' /etc/nginx/sites-enabled/* 2>/dev/null
/etc/nginx/sites-enabled/example.conf:42:location ~* ^/wp-content/uploads/.*\.php$ { deny all; }

Qué significa: Este servidor tiene una regla que niega PHP en uploads. Si no ves esto (o equivalente), los atacantes pueden dejar un backdoor “.php” en uploads y ejecutarlo.

Decisión: Si falta, añádela durante la recuperación. Si está presente pero todavía ves backdoors en uploads, busca rutas de ejecución alternativas (p. ej., .phtml, handlers mal configurados o enrutamiento directo a PHP-FPM).

Task 4: Inspect web access logs for exploit spikes and suspicious endpoints

cr0x@server:~$ sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  4123 /wp-login.php
  1988 /xmlrpc.php
   744 /wp-admin/admin-ajax.php
   321 /wp-content/uploads/2025/12/cache.php
   275 /wp-json/wp/v2/users

Qué significa: Alto volumen a wp-login.php y xmlrpc.php sugiere ataques de credenciales. Peticiones a un archivo PHP en uploads indican intentos de ejecución o uso exitoso.

Decisión: Si ves hits directos a un archivo sospechoso, prioriza aislar y preservar ese archivo, luego busca hermanos y mecanismos de persistencia.

Task 5: Correlate suspicious requests with response codes

cr0x@server:~$ sudo grep 'cache.php' /var/log/nginx/access.log | tail -5
203.0.113.44 - - [26/Dec/2025:09:12:56 +0000] "GET /wp-content/uploads/2025/12/cache.php?cmd=id HTTP/1.1" 200 31 "-" "curl/7.74.0"
203.0.113.44 - - [26/Dec/2025:09:13:10 +0000] "GET /wp-content/uploads/2025/12/cache.php?cmd=uname+-a HTTP/1.1" 200 98 "-" "curl/7.74.0"

Qué significa: Una respuesta 200 a ?cmd=id es básicamente una confesión. Esto es una web shell interactiva o ejecutor de comandos.

Decisión: Trátalo como ejecución remota confirmada. Asume robo de credenciales e intentos de movimiento lateral. Reconstruye el host a menos que puedas probar límites de aislamiento.

Task 6: Snapshot running processes (look for weird PHP or cron behavior)

cr0x@server:~$ ps aux --sort=-%cpu | head -10
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
www-data  21144  85.2  1.9 412300 38220 ?      R    09:14   3:12 php /var/www/example/wp-content/uploads/2025/12/cache.php
www-data  1055   4.1  0.7 225320 15400 ?      S    Dec26   0:40 php-fpm: pool www

Qué significa: PHP ejecutando un archivo dentro de uploads como proceso de larga duración es anormal. Típicamente es una web shell haciendo trabajo (spam, escaneo, minería de crypto, callbacks salientes).

Decisión: Contén inmediatamente (bloquea peticiones; pon fuera de línea el sitio), luego preserva evidencia. Matar el proceso está bien, pero no te quedes ahí—reemplaza el archivo y encuentra cómo llegó.

Task 7: Check outbound connections (is it calling home?)

cr0x@server:~$ sudo ss -tpn | head
State Recv-Q Send-Q Local Address:Port  Peer Address:Port  Process
ESTAB 0      0      10.0.0.12:52418    198.51.100.77:443  users:(("php",pid=21144,fd=12))
LISTEN 0     511    0.0.0.0:80         0.0.0.0:*         users:(("nginx",pid=812,fd=6))

Qué significa: Un proceso PHP haciendo conexiones TLS salientes suele ser exfiltración, C2 o envío de spam. También puede ser llamadas API legítimas, pero el PID apunta al proceso sospechoso.

Decisión: Bloquea egress saliente en el firewall de la instancia si es posible, al menos temporalmente, y luego procede con la reconstrucción/erradicación.

Task 8: Verify WordPress core integrity (without trusting the web UI)

cr0x@server:~$ cd /var/www/example && sudo -u www-data wp core verify-checksums
Warning: File should not exist: wp-includes/wp-tmp.php
Warning: File should not exist: wp-admin/css/colors/coffee/coffee.php
Error: Checksum verification failed for: wp-includes/version.php, wp-settings.php

Qué significa: Archivos inesperados en directorios core y fallos de checksum indican manipulación. Esto no es “solo un plugin malo”. Es más profundo.

Decisión: Reemplaza el core de WordPress completamente desde una fuente de confianza. No intentes editar línea por línea a menos que hagas forense.

Task 9: Enumerate admin users and look for sleepers

cr0x@server:~$ sudo -u www-data wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
+----+------------+----------------------+---------------------+
| ID | user_login  | user_email            | user_registered     |
+----+------------+----------------------+---------------------+
|  1 | editor-in-chief | eic@example.com  | 2021-04-12 10:22:19 |
| 42 | wp_support  | wp-support@proton.tld | 2025-12-26 09:10:03 |
+----+------------+----------------------+---------------------+

Qué significa: Cuentas admin nuevas creadas en el momento del compromiso son persistencia común. El dominio del email suele ser una pista, pero no te fíes de estereotipos—los atacantes usan direcciones que parecen normales también.

Decisión: Deshabilita/borra cuentas sospechosas después de preservar evidencia. Luego rota todas las contraseñas de admin e invalida sesiones.

Task 10: Dump and scan WordPress cron events for persistence

cr0x@server:~$ sudo -u www-data wp cron event list | head
+----------------------+---------------------+---------------------+----------+
| hook                 | next_run_gmt        | recurrence          | args     |
+----------------------+---------------------+---------------------+----------+
| wp_version_check     | 2025-12-27 02:10:00 | twice_daily         |          |
| wp_update_plugins    | 2025-12-27 02:12:00 | twice_daily         |          |
| wp_tmp_cache_refresh | 2025-12-26 09:20:00 | every_minute        |          |
+----------------------+---------------------+---------------------+----------+

Qué significa: Un hook no estándar que se ejecuta cada minuto es sospechoso, especialmente si tu sitio no tiene requisitos legítimos de ejecución de tareas programadas.

Decisión: Localiza quién registró ese hook (plugin/tema/mu-plugin) y elimínalo. Si no puedes atribuirlo con confianza, trátalo como malicioso hasta probar lo contrario.

Task 11: Identify mu-plugins and must-use persistence

cr0x@server:~$ ls -la /var/www/example/wp-content/mu-plugins
total 24
drwxr-xr-x 2 www-data www-data 4096 Dec 26 09:09 .
drwxr-xr-x 8 www-data www-data 4096 Dec 26 09:05 ..
-rw-r--r-- 1 www-data www-data 8120 Dec 26 09:09 security-update.php

Qué significa: Muchos sitios no usan mu-plugins en absoluto. Un mu-plugin creado recientemente es una ubicación de persistencia de alta señal porque se carga automáticamente.

Decisión: Cuarentena e inspecciona el archivo. Si es malicioso, ya tienes un probable mecanismo de persistencia y un ancla temporal para logs.

Task 12: Search for common obfuscation patterns (fast, not perfect)

cr0x@server:~$ sudo grep -R --line-number -E 'base64_decode\(|gzinflate\(|str_rot13\(|eval\(|preg_replace\(.*/e' /var/www/example/wp-content | head
/var/www/example/wp-content/uploads/2025/12/cache.php:1:
/var/www/example/wp-content/themes/example/functions.php:402:$a=gzinflate(base64_decode($b));

Qué significa: Estas funciones no son automáticamente maliciosas, pero en el mundo WordPress se usan desproporcionadamente para ocultar payloads. El hit en uploads es casi seguro malicioso.

Decisión: Trátalo como indicadores de compromiso. Reemplaza el tema/plugin desde una copia de confianza. No “borres solo la línea” y lo des por solucionado.

Task 13: Verify database for injected content and rogue options

cr0x@server:~$ sudo mysql -NBe "SELECT option_name FROM wp_options WHERE option_name IN ('active_plugins','siteurl','home')"
home
siteurl
active_plugins

Qué significa: Esto confirma que las claves de opción existen (normal). La comprobación real es el valor: ¿apuntan donde esperas y los plugins activos son coherentes?

Decisión: Si siteurl/home están modificados, puedes estar tratando con inyección de redirección o toma de control de admin. Si active_plugins incluye plugins desconocidos, eso es un vector de persistencia.

Task 14: Check for suspicious redirects in the database (conditional SEO spam)

cr0x@server:~$ sudo mysql -NBe "SELECT ID,post_title FROM wp_posts WHERE post_content LIKE '%document.location%' LIMIT 5"
1931  Summer Sale
2077  About Us

Qué significa: Redirecciones JavaScript en campos de contenido suelen impulsar spam SEO, redirecciones de afiliado o kits de explotación.

Decisión: Si las encuentras, planifica una pasada de limpieza de contenido (exportar, sanear, reimportar) e invalida caches/CDN. También asume que el atacante tuvo acceso de escritura a la BD o al admin.

Task 15: Confirm SSH access history and unexpected keys

cr0x@server:~$ sudo tail -50 /var/log/auth.log | egrep 'Accepted|Failed|session opened' | tail
Dec 26 09:08:14 server sshd[18422]: Accepted password for ubuntu from 203.0.113.44 port 53122 ssh2
Dec 26 09:08:15 server sshd[18422]: pam_unix(sshd:session): session opened for user ubuntu by (uid=0)

Qué significa: Un login SSH exitoso desde la misma IP que accede a tu web shell es una muy mala señal. Eso no es “WordPress se hackeó”, eso es “la máquina está comprometida”.

Decisión: Inicia reconstrucción completa del host. Rota todos los secretos que hayan tocado este host. Revisa logs de auditoría cloud por cambios de claves y snapshots.

Task 16: Confirm backups are not quietly compromised (spot-check a backup)

cr0x@server:~$ sudo tar -tf /backups/example/wp-files-2025-12-20.tar.gz | egrep 'wp-content/uploads/.*\.php|mu-plugins|wp-includes/wp-tmp' | head
var/www/example/wp-content/uploads/2025/12/cache.php
var/www/example/wp-content/mu-plugins/security-update.php

Qué significa: Si tu backup contiene los mismos artefactos maliciosos, restaurarlo resucitará el compromiso. Las copias de seguridad preservan la verdad, no la virtud.

Decisión: Encuentra la última copia conocida buena. Si no puedes, debes reconstruir desde fuentes limpias y migrar solo contenido validado.

Puntos de entrada comunes y cómo demostrarlos

No puedes decir “vulnerabilidad de plugin” porque suene plausible. Lo demuestras, o tratas el sistema como comprometido de múltiples maneras.

1) Credenciales de admin robadas

Cómo ocurre: reutilización de contraseñas, phishing, credential stuffing, sesión de navegador filtrada o laptop de un admin infectada.

Cómo probarlo:

  • Busca logins exitosos y acciones de admin alrededor del momento del compromiso en los access logs.
  • Revisa usuarios admin nuevos (Task 9).
  • Revisa sesiones de WordPress y contraseñas de aplicación si se usan.

Qué hacer: rota todas las credenciales de admin, fuerza cierre de sesión, habilita MFA y revisa quién tiene admin cuando no debería.

2) Plugin/tema vulnerable, o compromiso de la cadena de suministro

Cómo ocurre: plugin desactualizado con exploit conocido, o una actualización de plugin tomada de una fuente comprometida.

Cómo probarlo:

  • Compara archivos del plugin contra versiones conocidas buenas.
  • Revisa timestamps: un directorio de plugin modificado justo antes del compromiso es sospechoso.
  • Inspecciona logs para peticiones a endpoints específicos del plugin antes de que aparezca el primer archivo malicioso.

Qué hacer: elimina/reemplaza el plugin; fíjalo a fuentes de confianza; reduce el número de plugins como si fuera serio.

3) Sistema de archivos grabable + cadena RCE

Cómo ocurre: una vez que un atacante obtiene ejecución de código, escribe persistencia en archivos de WordPress, uploads o mu-plugins.

Cómo probarlo: PHP sospechoso en uploads (Task 2/5/6/12), mismatches de checksum (Task 8), mu-plugins inesperados (Task 11).

Qué hacer: cierra permisos de archivos; deshabilita ejecución PHP en directorios grabables; muévete a despliegues inmutables si es posible.

4) Cuenta de hosting / SSH comprometida

Cómo ocurre: contraseña SSH débil, clave filtrada, credenciales reutilizadas, token de API cloud robado.

Cómo probarlo: logs de auth muestran logins inesperados (Task 15), nuevas claves en ~/.ssh/authorized_keys, o uso inesperado de sudo.

Qué hacer: reconstruye el host, rota secretos, añade MFA al panel cloud, bloquea SSH, revisa IAM.

Fase 4: erradicación (eliminar al atacante de verdad)

La erradicación es donde la gente se vuelve imprudente. Borran la web shell que vieron, se sienten heroicos y siguen. Mientras tanto el atacante mantiene acceso vía un mu-plugin, un hook cron, un admin rogue y un inyector de base de datos. No erradicaste nada; hiciste un baile interpretativo en un sistema vivo.

Enfoque preferido: reconstruir en un host limpio

Si tienes la menor pista de compromiso a nivel host, reconstruye. Eso significa una nueva VM/contenedor, imagen OS fresca, paquetes parcheados, servicios mínimos, claves SSH limpias, acceso a BD con mínimo privilegio y una restauración que controles.

Sí, es más trabajo que “limpiar”. También es más rápido que jugar al whack-a-mole durante dos semanas.

Cuando debes limpiar en sitio (último recurso)

A veces la realidad muerde: no hay capacidad de repuesto, no hay automatización infra, no hay backup limpio, negocio no permite downtime. Si limpias en sitio, hazlo con disciplina:

  • Reemplaza el core de WordPress con una copia fresca (no parchees archivos individuales del core).
  • Reemplaza cada plugin y tema desde una fuente de confianza, o elimínalo.
  • Elimina archivos PHP desconocidos en uploads/cache/temp después de capturar evidencia.
  • Resetea todas las salts/keys de WordPress y todas las contraseñas de usuarios (al menos admins).
  • Rota credenciales de la base de datos y asegura que el usuario BD tenga mínimo privilegio.
  • Desactiva XML-RPC si no se necesita; aplica MFA para admins; restringe wp-admin por IP si es factible.

Dos áreas que la gente olvida: la base de datos y el scheduler

A los atacantes les encanta la persistencia que sobrevive al reemplazo de archivos. Eso suele ser: (1) opciones de WordPress, (2) tareas programadas, (3) usuarios admin, (4) contenido inyectado en posts, (5) cronjobs del servidor.

Fase 5: recuperación y restauración segura

Recuperación no es “el sitio carga”. Recuperación es “el sitio carga y podemos defender esa afirmación”. Un restore apropiado responde: qué restauraste, por qué lo confiamos, qué cambiamos y cómo detectaremos una recaída.

Estrategia de restauración que no te reinfecta

  1. Pon en pie un entorno limpio (nueva VM/contenedor, OS parcheado).
  2. Instala el core de WordPress desde una fuente confiable (misma versión que tu backup limpio, o actualizada si puedes validar compatibilidad).
  3. Restaura la base de datos desde la última snapshot conocida buena, luego inspecciona en busca de contenido/opciones inyectadas.
  4. Restaura uploads con cautela. Los uploads son donde se esconden backdoors. Copia solo medios; bloquea ejecución PHP; escanea por .php, .phtml, .phar y “imágenes” inesperadas que contengan PHP.
  5. Reinstala plugins y temas desde fuentes confiables, no desde el filesystem viejo.

Verifica antes de abrir las puertas

La verificación es por capas:

  • La verificación de checksums del core pasa.
  • No hay PHP en uploads (y el servidor lo bloquea de todos modos).
  • No hay usuarios admin inesperados.
  • Los logs muestran patrones de tráfico normales después de reabrir.
  • Las conexiones salientes son esperadas y mínimas.

Broma corta #2: “Lo ponemos en línea y monitoreamos de cerca” es el equivalente en respuesta a incidentes de “Empezaré a comer sano el lunes”.

Fase 6: endurecimiento que resiste al contacto humano

El endurecimiento no es una lista de buenas prácticas que pegas en un ticket y olvidas. Es el conjunto mínimo de guardarraíles que hacen que el próximo incidente sea más pequeño.

Permisos de archivos y modelo de despliegue

  • Haz el core de WordPress de solo lectura para el usuario web. Las actualizaciones deben ser una acción de despliegue, no un efecto secundario en tiempo de ejecución.
  • Uploads deben ser grabables, pero no ejecutables. Haz cumplir esto en el servidor web y, si es posible, en las opciones del montaje del sistema de archivos.
  • Prefiere despliegues inmutables (artefactos build, despliega, no edites en prod). Si eso es un salto grande, al menos restringe permisos de escritura.

Higiene de credenciales (la parte poco sexy)

  • Habilita MFA para administradores de WordPress y cuentas de proveedor de hosting.
  • Rota salts/keys de WordPress después de un incidente; invalida sesiones.
  • Usa contraseñas únicas y un gestor; evita enviar credenciales por email.
  • Desactiva cuentas no usadas y elimina usuarios “admin temporales” de proveedores.

Menor privilegio para acceso a la base de datos

El usuario de BD de WordPress normalmente no necesita privilegios globales. Necesita acceso a su propia base de datos y nada más. Si un atacante obtiene credenciales BD, limita el radio de daño.

Logging que ayuda en lugar de existir

  • Mantén logs web el tiempo suficiente para cubrir tu ventana de detección. Si te tomó 10 días notarlo, 2 días de logs es arte performativo.
  • Centraliza logs fuera del host si puedes. Los atacantes borran logs locales cuando son ordenados.
  • Alerta sobre picos extraños: intentos de login, hits a xmlrpc, POSTs a uploads, creación súbita de admins.

Backups: diseñalas como si las necesitaras

Las copias de seguridad deben ser inmutables (o al menos con control de acceso), probadas y separadas del entorno comprometido. La mejor copia de seguridad es la que el atacante no puede encriptar ni borrar.

Listas de verificación / plan paso a paso

Estas están escritas para personas que tienen que hacerlo bajo presión. Imprímelas si eres a la antigua. Cópialas en tu canal de incidentes si eres moderno.

Checklist A: primera hora (contención + preservación)

  1. Abre un canal de incidentes. Asigna un líder de incidente. Una persona decide.
  2. Contén: página de mantenimiento / bloqueo WAF / restringe wp-admin por IP.
  3. Bloquea egress saliente desde el host si es posible (temporal).
  4. Preserva: copia logs fuera del host (web, auth, PHP), captura lista de procesos y conexiones de red.
  5. Snapshot: snapshot del sistema de archivos y dump de BD (almacenamiento de solo lectura si es posible).
  6. Confirma alcance: ¿es un sitio, un host o múltiples tenants?

Checklist B: mismo día (triage + plan de erradicación)

  1. Ejecuta checksums del core y escanea en busca de archivos/funciones sospechosas.
  2. Enumera usuarios admin, eventos cron, mu-plugins.
  3. Decide: reconstruir host vs limpiar en sitio (por defecto reconstruir si se sospecha host comprometido).
  4. Identifica la última copia conocida buena (valídala para asegurarte de que no esté contaminada).
  5. Rota credenciales: hosting, claves SSH, usuarios BD, administradores WordPress, API keys usadas por plugins.
  6. Planea comunicaciones: quién necesita saber (legal, seguridad, clientes) según riesgo de exposición de datos.

Checklist C: recuperación y validación (antes de reabrir)

  1. Levanta un entorno limpio con OS parcheado y servicios mínimos.
  2. Instala core de WordPress limpio; instala plugins/temas desde fuentes confiables solamente.
  3. Restaura BD y uploads cuidadosamente; aplica “no PHP en uploads” en el servidor web.
  4. Verifica: checksums pasan; no hay admins rogue; no hay hooks cron sospechosos; logs lucen normales.
  5. Reabre gradualmente si es posible; vigila logs y conexiones salientes.
  6. Post-incidente: documenta la causa raíz (o la hipótesis mejor sustentada), añade monitorización, programa parches.

Errores comunes: síntoma → causa raíz → arreglo

Esta es la parte donde dejamos de fingir que todos tienen proceso perfecto. Estos son modos de fallo predecibles.

1) Síntoma: “Borramos el archivo malicioso, pero volvió.”

Causa raíz: Persistencia vía mu-plugin, evento cron, usuario admin rogue o compromiso a nivel host reescribiendo archivos.

Arreglo: Revisa mu-plugins (Task 11), eventos cron (Task 10), usuarios admin (Task 9). Si el host muestra SSH sospechoso o procesos extraños (Task 15/6), reconstruye.

2) Síntoma: “La página principal parece bien, pero Google muestra páginas de spam.”

Causa raíz: Spam SEO condicional servido a bots o inyectado en posts/opciones; a veces sólo visible con ciertos user agents.

Arreglo: Consulta la BD por scripts inyectados (Task 14), inspecciona functions.php del tema y busca lógica de cloaking. Limpia caches después.

3) Síntoma: “Los usuarios reportan redirecciones aleatorias.”

Causa raíz: JavaScript inyectado en contenido, plugin comprometido inyectando headers, o .htaccess/nginx modificado.

Arreglo: Haz diff de la configuración del servidor web, inspecciona contenido de la BD y reemplaza plugins/temas. Valida opciones home/siteurl (Task 13).

4) Síntoma: “wp-admin está lento y la CPU al máximo.”

Causa raíz: Tareas cron maliciosas, envío de spam, fuerza bruta o minería por PHP.

Arreglo: Inspecciona procesos (Task 6), conexiones (Task 7) y logs por picos (Task 4). Contén y reconstruye si hace falta.

5) Síntoma: “Restauramos desde backup y el hack volvió inmediatamente.”

Causa raíz: La copia ya estaba comprometida, o las credenciales seguían comprometidas y el atacante volvió a entrar.

Arreglo: Valida backups (Task 16), rota todos los secretos, aplica MFA y cierra el punto de entrada inicial antes de restaurar.

6) Síntoma: “El plugin de seguridad dice limpio, pero los navegadores aún advierten.”

Causa raíz: El escáner no detectó la ofuscación, o el contenido malicioso se sirve de forma condicional, o quedan activos assets maliciosos en caché.

Arreglo: Verificación manual: checksum del core (Task 8), escaneo de patrones sospechosos (Task 12), inspección de logs, purga de caches y re-chequeo desde un cliente limpio.

Tres mini-historias corporativas desde el frente

Mini-historia 1: el incidente causado por una suposición errónea

En una empresa mediana donde “marketing gestiona WordPress”, el equipo asumió que el compromiso se limitaba a WordPress porque el primer síntoma fue spam SEO. El sitio estaba detrás de un CDN, así que pusieron una página de mantenimiento y dijeron a liderazgo: “Restauraremos desde copia de seguridad”.

Restauraron archivos y BD en la misma VM. El spam desapareció. Por aproximadamente un día. Luego las redirecciones regresaron, pero solo para user agents móviles y en ciertos países. El equipo culpó al cache y pasó horas purgando capas que no necesitaban purgar.

La pista fue aburrida: conexiones salientes desde PHP a unas IPs en puerto 443, persistentes incluso con el sitio en modo mantenimiento. Una lista rápida de procesos mostró un proceso PHP ejecutando un archivo dentro de uploads. Eso debería haber sido imposible—salvo que su config de Nginx no bloqueaba ejecución PHP en uploads y su configuración de PHP-FPM enrutaba “try_files” de modo que aún se ejecutaba.

La suposición equivocada fue: “Si restauramos WordPress, estamos limpios”. No habían probado que el host estuviera limpio, ni habían rotado secretos. El atacante aún tenía una sesión admin y además una web shell. Jugaban a las damas contra alguien que podía volver cuando quisiera.

Reconstruyeron la VM, restauraron desde un backup validado y forzaron reinicios de contraseña para usuarios privilegiados. El incidente terminó. La acción del postmortem que más importó fue cultural: dejar de tratar WordPress como algo separado de “infra real”. Corre en un servidor. Los servidores se comprometen.

Mini-historia 2: la optimización que salió mal

Una organización más grande decidió “optimizar despliegues” permitiendo que WordPress autoupdatara plugins y temas directamente en producción. La razón era válida: menos tickets, parches de seguridad más rápidos, menos trabajo manual. La implementación fue el problema: hicieron todo el árbol de WordPress escribible por el usuario web para que las actualizaciones no fallaran.

Seis meses después, un endpoint de plugin vulnerable dio al atacante una entrada. Porque el sistema de archivos era escribible, el atacante no tuvo que ser sutil. Modificó archivos core, dejó un mu-plugin y plantó un backdoor en un archivo de tema que parecía código de analytics. El payload era pequeño y condicionalmente ingenioso.

La detección se demoró porque el sitio en general se comportaba bien. El atacante solo servía spam a bots y usó el servidor para enviar ráfagas de email a través de un mailer PHP comprometido. La primera alarma real fue la entregabilidad: sus emails transaccionales empezaron a caer en spam porque la reputación IP se desplomó.

Cuando respondieron, la limpieza fallaba porque el comportamiento de auto-actualización reintroducía archivos de plugin y enmascaraba la deriva. El sistema cambiaba continuamente, así que el equipo no podía distinguir cambios impulsados por el atacante de “automatización útil”.

La solución no fue “nunca actualizar”. La solución fue actualizar mediante despliegues controlados y mantener el runtime mayormente de solo lectura. Aún recibían parches rápidos, pero el atacante perdió la capacidad de reescribir la aplicación a voluntad.

Mini-historia 3: la práctica aburrida pero correcta que salvó el día

Una compañía con múltiples propiedades WordPress hizo algo poco glamoroso: centralizó logs y mantenía backups inmutables. No perfecto, no sofisticado, solo consistente. Logs web enviados fuera del host, backups de BD write-once con retención y restauraciones practicadas trimestralmente porque alguien en ops fue obstinado en el mejor sentido.

Cuando un sitio empezó a redirigir, lo contuvieron rápido en el CDN. Luego sacaron logs de la tienda central y detectaron una ventana estrecha: una ráfaga de POSTs a un endpoint de plugin, seguida por la creación de un nuevo admin, seguida por peticiones a un archivo PHP en uploads. Limpio, lineal, feo.

El equipo no discutió si “realmente estaba comprometido”. Tenían evidencia. Reconstruyeron el host usando una pipeline de automatización existente. Restauraron desde un backup tomado antes de la primera petición explotadora. Rotaron secretos y desactivaron XML-RPC porque el sitio no lo necesitaba. Tiempo total de inactividad medido en horas, no días.

Después, liderazgo preguntó lo de siempre: “¿Cómo se movieron tan rápido?” La respuesta no fue heroica. Fue la práctica aburrida de tener logs que sobrevivieron al atacante y backups que no pudieron ser sobreescritos silenciosamente.

No fue glamoroso. Fue correcto. Y eso es el mayor elogio en operaciones.

Preguntas frecuentes

1) ¿Debo sacar el sitio inmediatamente fuera de línea?

Si el sitio está activamente sirviendo malware, redireccionando usuarios o filtrando datos: sí. Contén en el borde si es posible para no destruir evidencias en el host.

2) ¿Puedo simplemente reinstalar el core de WordPress y listo?

Reinstalar el core ayuda, pero rara vez es suficiente. Los compromisos suelen incluir administradores rogue, persistencia en cron, plugins/temas modificados e inyecciones en la base de datos. Verifica todas las capas.

3) ¿Los plugins de seguridad arreglan un sitio hackeado?

Pueden detectar y a veces eliminar patrones conocidos. También pueden dar falsamente confianza. Úsalos como insumo, no como veredicto. Aún necesitas logs, checksums y endurecimiento de permisos.

4) ¿Cómo sé si el servidor está comprometido?

Indicadores: logins SSH inesperados, procesos desconocidos, conexiones salientes sospechosas, nuevos usuarios del sistema o manipulación fuera del directorio WordPress. Si lo sospechas, reconstruye.

5) ¿Cuál es el mecanismo de persistencia más común en compromisos WordPress?

Usuarios admin rogue y backdoors PHP en directorios grabables (uploads, cache, mu-plugins). A los atacantes les gusta la persistencia que sobrevive a cambios de tema.

6) ¿Las copias de seguridad son seguras para restaurar?

Sólo si las validas. Revisa muestras por artefactos sospechosos y confirma que la copia es anterior al compromiso. Además rota credenciales; de lo contrario puedes ser re-comprometido de inmediato.

7) ¿Debo rotar credenciales de BD y salts de WordPress?

Sí. Tras un compromiso, asume que credenciales y cookies pueden haber sido robadas. Rota credenciales BD, regenera salts de WordPress y fuerza re-autenticación para usuarios—al menos admins.

8) ¿Por qué los hacks “solo aparecen” en Google o ciertos países?

Cloaking. Los atacantes sirven contenido limpio a ti y contenido malicioso a bots o agentes específicos para evitar detección. Por eso usas logs y checks de integridad, no sólo una prueba en navegador.

9) Si reconstruyo el host, ¿aún necesito forense?

Algo de forense sí. Reconstruyes para obtener limpieza. Haces forense para prevenir recurrencias y evaluar exposición. Mínimo: identifica punto de entrada, ventana temporal y datos afectados.

10) ¿Cuál es la vía más rápida y segura para una pequeña empresa sin equipo SRE?

Contén (página de mantenimiento), consigue un host limpio (WordPress gestionado o nueva VM), restaura desde la última copia conocida buena, reemplaza plugins/temas desde fuentes confiables, rota credenciales y añade MFA.

Conclusión: qué hacer la próxima semana, no solo hoy

Hoy, tu trabajo es contención, preservación de evidencias y un camino de recuperación limpio. Sé decisivo: si el host parece comprometido, reconstruye. Si las copias parecen infectadas, deja de fingir que tienes un plan de restauración y empieza a construir uno limpio.

La próxima semana, haz el trabajo que prevenga la secuela:

  • Haz el core de WordPress de solo lectura en tiempo de ejecución; bloquea ejecución PHP en uploads.
  • Reduce plugins/temas; mantén una cadencia de parches que realmente puedas sostener.
  • Centraliza logs fuera del host; mantenlos el tiempo suficiente para igualar la realidad de detección.
  • Prueba restauraciones; guarda backups de forma inmutable; documenta criterios de “última copia conocida buena”.
  • Aplica MFA y menor privilegio en todas partes: WordPress, hosting, SSH, base de datos.

Si lo haces bien, el próximo momento de “WordPress hackeado” será una molestia contenida, no una crisis de una semana con daño reputacional. Ese es el listón.

← Anterior
Debian 13: Nginx devuelve de repente 403/404 — permisos vs configuración, cómo identificar al instante
Siguiente →
Dimensionamiento del ARC de ZFS: cuando demasiada caché ralentiza todo lo demás

Deja un comentario