Hace poco empecé a llevar el SEO de una web nueva. Lo primero que hago siempre es rastrearla entera con Screaming Frog para ver de dónde parto: enlaces rotos, redirecciones, títulos duplicados, lo normal. Esta vez el informe de enlaces salientes de la portada tenía una cosa que no cuadraba: 27 enlaces follow a 27 dominios distintos de casinos y apuestas online. Con anclas del tipo «casino free $20» o «casino en direct», y frases en inglés, francés, neerlandés y hasta finés, en la web de un negocio local que no tiene nada que ver con nada de eso.
Abrí la portada en el navegador y estaba perfecta. Ni rastro de casinos. Abrí el código fuente y ahí estaban todos.

Esto tiene nombre: inyección de enlaces spam, o spam link injection si lo buscas en inglés. Alguien mete enlaces en tu web sin tu permiso para aprovechar tu reputación en Google y posicionar las suyas. Google lo recoge en sus políticas contra el spam como contenido pirateado, en concreto como inyección de contenido: modificar páginas existentes añadiendo enlaces o texto ocultos con CSS o HTML. Y el truco de esconderlos también está en la lista negra de Google, en el apartado de abuso de texto oculto y enlaces , que lo pone casi con estas palabras: «usar CSS para incluir texto fuera de la pantalla».
No es una técnica nueva: lleva años en esa lista. Lo que me pareció que valía la pena contar de este caso no es la técnica, es otra cosa: el sitio ya había pasado por dos limpiezas, y el spam siguió ahí tres semanas. Por qué pasó eso es lo más útil de todo el artículo.
Las preguntas, en corto
¿Qué era? Un bloque HTML en la portada, dentro de un <div> con position: absolute; left: -48592px, lleno de enlaces a casinos. Fuera de la pantalla para las personas, dentro del HTML para los buscadores. Además, el atacante había publicado 35 entradas de blog con más enlaces del mismo tipo, y algunas llegaron a indexarse en Google.
¿Cómo entraron? Con una contraseña válida de un administrador. Sin fuerza bruta, sin vulnerabilidad: acertaron al primer intento.
¿Por qué volvió después de limpiar? Porque la primera limpieza borró el plugin malicioso pero no cambió la contraseña. Dos horas después el atacante estaba dentro otra vez, y al día siguiente instaló una puerta trasera que no depende de ninguna contraseña. Cuando se cambiaron las contraseñas, días después, ya no servía de nada.
¿Cómo lo miro en mi web? Busca en Google site:tudominio.com + casino, mira el código fuente de tu portada (no la portada) y revisa Search Console. Lo detallo más abajo.
¿Qué hago después de limpiar? Cambiar todas las contraseñas en el mismo momento en que limpias, cerrar todas las sesiones, regenerar las claves de wp-config.php, doble factor para todos los administradores y pedirle a Google que revise el sitio. Y buscar los restos, que los hay.
Cómo entraron: una contraseña que ya tenían
Con los logs de acceso del servidor se puede reconstruir casi todo. Para no dar fechas que identifiquen el sitio, lo cuento en días desde el primer acceso:
| Día | Qué pasó |
|---|---|
| 0 | Dos inicios de sesión válidos como administrador, desde dos IPs distintas, en el mismo segundo. Ni un intento fallido antes |
| 2 a 49 | Cientos de intentos, desde 443 IPs distintas, de abrir la pantalla de «Subir plugin». El cortafuegos del hosting los bloqueó todos |
| 15 a 17 | Accesos por XML-RPC con la contraseña correcta: el atacante comprueba que sigue funcionando |
| 21 | Entra, sube y activa un primer plugin malicioso |
| 28 | Primera limpieza. Alguien localiza el plugin y lo borra. La contraseña no se cambia |
| 28, dos horas después | El atacante vuelve a entrar por XML-RPC. Mete en la portada un enlace de prueba oculto y sube una imagen de 1×1 píxel. Está comprobando si le siguen abriendo la puerta |
| 29 | Instala un segundo plugin falso, mejor camuflado. Veinte minutos después, desde otra IP, publica el primer bloque de spam en la portada. Otros veinte minutos después, desde otra, el segundo |
| 28 y 29 | Publica 35 entradas de blog con enlaces de casino |
| 30 y 31 | Segunda limpieza. Se instala un plugin de seguridad, se actualiza todo, se borran las 35 entradas y se bloquea XML-RPC. Se cambian las contraseñas. Nadie ve la portada ni el segundo plugin |
| 31 a 51 | El spam sigue en la portada y la puerta trasera sigue instalada |
| 51 | Lo encuentro en la auditoría SEO |
Lo de acertar al primer intento, desde IPs residenciales que cambian en cada acceso (una red de proxies), apunta a una contraseña filtrada. Hay tres caminos típicos para eso: que se usara la misma contraseña en otro servicio que sufrió una fuga, que un programa espía la robara del navegador de alguno de los ordenadores desde los que se entra a la web, o un correo de phishing. Con los datos que había no se puede saber cuál fue. Lo que sí se sabe es que no hizo falta romper nada.
Y un detalle que me hizo gracia, más o menos. El servidor tenía Imunify360, el cortafuegos del hosting bloqueó cientos de intentos de subir plugins durante semanas, y desde la segunda limpieza la web tenía además el cortafuegos de Wordfence. Y aun así, ¿para qué? El atacante subió dos plugins, y la puerta trasera convivió tres semanas con Wordfence sin que nadie se enterara. No es que esas herramientas sean malas: es que un cortafuegos filtra tráfico sospechoso, y un administrador que entra con su contraseña correcta no parece sospechoso. Parece un administrador.
Por eso creo que la seguridad de WordPress debería poner mucho más peso en el escaneo periódico de lo que ya está dentro (código inyectado, malware, puertas traseras, cambios en la base de datos que nadie ha hecho) y no tanto en el cortafuegos de la entrada. Con una contraseña filtrada y un login legítimo, la entrada no te va a salvar. Lo que te salva es enterarte pronto de lo que han dejado.
Por qué volvió: el hueco de dos horas
Esta es la parte que más me interesa que se entienda, porque es un error muy fácil de cometer.
La primera limpieza hizo lo que parece lógico: encontró el fichero malicioso y lo borró. Pero el atacante no había entrado por ese fichero. Había entrado por la puerta principal, con usuario y contraseña, y el fichero era solo lo que dejó dentro. Borrarlo sin cambiar la contraseña es como cambiar la cerradura del cajón y dejar puesta la llave de la puerta de casa.
Dos horas después de la limpieza estaba dentro otra vez. Y esta vez no se conformó con volver a subir lo mismo: dejó algo que no depende de ninguna contraseña. Cuando dos días después se cambiaron todas, el atacante ya no las necesitaba. De hecho no volvió a iniciar sesión nunca más. No le hacía falta: el spam ya estaba publicado y la puerta de atrás seguía abierta.
El orden importa: primero se cortan los accesos y después se limpia. Contraseñas, sesiones y claves, antes o como muy tarde en el mismo momento en que borras el primer fichero. Nunca días después.
La puerta trasera: un plugin que no pedía contraseña
El segundo plugin se llamaba Kizora Assets Extra. Se presentaba como una herramienta para equipos editoriales («publish and edit posts, update existing posts, and keep your editorial workflow moving»), con un autor inventado, «Plenupla Layouts», y un número de versión creíble, 4.9.23.17. En la lista de plugins del panel pasaba perfectamente por uno más.
Por dentro eran 11 ficheros y unas 2.700 líneas de PHP, la mayoría relleno sin sentido para despistar a los antivirus. Cosas así, repartidas por todas partes:
$route = intval( 'dy' );
try {
$global_stream = $route + 862;
} catch ( Exception $vector ) {
$global_stream = 0;
}
Eso no hace nada. Está ahí para que el código parezca otra cosa y para que las firmas de los escáneres no casen. Funcionó: ningún escáner de servidor lo marcó.
Lo que sí hacía, quitando el relleno:
- Abre una ruta propia en la API REST de WordPress (
/wp-json/z6km7cu/v1/mf/zk/19) que acepta órdenes sin iniciar sesión. - Las órdenes van firmadas. Cada petición lleva una firma Ed25519 en la cabecera
X-WPP-Signature, y el plugin la comprueba contra una clave pública que guarda en un fichero aparte. Sin la clave privada del atacante, esa ruta no hace nada. Y la firma incluye el dominio del sitio, así que una orden firmada para una web no sirve en otra. - Se esconde del índice de la API. Engancha el filtro
rest_indexpara quitar su ruta del listado de/wp-json/. Si miras qué rutas tiene tu API, no aparece. - Crea y modifica contenido, incluido el
_elementor_datade las páginas. Y no solo para Elementor: el código tiene casos para Gutenberg, el editor clásico, Divi, Oxygen y WPBakery. - Puede instalar y activar otros plugins desde una URL.
O sea, no es un script de aficionado. Es un producto: una plataforma para colocar enlaces en webs ajenas a escala, con soporte para los maquetadores más usados y firma criptográfica para que nadie más pueda usar tu puerta trasera. En los logs se ve que el atacante lo instaló y en el segundo siguiente hizo la primera llamada a esa ruta, para dar de alta la web en su sistema. Es la única llamada a esa ruta que aparece en los logs: la puerta quedó abierta, esperando.
Dónde se esconde el spam aunque borres el bloque
Quitar los dos bloques de la portada fue lo fácil. Lo que casi nadie cuenta es que el spam se queda copiado en sitios que no ves, y algunos de ellos se siguen sirviendo o siguen avisando a buscadores:
- Revisiones de la página. WordPress guarda una copia cada vez que se edita. Había 14 revisiones de la portada con spam o con el enlace de prueba. Si alguien restaura una versión anterior «para deshacer», vuelve el spam.
- Caché del maquetador. Elementor genera CSS y datos en caché. Hay que regenerarla y purgar también la caché de página del servidor.
- El índice de enlaces del plugin SEO. Rank Math, por ejemplo, guarda en una tabla propia todos los enlaces de cada entrada. Las 35 entradas de spam llevaban semanas borradas y sus enlaces seguían ahí.
- El registro de IndexNow. Aquí va una que me gustó poco: el plugin SEO, al publicarse cada entrada de spam, avisó él solito a los buscadores de que había contenido nuevo. IndexNow lo usan Bing, Yandex y otros (Google no), y en su registro quedaban 40 URLs de spam notificadas.
- Categorías y etiquetas. El atacante creó una categoría «Nuevos Casinos Online» y una etiqueta «Casinos Nuevos España». Al borrar las entradas se quedaron vacías, pero existían.
- Las cadenas de traducción. En una web multiidioma, el plugin de traducciones registra los textos de cada bloque. Y también se había apuntado el plugin falso en su lista de componentes.
- Opciones sueltas. La lista de plugins activados recientemente, la de ficheros editados recientemente (con el primer plugin malicioso dentro) y una caché del tamaño de las carpetas que todavía listaba la del plugin falso.
Nada de esto es peligroso por sí solo una vez borrado el plugin. Pero si lo que quieres es saber si el sitio está limpio, buscar el nombre de los dominios de spam o el del plugin en toda la base de datos es la prueba buena, no mirar la portada.
Cómo puedes mirarlo en tu web
Esta clase de spam está diseñada para que el dueño no lo vea. La portada se ve bien, las visitas no notan nada, y los plugins de seguridad no siempre lo detectan porque no hay ningún fichero sospechoso: el spam vive en la base de datos, dentro del contenido de una página. Así que hay que mirar como mira Google.
1. Pregúntale a Google con site:
Escribe esto en el buscador de Google:
site:tudominio.com + casino
site:tudominio.com + apuestas
site:tudominio.com + bet
El operador site: le dice a Google que solo te enseñe resultados de tu dominio, y lo que va detrás filtra por esa palabra. Yo le pongo el + por costumbre; hoy Google lo ignora, así que funciona igual sin él. Si te salen páginas con esas palabras que tú no has escrito, tienes spam indexado. Prueba también con site:tudominio.com a secas y ve a las últimas páginas de resultados, que es donde suelen acabar las URLs raras. Cambia las palabras según lo que te preocupe: farmacia, réplicas, préstamos, o caracteres japoneses o chinos si tu web está en español.
2. Mira el código fuente de tu portada, no tu portada
Abre tu web, pulsa Ctrl + U (o clic derecho, «Ver código fuente») y busca con Ctrl + F:
casino,bet,slot, o las palabras de tu sector de spam favorito.left:-oleft: -seguido de un número enorme.display:noneofont-size:0cerca de un enlace que no reconozcas.data-wp-poster, que es el marcador que dejaba este plugin.
Si tu web está detrás de una caché o un sistema anti-bots, hazlo desde un navegador normal, no con curl: puede que el HTML que te devuelva no sea el real.
3. Search Console
- Rendimiento, filtrando por consultas: si ves búsquedas que no tienen nada que ver contigo, Google te está asociando con ellas.
- Páginas: URLs indexadas que no reconoces.
- Problemas de seguridad y Acciones manuales: si Google ya lo ha detectado, te lo dice aquí. Enseguida vuelvo a esto.
4. Pasa un rastreador
Screaming Frog es gratis hasta 500 URLs, que para la mayoría de webs pequeñas es la web entera. Rastrea y mira los enlaces salientes externos, sobre todo los de la portada. En mi caso fue exactamente así como salió.
5. Revisa los plugins que no conoces
Por cada plugin instalado, comprueba que existe en wordpress.org/plugins/nombre-del-plugin/. Los dos plugins de este caso no existían en el repositorio oficial. Uno con nombre genérico, autor que no te suena y sin enlace a «Ver detalles» merece una mirada.
El aviso de Google y la solicitud de revisión
Muchas veces, cuando Google detecta contenido pirateado, se lo enseña a tus visitas antes que a ti. En los resultados de búsqueda aparece debajo de tu web un mensaje del tipo «Este sitio puede haber sido comprometido» , y en algunos casos el navegador llega a mostrar una página de advertencia antes de dejar entrar. Para un negocio eso es peor que el spam en sí: la gente ve el aviso y se va con la competencia.
Si te pasa, el aviso no se quita solo al limpiar. El proceso es este, según la ayuda de Search Console :
- Entra en Search Console, en Seguridad y acciones manuales > Problemas de seguridad. Ahí verás el tipo de problema y ejemplos de URLs afectadas.
- Limpia todo el sitio, no solo las URLs de ejemplo, y cierra la puerta por la que entraron.
- En ese mismo informe, pulsa Solicitar revisión y explica qué pasó, qué has hecho y cómo lo has comprobado. Cuanto más concreto, mejor.
- Espera. Google habla de varios días a varias semanas. No mandes la solicitud antes de estar seguro, porque si en la revisión encuentran algo, la rechazan y vuelves a empezar.
Y para las URLs de spam que ya estén indexadas: que devuelvan 404 o 410 y, si quieres acelerar que desaparezcan, la herramienta de retirada de URLs de Search Console. Sin el aviso de seguridad no hay nada que pedir, pero conviene revisar igualmente Acciones manuales: enlaces salientes ocultos y artificiales es justo lo que Google penaliza.
Después de limpiar: lo que hay que cambiar
La limpieza quita lo que dejaron. Esto es para que no puedan volver a dejarlo. Va más o menos en orden de prioridad.
1. Contraseñas, todas y a la vez. Las de todos los administradores de WordPress, el panel del hosting, FTP, SSH y la base de datos. Y las del correo de las personas que administran la web: si la contraseña salió de un ordenador infectado o de un phishing, el correo es lo siguiente, y con el correo se recuperan todas las demás. Cada una distinta, y en un gestor de contraseñas.
2. Cierra todas las sesiones y regenera las claves. Cambiar la contraseña no echa a quien ya tiene una sesión abierta. Esto sí:
wp user session destroy --all-users
wp config shuffle-salts
Sin consola, las claves de wp-config.php se pueden generar en https://api.wordpress.org/secret-key/1.1/salt/ y sustituir a mano.
3. Revoca las contraseñas de aplicación. Están en el perfil de cada usuario, dan acceso a la API y sobreviven a un cambio de contraseña. Si no las usas, bórralas todas.
4. Doble factor para todos los administradores. Si esto llega a estar activo, la contraseña filtrada no habría servido para nada. Es la medida que más habría cambiado este caso, y cuesta diez minutos.
5. Menos administradores. Quien solo publica contenido no necesita ser administrador. Con el rol de Editor puede hacer todo su trabajo y no puede instalar plugins, que es justo lo que hizo el atacante con una cuenta de administrador.
wp user set-role usuario editor
6. Desactiva XML-RPC si no lo usas (lo usan la app móvil de WordPress y algunas integraciones como Jetpack). En este caso fue la vía para volver a entrar dos horas después de la limpieza. Lo más limpio es cerrarlo en el servidor:
<Files xmlrpc.php>
Require all denied
</Files>
7. Desactiva el editor de ficheros del panel. El atacante abrió el primer plugin en el editor de plugins de WordPress. Una línea en wp-config.php:
define( 'DISALLOW_FILE_EDIT', true );
Si gestionas las actualizaciones por otra vía, puedes ir más allá con DISALLOW_FILE_MODS, que impide instalar o actualizar nada desde el panel. Ojo, porque también te impide actualizar a ti.
8. Antivirus en los ordenadores. Si la contraseña se robó de un navegador, cambiarla no sirve de nada mientras el programa siga en el mismo equipo: te robará la nueva.
9. Registra los inicios de sesión antes de necesitarlo. En este caso el registro de quién entraba y desde dónde solo existía desde la segunda limpieza, cuando se instaló el plugin de seguridad. De antes solo había logs del servidor, que no dicen con qué usuario se entró. Cualquier plugin de seguridad decente guarda los inicios de sesión: tenlo puesto antes de que pase nada.
10. Revisa qué copias de seguridad están limpias. Aquí había copias diarias, pero las posteriores al día 21 tenían el primer plugin y las posteriores al 29, la puerta trasera. Si algún día hay que restaurar, conviene saber que «la copia de ayer» no es una copia limpia.
Para el resto del bastionado, la referencia oficial es el capítulo de Hardening WordPress del manual de administración de WordPress.
Cómo compruebo que no ha vuelto
Aquí entra justo lo que decía antes del escaneo periódico. Es algo que he construido para mis propios sitios y los que gestiono: Argos, un sistema automático de respaldo y análisis integral, de ficheros y base de datos, que he creado y opero. Te hablaré de él con calma en otro post. Cada noche copia los ficheros y la base de datos de cada web fuera del servidor, los compara con la noche anterior y con las sumas oficiales de WordPress.org, busca código malicioso conocido y me avisa si algo no cuadra.
Con este sitio lo di de alta el mismo día de la limpieza. El análisis de esa tarde salió sin rastro del atacante, y dos días después hice la comprobación de verdad: comparar fila a fila la base de datos de justo después de limpiar con la de dos días más tarde, y los ficheros igual. Solo habían cambiado cosas explicables: cachés, las contraseñas nuevas y el doble factor que activaron los administradores. Ni un inicio de sesión desconocido, ni contenido nuevo, ni tareas programadas raras.
Y la parte que me escuece: si hubiera estado vigilando desde antes, esta intrusión habría saltado el mismo día 29, por tres vías a la vez. Un cambio de contenido en la base de datos que nadie del equipo había hecho, un pico de ficheros nuevos y un plugin que no existe en el repositorio oficial de WordPress. Esa última regla la añadí a raíz de este caso.
Indicadores de compromiso
Para quien esté revisando un sitio con la misma pinta. El nombre de la ruta REST y el sufijo del plugin probablemente cambian en cada instalación, así que no te quedes solo con los literales. Lo reutilizable es el patrón, los nombres de cabecera y de error, y la clave pública del atacante.
Plugin:
wp-content/plugins/kizora-assets-extra-ri/
Plugin Name: Kizora Assets Extra
Author: Plenupla Layouts
Version: 4.9.23.17
Ruta REST, cabecera y marcadores:
POST /wp-json/z6km7cu/v1/mf/zk/19 # ruta de la puerta trasera
X-WPP-Signature # cabecera con la firma Ed25519
wp_poster_* # prefijo de los códigos de error
data-wp-poster="..." # atributo del div con el spam
position: absolute; left: -48592px; # el desplazamiento fuera de pantalla
Clave pública del atacante (fichero floda.php):
fpZnHoHVy0pY4UhxnPE/JtVIm+FNSb1HzTaU28Wf1xI=
Hashes SHA-256:
24d42fe3c8e4979050d2553e250db30651ca183943c968da643c46ee667e59d7 kizora-assets-extra-ri.php
fb93ca36f993df0219be90f024b2189db3cb91080ad1e1676c7702bd824e748b floda.php
7ce879fbe76be5bcf5248a7694c7619a94864eda0385b1bb08016ddd23e781e3 uninstall.php
El primer plugin (el que borró la primera limpieza): advanced-content-toolkit-d163. Mismo patrón de nombre genérico con sufijo corto.
Para buscarlo en tu servidor y en tu base de datos:
grep -rl "X-WPP-Signature\|wp_poster_" wp-content/plugins/
wp db search "data-wp-poster" --all-tables
wp db search "left: -48592px" --all-tables
Lo que me llevo de esto
Corta los accesos antes de limpiar. El atacante no entró por un fichero, entró por una contraseña. Borrar el fichero sin cambiar la contraseña le dio dos horas, y en dos horas dejó algo que ya no necesitaba contraseña.
Una portada que se ve bien no significa nada. Este spam está hecho para que el dueño no lo vea. Mira el código fuente, mira Google con site:, y mira Search Console.
Los cortafuegos miran la puerta, y el atacante tenía la llave. Ni Imunify360 ni Wordfence iban a parar un login legítimo, y el spam vivía en la base de datos, no en un fichero sospechoso. Lo que sí funciona es vigilar lo que ya está dentro y comparar: qué ha cambiado desde ayer, y qué plugin no existe en el repositorio oficial.
Una auditoría SEO también es una auditoría de seguridad. Esto no lo encontró ninguna herramienta de seguridad. Lo encontró un rastreo SEO rutinario buscando enlaces rotos.
Borrar el bloque no es limpiar. Revisiones, cachés, índices de enlaces, IndexNow, taxonomías. Si no buscas en toda la base de datos, no sabes si has terminado.
Doble factor. De todas las medidas de la lista, es la única que por sí sola habría evitado todo lo demás.
Si te ves en esto
Si has llegado aquí porque has encontrado enlaces de casino en tu código fuente, o porque Google le está diciendo a tus visitas que tu web puede estar comprometida, escríbeme y lo vemos. Mis datos están abajo. Si es algo que puedes resolver tú con lo de arriba, te lo digo y ya está.
Y lo mismo que en el caso de CoupDeGrace : no borres nada todavía, ni el plugin raro ni, sobre todo, los logs. Haz primero una copia de todo, cambia las contraseñas y cierra las sesiones, y después limpia. Si no sabes ni por dónde empezar a mirar, empieza por cómo saber si tu WordPress ha sido hackeado .