Todo ocurrió en menos de medio minuto
Las 5:06 de la mañana del 26 de agosto. Un bot automatizado, corriendo desde una dirección IPv6 de AWS con el agente Python-urllib/3.12, empezó a trabajar sobre un sitio WordPress. A las 5:07:10 ya había terminado. Cuatro peticiones HTTP, veinte segundos de reloj, y un webshell PHP esperando en la carpeta de descargas.
Lo que salvó el sitio no fue un plugin de seguridad. No fue una alerta en tiempo real. Fue una regla de servidor que impide ejecutar PHP dentro de wp-content/uploads — una protección estándar en hosting responsable que, en este caso, hizo exactamente lo que se supone que debe hacer.
Este artículo es la reconstrucción completa: el cronograma forense, la cadena de explotación, el análisis del dropper, el proceso de divulgación y las lecciones que quedan. Si tienes WordPress con un plugin de consentimiento activo, hay cosas aquí que deberías verificar antes de cerrar esta página.
El plugin: por qué existe y por qué importa
Antes de entrar en el ataque, contexto necesario: el RGPD obliga a bloquear cookies no esenciales hasta que el usuario dé su consentimiento explícito. Eso hace que un plugin de gestión de consentimiento no sea opcional para la mayoría de sitios con audiencia europea. El plugin afectado, gdpr-cookie-consent en su versión 4.4.1, tenía más de diez mil instalaciones activas en el momento del incidente.
La versión 4.4.1 había sido lanzada siete días antes del ataque. Siete días entre un lanzamiento legítimo y su explotación masiva y automatizada. Eso no es una anécdota — es una ventana que obliga a repensar cuánto tiempo nos tomamos en aplicar actualizaciones a componentes de terceros.
El cronograma exacto: cuatro peticiones, veinte segundos
Los registros de acceso reconstruyeron la secuencia con precisión de segundo:
- 05:06:50 — GET /wp-json/wp/v2/users?per_page=100 → respuesta 200. El endpoint REST estándar de WordPress devolvió el nombre de usuario del administrador sin autenticación.
- 05:07:02 — POST /wp-json/wplp-react-gdpr/v1/store-auth → respuesta 200. Envenenamiento de opciones: el plugin tenía una ruta REST sin autenticación real cuya única comprobación era un JWT de la plataforma SaaS del proveedor más la verificación de que el nombre de usuario perteneciera a un administrador. El bot escribió contenido JSON controlado por él en una opción de WordPress, incluyendo la clave maestra interna del plugin.
- 05:07:06 — POST /wp-json/wplp-react-gdpr/v1/upload-logo → respuesta 200. Una segunda ruta verificaba la clave de la petición contra el valor recién plantado en el paso anterior, y luego escribía contenido en base64 controlado por el atacante en uploads/nombre-de-archivo-controlado-por-atacante, sin validación de extensión ni de tipo MIME. El resultado: uxq9KacU_400x400.jpg.php, un webshell disfrazado con el patrón de nombre de un avatar de Twitter.
- 05:07:10 — GET /wp-content/uploads/2026/08/uxq9KacU_400x400.jpg.php → respuesta 403, tres veces. El servidor se negó a ejecutar PHP en esa ruta. El dropper nunca arrancó.
Un detalle forense relevante: la respuesta del servidor al paso de envenenamiento (store-auth) fue reconstruida desde el código fuente del plugin y coincidió byte a byte con los 102 bytes que aparecían en el registro de acceso. Así se pudo confirmar que el envenenamiento había funcionado aunque nada fuera visible en el panel de administración.
Lo que habría hecho el dropper si hubiera ejecutado
El archivo uxq9KacU_400x400.jpg.php pesaba 15.187 bytes y tenía un SHA256 de E3DA2973957017A72403531D5475BD95189DFF1C70B23E4657D0D5F23955575E. En el momento de subirlo a VirusTotal obtuvo 0 detecciones sobre 70 motores — comportamiento normal para un dropper PHP nuevo. Las firmas no existen hasta que alguien analiza la muestra.
La secuencia de ejecución del dropper era la siguiente: arrancar WordPress en modo silencioso, crear un administrador oculto con nombre de usuario aleatorio, suprimir todos los correos de notificación para que el propietario no recibiera ningún aviso, antedatar la fecha de registro de la cuenta nueva para que coincidiera con la de un usuario existente (lo que hace inútil el truco habitual de ordenar usuarios por fecha de creación), devolver la contraseña generada como JSON al atacante, escanearel volumen completo en busca de otras instalaciones de WordPress y leer sus credenciales de base de datos desde cada wp-config.php encontrado, y finalmente autoeliminarse.
Este último punto merece atención propia: el dropper no apuntaba solo a un sitio. Intentaba saltar lateralmente a cualquier otra instalación de WordPress en el mismo servidor. El aislamiento entre sitios en la capa del hosting no es solo comodidad — es la diferencia entre un incidente contenido y un compromiso en cadena.
Otro detalle: el dropper se autoeliminaba solo después de ejecutarse con éxito. Como la ejecución fue bloqueada, el archivo quedó en su lugar. Eso fue lo que el escáner de malware del servidor encontró a la mañana siguiente. Sin el bloqueo de ejecución, el archivo habría desaparecido solo y probablemente no habría rastro visible.
Qué realmente detuvo el ataque
Vale la pena ser preciso aquí, porque hay una lección de fondo que se pierde si no se nombra bien.
Lo que detuvo el compromiso total no fue ninguna de las cosas que la mayoría de guías de seguridad para WordPress recomienda configurar. No fue un plugin de seguridad, no fue un firewall de aplicación configurado manualmente, no fue un sistema de detección en tiempo real. Fue una política de servidor — PHP no se ejecuta dentro de wp-content/uploads — que en este caso era parte del comportamiento estándar del hosting.
Esto tiene una implicación práctica inmediata: si no sabes si tu servidor aplica esa regla, tienes que averiguarlo. La pregunta es sencilla y se puede hacer directamente al soporte del hosting. Si la respuesta no es clara o si gestionas tu propio servidor, es una configuración que se puede añadir en minutos mediante una regla en nginx, en .htaccess o en haproxy — y hay quien lo ha implementado exactamente así después de conocer este caso.
La comunidad técnica que analizó este incidente fue explícita sobre otro punto: bloquear el User-Agent Python-urllib habría reducido el ruido pero no habría cambiado el resultado. El encabezado User-Agent es una cadena de texto que el atacante puede cambiar en una línea de código, y muchas campañas automatizadas ya rotan entre User-Agents que imitan navegadores reales. La defensa que funcionó aquí era agnóstica al comportamiento del atacante: no le importaba qué decía la petición, simplemente PHP no ejecuta en esa carpeta.
El proceso de divulgación: cinco canales, cinco días
Una vez identificado el fallo y documentado el ataque, se realizaron cinco divulgaciones coordinadas:
- Proveedor (WPLP/WPEKA) a través de su programa de bug bounty: reconocimiento rápido, parche publicado en 4.4.2 dentro de cinco días.
- WPScan: al reproducir la cadena, encontraron dos rutas adicionales con la misma capacidad de escritura arbitraria de archivos y sin sanitización de nombres. Esas rutas podían escribir fuera de uploads, lo que habría eludido completamente la capa que salvó el sitio. El proveedor también las corrigió.
- Patchstack: ya tenía un reporte independiente del mismo problema, lo que confirma que otros investigadores habían llegado a la misma vulnerabilidad. Lo habían escalado al equipo de WordPress.org.
- Reporte de abuso a AWS por la IP atacante.
- Reporte al registrador por el dominio de exfiltración que usaba el dropper.
La divulgación coordinada bien documentada funciona. Cinco canales paralelos, con evidencia técnica sólida, produjeron un parche en una semana. El sistema es lento hasta que tiene suficiente presión y suficiente contexto.
Qué cambió en la versión 4.4.2
El análisis del código fuente de la versión parcheada confirma correcciones concretas. La ruta upload-logo y su manejador fueron eliminados completamente. Cada ruta en ese espacio de nombres REST requiere ahora una firma HMAC-SHA256 con un secreto por sitio y una ventana anti-replay. El JWT está vinculado a la cuenta conectada del sitio. Las cargas de imágenes restantes pasan por validación real: lista de extensiones permitidas, verificación de byte mágico, y los nombres de archivo se reconstruyen para que una cadena como shell.php.jpg se convierta en shell-php.jpg.
Si tienes instalado gdpr-cookie-consent, la acción es inmediata: actualizar a 4.4.2. Y adicionalmente, revisar la tabla wp_users buscando administradores no reconocidos. El dropper antedataba la fecha de registro para que coincidiera con la de un usuario existente, así que buscar cuentas recientes no es suficiente — hay que comparar marcas de tiempo idénticas entre usuarios distintos.
El patrón de fondo: SaaS acoplado a plugins establecidos
Hay algo en este incidente que va más allá del plugin concreto. El ecosistema de WordPress lleva años en el que cada componente implementa sus propias funcionalidades, incluida la autenticación REST personalizada. El patrón que aparece aquí — un dashboard conectado a una plataforma SaaS, acoplado a un plugin establecido, con nuevas rutas REST y autenticación propia — es exactamente el tipo de superficie de ataque que nadie audita hasta que aparece un registro como este.
Un update legítimo es el canal de distribución. No hay señal de que el mantenedor del plugin fuera comprometido — la vulnerabilidad era un error de diseño en la autenticación de las nuevas rutas. Pero el resultado práctico es el mismo: una actualización que llega a diez mil instalaciones y que siete días después está siendo explotada de forma automatizada y masiva.
Esto no es un argumento para no actualizar. Es exactamente el argumento contrario: actualizar rápido es la única forma de cerrar ventanas que, como demuestra este caso, se miden en días, no en semanas.
Indicadores de compromiso para compartir
El parche es público. Estos IOCs son de libre distribución:
- SHA256 del dropper: E3DA2973957017A72403531D5475BD95189DFF1C70B23E4657D0D5F23955575E
- Patrón de nombre del payload: *_400x400.jpg.php (imita avatares de Twitter)
- Admin rogue: nombre de usuario = dominio del sitio + 3 caracteres aleatorios; nombre visible Lucas Hayes; dominio de correo ifuqpatr.com
- IP atacante: 2600:1f18:472b:900:f81d:f8eb:a12e:7fd3 (IPv6 AWS), UA Python-urllib/3.12
- Patrón de ataque: GET a /wp-json/wp/v2/users?per_page=100, POST a /wp-json/wplp-react-gdpr/v1/store-auth, POST a /wp-json/wplp-react-gdpr/v1/upload-logo
Las preguntas que quedan abiertas
Este incidente deja tres preguntas que no tienen respuesta fácil y que merecen atención.
La primera: cuántos sitios fueron comprometidos antes de que el parche estuviera disponible. El bot era una campaña masiva. El mismo problema fue reportado de forma independiente por al menos otro investigador. No hay forma de saber cuántas instalaciones con el dropper ejecutado y autoeliminado no dejaron rastro.
La segunda: cómo verificar que un hosting aplica correctamente el bloqueo de PHP en uploads en toda su infraestructura, no solo en teoría. La respuesta honesta es que no hay una forma universal de comprobarlo desde fuera. Lo que sí se puede hacer es preguntar directamente al soporte con una pregunta técnica específica — y evaluar la calidad de la respuesta como señal de la madurez de seguridad del proveedor.
La tercera: qué prácticas de auditoría deberían aplicarse a nuevos componentes SaaS en plugins establecidos antes de publicarlos. Esta es la pregunta más difícil porque no tiene un responsable claro en el ecosistema actual. WordPress.org no audita proactivamente cada update. El proveedor del plugin tiene incentivos para publicar rápido. El resultado es que la auditoría ocurre, si ocurre, después de la distribución.
Lo que se puede hacer hoy, sin esperar
Sin entrar en costes ni proyectos: hay acciones concretas que no requieren presupuesto.
- Preguntar al hosting si bloquea la ejecución de PHP dentro de wp-content/uploads. Si la respuesta no es un sí claro, solicitar que lo apliquen o valorar la alternativa de añadir una regla propia.
- Bloquear la enumeración de usuarios a través de REST (/wp-json/wp/v2/users). Fue el paso 1 de este ataque. No es una solución completa, pero elimina el paso más fácil de la cadena.
- Configurar retención de registros de acceso más larga de la que tiene por defecto el hosting. Toda la reconstrucción forense de este caso provino de registros. Sin ellos, no hay nada que analizar.
- Si se usa un plugin de consentimiento de cookies, evaluar si tiene rutas REST expuestas, qué autenticación aplican y si el proveedor tiene un canal de reporte de vulnerabilidades activo.
Después del incidente, el autor migró a un script de consentimiento alojado externamente, sin rutas de plugin propias ni superficie REST. Es una opción válida cuando la funcionalidad lo permite.
Para una pyme, el coste de un compromiso real no es solo técnico: incluye la notificación obligatoria a la autoridad de control en 72 horas según el artículo 33 del RGPD si hay datos personales afectados, la auditoría forense, el posible impacto en el posicionamiento si Google detecta el malware, y la carga reputacional. Comparado con eso, una pregunta al soporte del hosting y revisar la configuración de registros son inversiones de tiempo que se miden en minutos.
La pregunta que vale la pena hacerse ahora mismo: ¿sabes con certeza si tu servidor ejecutaría un PHP subido a wp-content/uploads?
¿Necesitas asesoramiento experto para tu empresa?
Realizamos un estudio personalizado de tu infraestructura IT, ciberseguridad o procesos de inteligencia artificial. Descubre el verdadero potencial de tu negocio.
Solicitar auditoría gratuita