Cómo auditar el compromiso de cadena de suministro en plugins de BdThemes
Una respuesta JSON remota manipulada comprometió la cadena de suministro de siete plugins de BdThemes. El ataque no alteró archivos en el repositorio oficial de WordPress.org ni exigió actualizar un plugin: bastaba con que un administrador cargase cualquier pantalla de wp-admin.
El vector convierte un banner promocional aparentemente inocuo en JavaScript que se ejecuta dentro de una sesión autenticada. A partir de ahí, puede crear administradores fraudulentos, instalar una webshell y dejar mecanismos persistentes que ocultan el compromiso.
Qué plugins están afectados
La vulnerabilidad afecta a la biblioteca interna Biggopti en varias versiones y tiene una puntuación CVSS de 5,4 sobre 10 (media). No había parche disponible durante la investigación.
- Element Pack Addons for Elementor – Elementor Widgets, Elementor Templates, Elementor Addons (
bdthemes-element-pack-lite). - Prime Slider Addons for Elementor – Widgets, Templates & Elementor Addons (
bdthemes-prime-slider-lite). - Pixel Gallery Addons for Elementor – Easy Grid, Creative Gallery, Drag and Drop Grid, Custom Grid Layout, Portfolio Gallery (
pixel-gallery). - Ultimate Post Kit Addons for Elementor (
ultimate-post-kit). - Ultimate Store Kit – Addon For WooCommerce, EDD and Elementor (
ultimate-store-kit). - Live Copy Paste for Elementor – Cross Domain Copy Paste & Page Duplicator (
live-copy-paste). - Smart Admin Assistant – Dashboard and Site Enhancements (
smart-admin-assistant).
El directorio de plugins de WordPress cerró temporalmente los siete plugins el 7 de agosto de 2026 mientras revisaba el incidente. Si uno de ellos está instalado, trata el sitio como potencialmente expuesto aunque la comprobación de integridad de archivos no detecte cambios.
Cómo se produjo el XSS desde un banner remoto
Biggopti descarga banners promocionales desde una API del proveedor y los muestra en el escritorio de WordPress. La API no era una aplicación dinámica: entregaba archivos JSON estáticos desde un bucket de DigitalOcean Spaces protegido por Cloudflare.
Cada plugin que usa Biggopti registra un archivo JavaScript del lado del cliente en admin_init. Por eso el navegador lo ejecuta sin condiciones en todas las cargas de wp-admin, descarga el banner remoto y construye su HTML en el DOM.
El fallo aparece al usar display_id de la respuesta sin escapar dentro del atributo HTML id. Este es el patrón vulnerable que incorporaba el JavaScript:
// JavaScript: display_id llega desde el JSON remoto
var f = t.display_id || t.id || "default";
var m = "bdt-admin-biggopti-api-biggopti-" + f;
A = 'id="' + m + '"'; // Inserción en el DOM sin escaparEl mismo código escapaba correctamente el valor destinado a data-display-id, aunque con una estructura compleja propia de un archivo minimizado o empaquetado. El contraste deja claro que el atributo id quedó fuera de esa protección.
// JavaScript: función de escape que sí se aplicaba a otro atributo
function escapeAttribute(value) {
return (value || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """);
}El historial SVN sitúa la introducción de la inyección sin escapar el 1 de marzo de 2026, en Prime Slider 4.1.9, commit r3471891. Después se aplicó a otros plugins. Las versiones posteriores añadieron un sanitizador basado en DOMParser para el campo de contenido del banner, capaz de eliminar etiquetas HTML peligrosas y manejadores inline, pero nunca escaparon el atributo id.
Un actor no autorizado consiguió permiso de escritura sobre el almacenamiento del proveedor y reemplazó respuestas JSON legítimas por registros preparados. El campo display_id cerraba el atributo id, añadía un manejador onanimationstart y activaba una animación CSS en 10 milisegundos. Así cargaba JavaScript de forma silenciosa al abrir cualquier página del escritorio como administrador.
La respuesta manipulada incluía, entre otros campos, un display_id que empezaba por SAFE" onanimationstart=... y un banner de tipo adminDashboard titulado “Build more. Pay less. This Summer”. El manejador reconstruía mediante códigos de caracteres una petición a un script externo.
Dos cadenas de carga maliciosa
La campaña utilizó dos payloads según el endpoint de Biggopti que consultase cada plugin. Ambos aprovechaban los permisos de la sesión de administrador ya abierta en el navegador; no necesitaban la contraseña del administrador ni modificar primero archivos del plugin.
Payload principal: w2.js
Los plugins que consumían api-data-all-records recibían el payload principal, w2.js. Al ejecutarse, seguía esta secuencia:
- Verificaba el C2: contactaba con
ia-cdn[.]com/fz/c, enviaba el origen del sitio y esperaba instrucciones de segmentación. Si el servidor respondíaskipodone, detenía la ejecución. - Creaba un administrador fraudulento: usaba el encabezado
X-WP-Noncedisponible en la sesión activa para crear un usuario administrador mediante la API REST de WordPress. Si esa vía fallaba, recurría al envío de formularios estándar. - Instalaba una webshell: descargaba desde el C2 un ZIP de plugin falso y lo subía mediante el formulario habitual de carga de plugins. Se camuflaba bajo el slug
wp-smart-thumbnailsu otros nombres neutros e incluía la webshellemer-run.php, invocable directamente por URL. - Instalaba puertas traseras en plugins obligatorios: al invocar
emer-run.php, copiaba dos módulos persistentes al directorio de plugins obligatorios. Las dos muestras analizadas tenían fechas retroactivas a septiembre de 2025 para mezclarse con las marcas de tiempo legítimas; los nombres de archivo podían variar entre infecciones. Uno permitía iniciar sesión como administrador sin autenticar mediante?_wplogin=<token>y elegía al administrador registrado desde hacía más tiempo. El otro se enganchaba a las consultas de la base de datos de WordPress para ocultar las cuentas fraudulentas en la lista de usuarios del escritorio y reducir también el total de usuarios mostrado. - Exfiltraba resultados: enviaba al C2 los resultados de cada fase mediante
navigator.sendBeacon.
Payload alternativo: x.js
El script secundario, x.js, estaba alojado directamente en la infraestructura de BdThemes y llegaba a los plugins que usaban api-data-records. Calculaba credenciales administrativas deterministas a partir del nombre de host del sitio.
// JavaScript: el hash del host define siempre las mismas credenciales
function makeCreds() {
var host = siteKey();
var hash = 0;
for (var i = 0; i < host.length; i++) {
hash = ((hash << 5) - hash + host.charCodeAt(i)) | 0;
}
var n = Math.abs(hash).toString(36).slice(0, 6);
return {
user: 'bd_' + n,
pass: 'Bd@26!' + n + 'x'
};
}El algoritmo generaba nombres con el prefijo bd_ y seis caracteres de un hash base36, además de contraseñas con el patrón Bd@26!<hash>x. Asociaba la cuenta a una dirección @wordpress.org. Como las credenciales dependen del host, los atacantes no necesitaban guardar una lista centralizada de sitios comprometidos: un equipo de respuesta puede calcularlas al investigar un dominio sospechoso.
Después intentaba crear el administrador desde /wp-admin/user-new.php, extraía el nonce del formulario y enviaba la solicitud como mismo origen. Informaba del resultado al C2 con sendBeacon o, como alternativa, con una petición fetch POST de tipo no-cors, e incluía el estado, las credenciales, el origen, la URL de cierre de sesión y la página actual. Cuando podía, guardaba una marca en localStorage para no repetir la operación en ese navegador.
Por qué el compromiso resulta difícil de ver
La explotación vivía por completo en la respuesta de una API remota. No requería actualizar el plugin, no cambiaba archivos en disco y convertía el feed de promoción de un proveedor de confianza en el vehículo de entrega de malware.
Por ese motivo, los escáneres que solo comparan archivos con una versión conocida apenas ven señales, y un firewall de aplicaciones web también puede pasar por alto gran parte de la cadena. El compromiso del bucket o de las credenciales de infraestructura de BdThemes permitió publicar tanto registros JSON maliciosos como el payload x.js dentro de la propia infraestructura del proveedor.
El dominio de C2 se relacionó con los actores de los compromisos de cadena de suministro de Advanced Responsive Video Embedder y OptinMonster ocurridos en los dos meses anteriores. El objetivo de esta campaña era mantener acceso administrativo silencioso y ejecutar código remoto en un número amplio de instalaciones de WordPress.
Indicadores de compromiso
Busca estos recursos externos en registros, contenido inyectado y comunicaciones salientes. Los endpoints de la API del proveedor ya devolvían JSON limpio el 8 de agosto de 2026, pero su presencia histórica sirve para acotar la exposición.
- Payload principal:
ia-cdn[.]com/fz/w2.js. - Endpoint de beacon C2:
ia-cdn[.]com/fz/c. - Endpoint envenenado:
api[.]sigmative[.]io/prod/store/api/biggopti/api-data-all-records. - Endpoint envenenado:
api[.]sigmative[.]io/prod/store/api/biggopti/api-data-records. - Payload alternativo alojado por el proveedor:
api[.]sigmative[.]io/prod/store/api/biggopti/x.js.
Revisa también el sistema de archivos y la base de datos para localizar estos indicadores:
- La webshell
emer-run.php, con MD51024732009983dd5e54b4cf5593f04d4. - La puerta trasera de inicio de sesión mágico
class-wp-token-validate.php, con MD57719cd98a35ffad2771f26d1ceab7d27. - El módulo de ocultación
class-wp-query-9d127ff3.php, o nombres similares con el patrónclass-wp-query-*.php, con MD59aadc3e5c5242b273bd17c5bdc358845. - El módulo presentado como comprobación de estado
wp-cache-optimizer.php, con MD5e450ae5bc4bfc0d960dded06a76bb8e9. - La opción de base de datos
fz_emer_login_tokens, que almacena tokens de inicio de sesión mágico. - La opción de base de datos
fz_emer_done_v1, que indica que el compromiso se completó. - Cuentas con direcciones de correo de los dominios
@wordpress.orgo@developer.wordpress.org. - Nombres de usuario que coincidan con
bd_seguido de seis caracteres alfanuméricos.
Cronología del incidente
- 1 de marzo de 2026: Biggopti añadió el JavaScript de API a Prime Slider 4.1.9 mediante el commit SVN
r3471891.display_identró sin escapar en un atributoidy el plugin consultabaapi-data-records. - 10 de mayo de 2026: el endpoint cambió a
api-data-all-records. Se añadió el sanitizadorDOMParserpara el campo de contenido, pero el atributoidcontinuó sin escapar. - 23 de junio de 2026: la campaña registró esa fecha de inicio en el aviso “Summer Sale”; es la fecha más temprana en la que el XSS pudo estar activo.
- 6 de agosto de 2026, 22:40 UTC: última modificación del endpoint envenenado
api-data-records. - 7 de agosto de 2026, 09:11 UTC: última modificación del endpoint envenenado
api-data-all-records. - 7 de agosto de 2026: se notificó el incidente y se observó el ataque en entornos reales.
- 7 de agosto de 2026: el directorio de WordPress cerró los plugins afectados mientras los revisaba.
- 7 de agosto de 2026, 16:56 UTC: se capturaron los artefactos de la investigación.
- 8 de agosto de 2026: ambos endpoints de API ya devolvían JSON limpio.
Qué debes revisar ahora
Audita de inmediato la lista de usuarios directamente en la base de datos y no dependas solo de lo que muestra el escritorio: el módulo de ocultación puede alterar las consultas que alimentan esa pantalla. Elimina las cuentas fraudulentas, pero conserva antes evidencias de sus metadatos y de los registros asociados para investigar el alcance.
Inspecciona los directorios de plugins y de plugins obligatorios en busca de los archivos, patrones y hashes indicados. Revisa las opciones fz_emer_login_tokens y fz_emer_done_v1, identifica llamadas a los dominios señalados en los registros y busca administradores con los patrones de correo y usuario descritos.
Las firmas de malware y reglas de firewall para detectar esta campaña estuvieron disponibles desde el 7 de agosto de 2026 para las modalidades Premium, Care y Response de Wordfence y para clientes de pago de Wordfence CLI. Las modalidades gratuitas de Wordfence y Wordfence CLI las recibirán tras el retraso estándar de 30 días.
Laura Fernández
Ingeniera QA y experta en automatización. Cypress y Playwright son mis favoritos. Creo que las buenas pruebas son la base del software de calidad.
Todas las publicaciones