La puerta trasera que sobrevivió a una limpieza de malware: 14.000 carpetas y un falso acme-challenge
Una cuenta de hosting compartido se reinfectó en menos de 30 segundos, cinco días después de una limpieza completa. La vía de vuelta estaba donde ningún escaneo miraba.

En pocas palabras: cuando una web WordPress se reinfecta pocos días después de una limpieza, casi siempre se ha quedado sin cerrar una vía de entrada. En este caso eran cuatro shells PHP escondidas en un árbol de unas 14.000 carpetas que imitaba una ruta de validación de Let’s Encrypt. Los escaneos por patrones las pasaron por alto. Leer los registros de acceso en el minuto exacto de la reinfección mostró por dónde volvió a entrar el atacante.
El escenario
Una cuenta de hosting compartido con 16 instalaciones de WordPress y unas 40 webs más, todas ejecutándose con el mismo usuario del sistema. Ese último detalle importa: en una cuenta compartida, una shell en cualquier web puede escribir en todas las demás. Una web limpia junto a una infectada no se mantiene limpia mucho tiempo.
Cronología
- 4 y 5 de octubre: el atacante crea una carpeta llamada
well-known/acme-challenge(sin el punto inicial) dentro de una web y la llena de carpetas llamadas de laaa lah, con cuatro niveles de profundidad. Más de 9.000 carpetas en una copia y 4.600 en una segunda copia dentro dewp-content. Cuatro de ellas contienen shells PHP. - 7 de octubre: una limpieza grande. 408 carpetas de web shells eliminadas en toda la cuenta, administradores falsos borrados y núcleo de WordPress reinstalado. El laberinto se queda, porque nada en él coincidía con lo que buscábamos.
- 7 de octubre, 15:44: aparecen nuevos administradores falsos en tres webs, pocas horas después de la limpieza.
- 12 de octubre, 09:37:45: una petición desde una IP externa a una shell dentro del laberinto.
- De 09:38:01 a 09:38:27: el servidor empieza a llamar a shells recién escritas en unas 50 webs, cada una con un parámetro
?pass=para comprobar que funciona. Toda la cuenta queda reinfectada en menos de 30 segundos.
Lo que dejó el atacante
- Cadenas de tres carpetas con nombres aleatorios de siete caracteres, cada una con un
index.phpofuscado, untool.phpde 220 KB y unphp.ini, todos en solo lectura. - Treinta archivos
.htaccessidénticos dentro dewp-contentymu-pluginsque permiten de forma explícita ejecutar PHP ahí. - Un gestor de archivos web en la raíz de una web, con un nombre aleatorio.
- El
wp-login.phpde esa misma web renombrado, para que el propietario no pudiera entrar. - Un script PHP de envío de correo para mandar spam desde la cuenta.
- Dieciséis cuentas de administrador falsas en cinco webs, y más de 200 entradas de spam de apuestas en una de ellas, publicadas en cuatro idiomas.
Por qué la primera limpieza no lo encontró
El primer escaneo buscó lo que ya habíamos visto: las cadenas de carpetas de siete caracteres, nombres de archivo conocidos y firmas de código conocidas. El laberinto no coincidía con nada de eso. Sus carpetas tenían nombres de una letra y la ruta parecía creada por una herramienta de certificados. Estuvo quieto una semana y se usó una sola vez.
La lección es buscar por comportamiento, no por nombre:
- Archivos PHP en carpetas que nunca deberían contener PHP (uploads, upgrade, cualquier cosa que imite un
.well-known). - Archivos PHP modificados hace poco en cualquier sitio fuera de las carpetas del núcleo, los plugins y los temas.
- Peticiones en el registro de acceso a archivos
.phpfuera dewp-admin,wp-includesy los plugins y temas activos.
El orden de limpieza que funciona en una cuenta compartida
- Neutraliza todo a la vez. Movimos 340 carpetas de shells y los dos laberintos a una cuarentena fuera de la raíz web en una sola pasada. Limpiar web por web deja que las demás vuelvan a infectar lo que ya está limpio.
- Elimina los archivos que vuelven a habilitar PHP. Los 30
.htaccessmaliciosos eran idénticos byte a byte, así que comparar hashes encontró todas las copias. - Bloquea PHP donde nunca debe estar. En
wp-content/uploadsywp-content/upgradede todas las webs:<FilesMatch "\.(php|phtml|php[0-9]|phar)$"> Require all denied </FilesMatch>Compruébalo: un archivo PHP colocado ahí tiene que ser rechazado mientras las imágenes siguen cargando.
- Verifica el núcleo de WordPress con los checksums oficiales de
api.wordpress.org. Aquí las 16 instalaciones estaban intactas, lo que nos dijo que el atacante trabajaba desde fuera del núcleo. - Elimina los administradores falsos. Los nuestros tenían nombres como
admin_seguido de diez caracteres hexadecimales, usuarios hexadecimales aleatorios, una dirección[email protected]y fechas de registro en 1970. Guarda una copia de cada registro antes de borrarlo. - Cambia todas las credenciales que el atacante pudo leer: contraseñas de las bases de datos y salts de todas las webs, la contraseña del panel del hosting y las cuentas FTP.
- Vuelve a escanear tras unas horas de tráfico real y ejecuciones de cron. La reinfección aparece ahí, no en el escaneo que haces justo después de limpiar.
Preguntas que nos hacen sobre esto
¿Por qué vuelven las shells después de borrarlas?
Porque queda al menos una vía de entrada: una shell que no encontraste, una cuenta de administrador que no eliminaste o una contraseña filtrada. Borrar los archivos visibles sin cerrar el camino de vuelta solo pone el reloj a cero.
¿Basta con un plugin de seguridad?
Ayuda con las firmas conocidas. No ve una shell que se ejecuta desde una carpeta que el plugin no escanea, y no puede proteger una web de otra web de la misma cuenta de hosting.
¿Tengo que cambiar la contraseña de la base de datos si nadie inició sesión?
Sí. Una shell puede leer wp-config.php sin iniciar sesión en nada. Si se ejecutó una shell en tu cuenta, da por hecho que las credenciales de la base de datos se copiaron.