{"id":265,"date":"2026-08-09T10:40:31","date_gmt":"2026-08-09T08:40:31","guid":{"rendered":"https:\/\/helloblog.io\/es\/xss-bdthemes-compromiso-cadena-suministro\/"},"modified":"2026-08-09T10:40:31","modified_gmt":"2026-08-09T08:40:31","slug":"xss-bdthemes-compromiso-cadena-suministro","status":"publish","type":"post","link":"https:\/\/helloblog.io\/es\/xss-bdthemes-compromiso-cadena-suministro\/","title":{"rendered":"C\u00f3mo auditar el compromiso de cadena de suministro en plugins de BdThemes"},"content":{"rendered":"\n<p>Una respuesta JSON remota manipulada comprometi\u00f3 la cadena de suministro de siete plugins de BdThemes. El ataque no alter\u00f3 archivos en el repositorio oficial de WordPress.org ni exigi\u00f3 actualizar un plugin: bastaba con que un administrador cargase cualquier pantalla de <code>wp-admin<\/code>.<\/p>\n\n\n\n<p>El vector convierte un banner promocional aparentemente inocuo en JavaScript que se ejecuta dentro de una sesi\u00f3n autenticada. A partir de ah\u00ed, puede crear administradores fraudulentos, instalar una webshell y dejar mecanismos persistentes que ocultan el compromiso.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 plugins est\u00e1n afectados<\/h2>\n\n\n\n<p>La vulnerabilidad afecta a la biblioteca interna Biggopti en varias versiones y tiene una puntuaci\u00f3n CVSS de <strong>5,4 sobre 10 (media)<\/strong>. No hab\u00eda parche disponible durante la investigaci\u00f3n.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Element Pack Addons for Elementor &#8211; Elementor Widgets, Elementor Templates, Elementor Addons (<code>bdthemes-element-pack-lite<\/code>).<\/li>\n\n\n<li>Prime Slider Addons for Elementor &#8211; Widgets, Templates &#038; Elementor Addons (<code>bdthemes-prime-slider-lite<\/code>).<\/li>\n\n\n<li>Pixel Gallery Addons for Elementor &#8211; Easy Grid, Creative Gallery, Drag and Drop Grid, Custom Grid Layout, Portfolio Gallery (<code>pixel-gallery<\/code>).<\/li>\n\n\n<li>Ultimate Post Kit Addons for Elementor (<code>ultimate-post-kit<\/code>).<\/li>\n\n\n<li>Ultimate Store Kit &#8211; Addon For WooCommerce, EDD and Elementor (<code>ultimate-store-kit<\/code>).<\/li>\n\n\n<li>Live Copy Paste for Elementor &#8211; Cross Domain Copy Paste &#038; Page Duplicator (<code>live-copy-paste<\/code>).<\/li>\n\n\n<li>Smart Admin Assistant &#8211; Dashboard and Site Enhancements (<code>smart-admin-assistant<\/code>).<\/li>\n\n<\/ul>\n\n\n\n<p>El directorio de plugins de WordPress cerr\u00f3 temporalmente los siete plugins el 7 de agosto de 2026 mientras revisaba el incidente. Si uno de ellos est\u00e1 instalado, trata el sitio como potencialmente expuesto aunque la comprobaci\u00f3n de integridad de archivos no detecte cambios.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">C\u00f3mo se produjo el XSS desde un banner remoto<\/h2>\n\n\n\n<p>Biggopti descarga banners promocionales desde una API del proveedor y los muestra en el escritorio de WordPress. La API no era una aplicaci\u00f3n din\u00e1mica: entregaba archivos JSON est\u00e1ticos desde un bucket de DigitalOcean Spaces protegido por Cloudflare.<\/p>\n\n\n\n<p>Cada plugin que usa Biggopti registra un archivo JavaScript del lado del cliente en <code>admin_init<\/code>. Por eso el navegador lo ejecuta sin condiciones en <strong>todas<\/strong> las cargas de <code>wp-admin<\/code>, descarga el banner remoto y construye su HTML en el DOM.<\/p>\n\n\n\n<p>El fallo aparece al usar <code>display_id<\/code> de la respuesta sin escapar dentro del atributo HTML <code>id<\/code>. Este es el patr\u00f3n vulnerable que incorporaba el JavaScript:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#24292e\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>\/\/ JavaScript: display_id llega desde el JSON remoto\nvar f = t.display_id || t.id || &quot;default&quot;;\nvar m = &quot;bdt-admin-biggopti-api-biggopti-&quot; + f;\nA = 'id=&quot;' + m + '&quot;'; \/\/ Inserci\u00f3n en el DOM sin escapar\n<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D\">\/\/ JavaScript: display_id llega desde el JSON remoto<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">var<\/span><span style=\"color:#E1E4E8\"> f <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> t.display_id <\/span><span style=\"color:#F97583\">||<\/span><span style=\"color:#E1E4E8\"> t.id <\/span><span style=\"color:#F97583\">||<\/span><span style=\"color:#9ECBFF\"> \"default\"<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">var<\/span><span style=\"color:#E1E4E8\"> m <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#9ECBFF\"> \"bdt-admin-biggopti-api-biggopti-\"<\/span><span style=\"color:#F97583\"> +<\/span><span style=\"color:#E1E4E8\"> f;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#79B8FF\">A<\/span><span style=\"color:#F97583\"> =<\/span><span style=\"color:#9ECBFF\"> 'id=\"'<\/span><span style=\"color:#F97583\"> +<\/span><span style=\"color:#E1E4E8\"> m <\/span><span style=\"color:#F97583\">+<\/span><span style=\"color:#9ECBFF\"> '\"'<\/span><span style=\"color:#E1E4E8\">; <\/span><span style=\"color:#6A737D\">\/\/ Inserci\u00f3n en el DOM sin escapar<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>El mismo c\u00f3digo escapaba correctamente el valor destinado a <code>data-display-id<\/code>, aunque con una estructura compleja propia de un archivo minimizado o empaquetado. El contraste deja claro que el atributo <code>id<\/code> qued\u00f3 fuera de esa protecci\u00f3n.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#24292e\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>\/\/ JavaScript: funci\u00f3n de escape que s\u00ed se aplicaba a otro atributo\nfunction escapeAttribute(value) {\n  return (value || &quot;&quot;)\n    .replace(\/&amp;\/g, &quot;&amp;amp;&quot;)\n    .replace(\/&lt;\/g, &quot;&amp;lt;&quot;)\n    .replace(\/&gt;\/g, &quot;&amp;gt;&quot;)\n    .replace(\/&quot;\/g, &quot;&amp;quot;&quot;);\n}\n<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D\">\/\/ JavaScript: funci\u00f3n de escape que s\u00ed se aplicaba a otro atributo<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">function<\/span><span style=\"color:#B392F0\"> escapeAttribute<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#FFAB70\">value<\/span><span style=\"color:#E1E4E8\">) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  return<\/span><span style=\"color:#E1E4E8\"> (value <\/span><span style=\"color:#F97583\">||<\/span><span style=\"color:#9ECBFF\"> \"\"<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    .<\/span><span style=\"color:#B392F0\">replace<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#DBEDFF\">&#x26;<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#F97583\">g<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">\"&#x26;amp;\"<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    .<\/span><span style=\"color:#B392F0\">replace<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#DBEDFF\">&#x3C;<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#F97583\">g<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">\"&#x26;lt;\"<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    .<\/span><span style=\"color:#B392F0\">replace<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#DBEDFF\">><\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#F97583\">g<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">\"&#x26;gt;\"<\/span><span style=\"color:#E1E4E8\">)<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    .<\/span><span style=\"color:#B392F0\">replace<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#DBEDFF\">\"<\/span><span style=\"color:#9ECBFF\">\/<\/span><span style=\"color:#F97583\">g<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#9ECBFF\">\"&#x26;quot;\"<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>El historial SVN sit\u00faa la introducci\u00f3n de la inyecci\u00f3n sin escapar el 1 de marzo de 2026, en Prime Slider 4.1.9, commit <code>r3471891<\/code>. Despu\u00e9s se aplic\u00f3 a otros plugins. Las versiones posteriores a\u00f1adieron un sanitizador basado en <code>DOMParser<\/code> para el campo de contenido del banner, capaz de eliminar etiquetas HTML peligrosas y manejadores inline, pero nunca escaparon el atributo <code>id<\/code>.<\/p>\n\n\n\n<p>Un actor no autorizado consigui\u00f3 permiso de escritura sobre el almacenamiento del proveedor y reemplaz\u00f3 respuestas JSON leg\u00edtimas por registros preparados. El campo <code>display_id<\/code> cerraba el atributo <code>id<\/code>, a\u00f1ad\u00eda un manejador <code>onanimationstart<\/code> y activaba una animaci\u00f3n CSS en 10 milisegundos. As\u00ed cargaba JavaScript de forma silenciosa al abrir cualquier p\u00e1gina del escritorio como administrador.<\/p>\n\n\n\n<p>La respuesta manipulada inclu\u00eda, entre otros campos, un <code>display_id<\/code> que empezaba por <code>SAFE&quot; onanimationstart=...<\/code> y un banner de tipo <code>adminDashboard<\/code> titulado \u201cBuild more. Pay less. This Summer\u201d. El manejador reconstru\u00eda mediante c\u00f3digos de caracteres una petici\u00f3n a un script externo.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Dos cadenas de carga maliciosa<\/h2>\n\n\n\n<p>La campa\u00f1a utiliz\u00f3 dos payloads seg\u00fan el endpoint de Biggopti que consultase cada plugin. Ambos aprovechaban los permisos de la sesi\u00f3n de administrador ya abierta en el navegador; no necesitaban la contrase\u00f1a del administrador ni modificar primero archivos del plugin.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Payload principal: <code>w2.js<\/code><\/h3>\n\n\n\n<p>Los plugins que consum\u00edan <code>api-data-all-records<\/code> recib\u00edan el payload principal, <code>w2.js<\/code>. Al ejecutarse, segu\u00eda esta secuencia:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n\n<li><strong>Verificaba el C2:<\/strong> contactaba con <code>ia-cdn[.]com\/fz\/c<\/code>, enviaba el origen del sitio y esperaba instrucciones de segmentaci\u00f3n. Si el servidor respond\u00eda <code>skip<\/code> o <code>done<\/code>, deten\u00eda la ejecuci\u00f3n.<\/li>\n\n\n<li><strong>Creaba un administrador fraudulento:<\/strong> usaba el encabezado <code>X-WP-Nonce<\/code> disponible en la sesi\u00f3n activa para crear un usuario administrador mediante la API REST de WordPress. Si esa v\u00eda fallaba, recurr\u00eda al env\u00edo de formularios est\u00e1ndar.<\/li>\n\n\n<li><strong>Instalaba una webshell:<\/strong> descargaba desde el C2 un ZIP de plugin falso y lo sub\u00eda mediante el formulario habitual de carga de plugins. Se camuflaba bajo el slug <code>wp-smart-thumbnails<\/code> u otros nombres neutros e inclu\u00eda la webshell <code>emer-run.php<\/code>, invocable directamente por URL.<\/li>\n\n\n<li><strong>Instalaba puertas traseras en plugins obligatorios:<\/strong> al invocar <code>emer-run.php<\/code>, copiaba dos m\u00f3dulos persistentes al directorio de plugins obligatorios. Las dos muestras analizadas ten\u00edan fechas retroactivas a septiembre de 2025 para mezclarse con las marcas de tiempo leg\u00edtimas; los nombres de archivo pod\u00edan variar entre infecciones. Uno permit\u00eda iniciar sesi\u00f3n como administrador sin autenticar mediante <code>?_wplogin=&lt;token&gt;<\/code> y eleg\u00eda al administrador registrado desde hac\u00eda m\u00e1s 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\u00e9n el total de usuarios mostrado.<\/li>\n\n\n<li><strong>Exfiltraba resultados:<\/strong> enviaba al C2 los resultados de cada fase mediante <code>navigator.sendBeacon<\/code>.<\/li>\n\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Payload alternativo: <code>x.js<\/code><\/h3>\n\n\n\n<p>El script secundario, <code>x.js<\/code>, estaba alojado directamente en la infraestructura de BdThemes y llegaba a los plugins que usaban <code>api-data-records<\/code>. Calculaba credenciales administrativas deterministas a partir del nombre de host del sitio.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#24292e\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#e1e4e8;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly>\/\/ JavaScript: el hash del host define siempre las mismas credenciales\nfunction makeCreds() {\n  var host = siteKey();\n  var hash = 0;\n\n  for (var i = 0; i &lt; host.length; i++) {\n    hash = ((hash &lt;&lt; 5) - hash + host.charCodeAt(i)) | 0;\n  }\n\n  var n = Math.abs(hash).toString(36).slice(0, 6);\n  return {\n    user: 'bd_' + n,\n    pass: 'Bd@26!' + n + 'x'\n  };\n}\n<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#6A737D\">\/\/ JavaScript: el hash del host define siempre las mismas credenciales<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">function<\/span><span style=\"color:#B392F0\"> makeCreds<\/span><span style=\"color:#E1E4E8\">() {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  var<\/span><span style=\"color:#E1E4E8\"> host <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#B392F0\"> siteKey<\/span><span style=\"color:#E1E4E8\">();<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  var<\/span><span style=\"color:#E1E4E8\"> hash <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\"> 0<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  for<\/span><span style=\"color:#E1E4E8\"> (<\/span><span style=\"color:#F97583\">var<\/span><span style=\"color:#E1E4E8\"> i <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#79B8FF\"> 0<\/span><span style=\"color:#E1E4E8\">; i <\/span><span style=\"color:#F97583\">&#x3C;<\/span><span style=\"color:#E1E4E8\"> host.<\/span><span style=\"color:#79B8FF\">length<\/span><span style=\"color:#E1E4E8\">; i<\/span><span style=\"color:#F97583\">++<\/span><span style=\"color:#E1E4E8\">) {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    hash <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> ((hash <\/span><span style=\"color:#F97583\">&#x3C;&#x3C;<\/span><span style=\"color:#79B8FF\"> 5<\/span><span style=\"color:#E1E4E8\">) <\/span><span style=\"color:#F97583\">-<\/span><span style=\"color:#E1E4E8\"> hash <\/span><span style=\"color:#F97583\">+<\/span><span style=\"color:#E1E4E8\"> host.<\/span><span style=\"color:#B392F0\">charCodeAt<\/span><span style=\"color:#E1E4E8\">(i)) <\/span><span style=\"color:#F97583\">|<\/span><span style=\"color:#79B8FF\"> 0<\/span><span style=\"color:#E1E4E8\">;<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  }<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  var<\/span><span style=\"color:#E1E4E8\"> n <\/span><span style=\"color:#F97583\">=<\/span><span style=\"color:#E1E4E8\"> Math.<\/span><span style=\"color:#B392F0\">abs<\/span><span style=\"color:#E1E4E8\">(hash).<\/span><span style=\"color:#B392F0\">toString<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">36<\/span><span style=\"color:#E1E4E8\">).<\/span><span style=\"color:#B392F0\">slice<\/span><span style=\"color:#E1E4E8\">(<\/span><span style=\"color:#79B8FF\">0<\/span><span style=\"color:#E1E4E8\">, <\/span><span style=\"color:#79B8FF\">6<\/span><span style=\"color:#E1E4E8\">);<\/span><\/span>\n<span class=\"line\"><span style=\"color:#F97583\">  return<\/span><span style=\"color:#E1E4E8\"> {<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    user: <\/span><span style=\"color:#9ECBFF\">'bd_'<\/span><span style=\"color:#F97583\"> +<\/span><span style=\"color:#E1E4E8\"> n,<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">    pass: <\/span><span style=\"color:#9ECBFF\">'Bd@26!'<\/span><span style=\"color:#F97583\"> +<\/span><span style=\"color:#E1E4E8\"> n <\/span><span style=\"color:#F97583\">+<\/span><span style=\"color:#9ECBFF\"> 'x'<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">  };<\/span><\/span>\n<span class=\"line\"><span style=\"color:#E1E4E8\">}<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>El algoritmo generaba nombres con el prefijo <code>bd_<\/code> y seis caracteres de un hash base36, adem\u00e1s de contrase\u00f1as con el patr\u00f3n <code>Bd@26!&lt;hash&gt;x<\/code>. Asociaba la cuenta a una direcci\u00f3n <code>@wordpress.org<\/code>. 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.<\/p>\n\n\n\n<p>Despu\u00e9s intentaba crear el administrador desde <code>\/wp-admin\/user-new.php<\/code>, extra\u00eda el nonce del formulario y enviaba la solicitud como mismo origen. Informaba del resultado al C2 con <code>sendBeacon<\/code> o, como alternativa, con una petici\u00f3n <code>fetch<\/code> POST de tipo <code>no-cors<\/code>, e inclu\u00eda el estado, las credenciales, el origen, la URL de cierre de sesi\u00f3n y la p\u00e1gina actual. Cuando pod\u00eda, guardaba una marca en <code>localStorage<\/code> para no repetir la operaci\u00f3n en ese navegador.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Por qu\u00e9 el compromiso resulta dif\u00edcil de ver<\/h2>\n\n\n\n<p>La explotaci\u00f3n viv\u00eda por completo en la respuesta de una API remota. No requer\u00eda actualizar el plugin, no cambiaba archivos en disco y convert\u00eda el feed de promoci\u00f3n de un proveedor de confianza en el veh\u00edculo de entrega de malware.<\/p>\n\n\n\n<p>Por ese motivo, los esc\u00e1neres que solo comparan archivos con una versi\u00f3n conocida apenas ven se\u00f1ales, y un firewall de aplicaciones web tambi\u00e9n puede pasar por alto gran parte de la cadena. El compromiso del bucket o de las credenciales de infraestructura de BdThemes permiti\u00f3 publicar tanto registros JSON maliciosos como el payload <code>x.js<\/code> dentro de la propia infraestructura del proveedor.<\/p>\n\n\n\n<p>El dominio de C2 se relacion\u00f3 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\u00f1a era mantener acceso administrativo silencioso y ejecutar c\u00f3digo remoto en un n\u00famero amplio de instalaciones de WordPress.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Indicadores de compromiso<\/h2>\n\n\n\n<p>Busca estos recursos externos en registros, contenido inyectado y comunicaciones salientes. Los endpoints de la API del proveedor ya devolv\u00edan JSON limpio el 8 de agosto de 2026, pero su presencia hist\u00f3rica sirve para acotar la exposici\u00f3n.<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>Payload principal: <code>ia-cdn[.]com\/fz\/w2.js<\/code>.<\/li>\n\n\n<li>Endpoint de beacon C2: <code>ia-cdn[.]com\/fz\/c<\/code>.<\/li>\n\n\n<li>Endpoint envenenado: <code>api[.]sigmative[.]io\/prod\/store\/api\/biggopti\/api-data-all-records<\/code>.<\/li>\n\n\n<li>Endpoint envenenado: <code>api[.]sigmative[.]io\/prod\/store\/api\/biggopti\/api-data-records<\/code>.<\/li>\n\n\n<li>Payload alternativo alojado por el proveedor: <code>api[.]sigmative[.]io\/prod\/store\/api\/biggopti\/x.js<\/code>.<\/li>\n\n<\/ul>\n\n\n\n<p>Revisa tambi\u00e9n el sistema de archivos y la base de datos para localizar estos indicadores:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li>La webshell <code>emer-run.php<\/code>, con MD5 <code>1024732009983dd5e54b4cf5593f04d4<\/code>.<\/li>\n\n\n<li>La puerta trasera de inicio de sesi\u00f3n m\u00e1gico <code>class-wp-token-validate.php<\/code>, con MD5 <code>7719cd98a35ffad2771f26d1ceab7d27<\/code>.<\/li>\n\n\n<li>El m\u00f3dulo de ocultaci\u00f3n <code>class-wp-query-9d127ff3.php<\/code>, o nombres similares con el patr\u00f3n <code>class-wp-query-*.php<\/code>, con MD5 <code>9aadc3e5c5242b273bd17c5bdc358845<\/code>.<\/li>\n\n\n<li>El m\u00f3dulo presentado como comprobaci\u00f3n de estado <code>wp-cache-optimizer.php<\/code>, con MD5 <code>e450ae5bc4bfc0d960dded06a76bb8e9<\/code>.<\/li>\n\n\n<li>La opci\u00f3n de base de datos <code>fz_emer_login_tokens<\/code>, que almacena tokens de inicio de sesi\u00f3n m\u00e1gico.<\/li>\n\n\n<li>La opci\u00f3n de base de datos <code>fz_emer_done_v1<\/code>, que indica que el compromiso se complet\u00f3.<\/li>\n\n\n<li>Cuentas con direcciones de correo de los dominios <code>@wordpress.org<\/code> o <code>@developer.wordpress.org<\/code>.<\/li>\n\n\n<li>Nombres de usuario que coincidan con <code>bd_<\/code> seguido de seis caracteres alfanum\u00e9ricos.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Cronolog\u00eda del incidente<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n\n<li><strong>1 de marzo de 2026:<\/strong> Biggopti a\u00f1adi\u00f3 el JavaScript de API a Prime Slider 4.1.9 mediante el commit SVN <code>r3471891<\/code>. <code>display_id<\/code> entr\u00f3 sin escapar en un atributo <code>id<\/code> y el plugin consultaba <code>api-data-records<\/code>.<\/li>\n\n\n<li><strong>10 de mayo de 2026:<\/strong> el endpoint cambi\u00f3 a <code>api-data-all-records<\/code>. Se a\u00f1adi\u00f3 el sanitizador <code>DOMParser<\/code> para el campo de contenido, pero el atributo <code>id<\/code> continu\u00f3 sin escapar.<\/li>\n\n\n<li><strong>23 de junio de 2026:<\/strong> la campa\u00f1a registr\u00f3 esa fecha de inicio en el aviso \u201cSummer Sale\u201d; es la fecha m\u00e1s temprana en la que el XSS pudo estar activo.<\/li>\n\n\n<li><strong>6 de agosto de 2026, 22:40 UTC:<\/strong> \u00faltima modificaci\u00f3n del endpoint envenenado <code>api-data-records<\/code>.<\/li>\n\n\n<li><strong>7 de agosto de 2026, 09:11 UTC:<\/strong> \u00faltima modificaci\u00f3n del endpoint envenenado <code>api-data-all-records<\/code>.<\/li>\n\n\n<li><strong>7 de agosto de 2026:<\/strong> se notific\u00f3 el incidente y se observ\u00f3 el ataque en entornos reales.<\/li>\n\n\n<li><strong>7 de agosto de 2026:<\/strong> el directorio de WordPress cerr\u00f3 los plugins afectados mientras los revisaba.<\/li>\n\n\n<li><strong>7 de agosto de 2026, 16:56 UTC:<\/strong> se capturaron los artefactos de la investigaci\u00f3n.<\/li>\n\n\n<li><strong>8 de agosto de 2026:<\/strong> ambos endpoints de API ya devolv\u00edan JSON limpio.<\/li>\n\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Qu\u00e9 debes revisar ahora<\/h2>\n\n\n\n<p>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\u00f3dulo de ocultaci\u00f3n 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.<\/p>\n\n\n\n<p>Inspecciona los directorios de plugins y de plugins obligatorios en busca de los archivos, patrones y hashes indicados. Revisa las opciones <code>fz_emer_login_tokens<\/code> y <code>fz_emer_done_v1<\/code>, identifica llamadas a los dominios se\u00f1alados en los registros y busca administradores con los patrones de correo y usuario descritos.<\/p>\n\n\n\n<p>Las firmas de malware y reglas de firewall para detectar esta campa\u00f1a 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\u00e1n tras el retraso est\u00e1ndar de 30 d\u00edas.<\/p>\n\n\n<div class=\"references-section\">\n                <h2>Referencias \/ Fuentes<\/h2>\n                <ul class=\"references-list\"><li><a href=\"https:\/\/www.wordfence.com\/blog\/2026\/08\/psa-supply-chain-compromise-in-bdthemes-ecosystem-via-poisoned-api-response\/\" target=\"_blank\" rel=\"noopener noreferrer\">PSA: Supply Chain Compromise in BdThemes Ecosystem via Poisoned API Response<\/a><\/li><li><a href=\"https:\/\/www.wordfence.com\/threat-intel\/vulnerabilities\/id\/7bf1d6f9-afe2-41c2-968b-20dbc474cd73\" target=\"_blank\" rel=\"noopener noreferrer\">Biggopti Library (Various Versions) &#8211; Cross-Site Scripting via display_id from Sigmative API<\/a><\/li><\/ul>\n            <\/div>","protected":false},"excerpt":{"rendered":"<p>Una API de banners promocionales manipulada ejecut\u00f3 XSS en `wp-admin` y abri\u00f3 la puerta a administradores fraudulentos, webshells y puertas traseras persistentes.<\/p>\n","protected":false},"author":17,"featured_media":264,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9],"tags":[135,12,11,10,125],"class_list":["post-265","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-seguridad","tag-cadena-de-suministro","tag-plugins","tag-seguridad","tag-wordpress","tag-xss"],"_links":{"self":[{"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/posts\/265","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/users\/17"}],"replies":[{"embeddable":true,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/comments?post=265"}],"version-history":[{"count":0,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/posts\/265\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/media\/264"}],"wp:attachment":[{"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/media?parent=265"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/categories?post=265"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/helloblog.io\/es\/wp-json\/wp\/v2\/tags?post=265"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}