Cadeado escuro preso a uma porta — ilustração de segurança para case anonimizado
Case· Portal de notícias (case anonimizado)

El backdoor que recibía órdenes desde una blockchain: recuperación de un portal WordPress comprometido

Un portal de noticias con nueve años de archivo llegó con el sitio "desconfigurado". No era desconfiguración: era una intrusión activa, con un backdoor que se escondía del administrador y consultaba un contrato en la blockchain Polygon para descubrir de dónde bajar el payload.

Este case se publica de forma anonimizada, sin nombre del cliente y sin dominio. Se trata de un portal de noticias con nueve años de operación y cerca de 36 mil posts publicados, alojado en servidor propio. El pedido original era simple: recuperar el acceso al panel y arreglar un sitio que estaba "desconfigurado".

La auditoría cambió el diagnóstico enseguida. No era desconfiguración accidental. Era una intrusión activa, con un atacante que todavía tenía acceso al servidor en el momento en que empezamos a mirar.

Cómo se distingue una intrusión de un bug

La primera etapa fue de solo lectura — ninguna alteración en el servidor hasta tener el cuadro completo y el backup listo. Eso importa: en un entorno comprometido, empezar a tocar antes de entender destruye la evidencia de que el problema ya existía antes de que llegaras.

Lo que encontró la lectura: una cuenta de administrador falsificada en la base, creada semanas antes; un plugin backdoor activo en la lista de plugins de WordPress; copias del mismo código sueltas fuera de la carpeta de plugins, nombradas con timestamp; y decenas de carpetas con nombres genéricos plausibles — analytics, security, slider, widget — todas creadas en la misma ventana de una hora, señal clara de ataque automatizado en masa.

Una de esas copias había sido creada en la madrugada del propio día de la auditoría. El acceso del atacante no era histórico: era corriente.

La causa raíz estaba en el log, no en la suposición

La hipótesis inicial más obvia era una falla conocida en un plugin de gestión de archivos instalado en el sitio. Fue descartada por verificación: la versión instalada era actual y la vulnerabilidad conocida de ese plugin había sido corregida años antes.

La respuesta real vino del log de acceso del servidor. En un intervalo de segundos, una misma dirección hizo varias peticiones de login alternando el identificador de navegador entre Chrome, Safari y Firefox — firma de script automatizado — y recibió respuesta de redirección de login exitoso más de una vez. Entrada por fuerza bruta directo en la pantalla de login de WordPress, contra un sitio sin ningún límite de intentos activo.

Descartar la hipótesis conveniente por verificación, y no por intuición, es lo que separa remediar de solo limpiar lo que está visible.

El hallazgo más grave: un backdoor que se escondía del administrador

Entre los artefactos había un plugin falso con nombre imitando uno de los plugins de seguridad más populares de WordPress. La elección del nombre no es accidente: en un panel con decenas de plugins, un ítem llamado "seguridad" es el último en el que alguien hace clic para investigar.

Ese plugin inyectaba un script ofuscado en base64 con XOR en las páginas del sitio. E hizo algo que vale destacar: antes de inyectar, verificaba si el visitante era administrador con sesión iniciada. Si lo era, el payload se saltaba. El dueño del sitio podía navegar por su propio portal todo el día sin ver nunca el código malicioso que recibían todos los demás visitantes.

Decodificado, el script reveló la parte poco común. No tenía la dirección del servidor malicioso embebida. En cambio, consultaba un smart contract en una blockchain pública — probando varios proveedores de acceso hasta que uno respondiera — y leía de ahí desde qué dominio debía cargar el siguiente script remoto.

Por qué esto importa

Esta técnica se conoce como EtherHiding, e invierte la lógica de defensa. En un backdoor común, la dirección del servidor del atacante está en el código: bloqueas el dominio, o el proveedor da de baja el servidor, y la infraestructura maliciosa muere.

Cuando la dirección viene de un contrato en blockchain, el atacante puede cambiar el destino en cualquier momento sin volver a tocar el sitio comprometido. Y el registro en sí no puede ser removido por terceros — no hay proveedor a quien notificar, no hay host que dar de baja. Bloquear por dominio o IP se vuelve un juego que la defensa no gana. El único camino es remover el inyector en el sitio.

El plugin fue removido del disco y retirado de la lista de plugins activos en la base.

El enlace invisible en la página inicial

Otro hallazgo explicaba un síntoma que el cliente ya había notado sin saber nombrarlo: la descripción que aparecía en Google y en las redes sociales al compartir la home no era el texto del sitio.

El contenido de la página inicial en la base había sido sustituido por un único enlace posicionado fuera del área visible de la pantalla, con texto ancla de marca de casino apuntando a un dominio externo. Invisible para cualquier visitante humano, perfectamente legible para los buscadores — inyección de enlace para SEO, usando la autoridad acumulada en nueve años del portal para empujar un sitio de terceros.

Removido, con revisión de todas las páginas del sitio para confirmar que era caso único.

291 posts de spam, y por qué limpiar fue la parte difícil

El atacante también había publicado cientos de posts de spam de apuestas en varios idiomas — inglés, ruso, polaco, checo, turco, azerbaiyano. Aparecían incluso en el widget de "más populares" de la home, porque el tráfico de bots inflaba las métricas del propio sitio.

Limpiar eso en un archivo de 36 mil posts legítimos es donde vive el trabajo real. Aparecieron tres trampas, y cada una exigió cambiar el criterio.

Filtrar por fecha no funcionaba: parte de los posts de spam tenía fecha retroactiva falsificada, anterior al ataque. Filtrar por palabra clave tampoco bastaba: las siguientes tandas venían en cirílico, en grafía polaca, con nombres de marca que no contenían ninguno de los términos buscados. Y los términos genéricos generaban falsos positivos en contenido real — había artículos legítimos sobre bandas, hoteles y eventos que mencionaban la palabra en contexto verdadero.

El criterio final fue estructural, no textual: el rango de ID secuencial donde comenzó la contaminación, cruzado con la autoría del post. Todo listado se generó en archivo y se revisó línea por línea antes de cualquier remoción — nunca un filtro alimentando directamente un comando de exclusión masiva.

Total removido: 291 posts. Cero pérdida de contenido legítimo, confirmada incluso en los casos ambiguos que mencionaban términos de casino en contexto real. La verificación final se rehízo inmediatamente antes de declarar concluido — y fue exactamente ese reconteo el que reveló una última tanda de 32 posts que había escapado de todas las verificaciones anteriores.

En una base grande, "verifiqué hace dos horas" no es verificación. El conteo final tiene que rehacerse en el momento de cerrar.

La corrección que casi nadie prueba: ¿la protección protege de verdad?

Removido el malware, quedaba la causa raíz: el login sin límite de intentos. El sitio tenía dos plugins de protección de login instalados y activos, lo que en la mayoría de las auditorías se registraría como "protegido, ítem resuelto".

Lo probamos de verdad. Cinco intentos de login incorrectos seguidos pasaron sin ningún bloqueo. La protección estaba activa en el panel e inerte en la práctica — es decir, después de toda la limpieza, la puerta por la que entró el atacante seguía exactamente igual.

La corrección se hizo una capa más abajo, directo en el servidor web: límite de peticiones por dirección en la ruta de login, con respuesta de bloqueo por encima del límite. Validado con ocho peticiones simultáneas, con el bloqueo ocurriendo como se esperaba y el login legítimo siguiendo normal.

Se registró una advertencia en la documentación de entrega: ese archivo de configuración es generado por un panel de administración del servidor y puede ser sobrescrito si alguien lo regenera por la interfaz. Backup del archivo guardado y la salvedad entregada por escrito, porque una protección que puede desaparecer sin aviso necesita estar documentada.

Qué más se hizo, y qué quedó abierto

Además de la remediación: cuenta de administrador falsificada removida, contraseñas de base y de panel rotadas, permisos de archivo corregidos en el servidor — incluido un archivo de configuración con permiso de escritura para cualquier proceso — y cerca de 7,5 GB de backups antiguos abandonados removidos para aliviar el disco, que estaba al 95% de uso.

Ajustes de capacidad en PHP y en la base resolvieron imágenes que no cargaban y redujeron a la mitad el tiempo de carga de la home. No resolvieron todo: el servidor sigue subdimensionado para el tráfico real del portal, y eso se comunicó como decisión de capacidad a tomar, no como ítem entregado.

Quedó una pendencia honesta: un fragmento de texto inerte todavía aparece una vez en el pie de la home. No es enlace, no es script, no redirige y no recoge nada. La investigación no localizó el origen — no está en ningún archivo del sitio, no está en ninguna columna de texto de la base, no es caché y no es reescritura del servidor web. Se registró como ítem de fase siguiente, con escáner dedicado, en vez de ser declarado resuelto.

Qué enseña este tipo de proyecto

Un sitio comprometido rara vez tiene un problema. Tiene una cadena: una puerta abierta, un atacante que entró por ella, la persistencia que instaló para volver, y el daño que monetizó. Remover el malware sin cerrar la puerta es una invitación a la segunda visita.

Y vale la nota sobre responsabilidad: cuando un sitio con registro de usuarios es comprometido, la evaluación sobre el deber de comunicación a los titulares y a la autoridad de protección de datos es del controlador — el dueño del sitio. Nuestro papel es informar el hallazgo con claridad y por escrito, documentar qué se encontró y cuándo, y no decidir eso en su lugar.

Alcance y stack

WordPress · PHP · MySQL · nginx · Linux · WP-CLI · análisis de log de acceso · rate limiting en el servidor web · rotación de credenciales · corrección de permisos