Retour à tous les articles
Publié le · par Renaud Deraison

Il demandait à être exécuté

Sonatype suit 846 paquets npm malveillants publiés depuis des centaines de comptes jetables sous des noms générés par IA. Aucun ne porte de script d’installation. Le README vous dit de charger le paquet avec require(), et l’import exécute le loader. Les défenses qui reconnaissent un mauvais paquet à son nom, à son auteur ou à son hook déclaré courent désormais après un générateur. Bromure Agentic Coding bloque sur la seule propriété qu’un générateur ne peut pas falsifier : l’âge du paquet.

Les paquets ne portent aucun hook d’installation, donc npm n’a rien à intercepter. Le README vous dit de les charger avec require(), et l’import exécute le loader. De plus en plus, le lecteur qui suit cette instruction est un agent de code.

Sonatype Research Labs a divulgué le 5 août une campagne qu’il appelle Flooding Dropper, comptant 846 composants npm malveillants et le compte grimpe encore. Paul McCarty, chez OpenSourceMalware, suit la charge utile sous le nom WEL1DROPPER. La couverture plus large est parue le 7 août.

Deux détails la distinguent des affaires de supply chain npm du mois écoulé.

Le premier, c’est la façon dont les paquets obtiennent leur nom. L’attaquant les génère, en faisant tourner le slopsquatting comme une chaîne de production. Des radicaux récurrents comme bigops et bnpl se recombinent avec d’autres mots pour donner des noms tels que bigops-api et dolyame-boxy-desktop-bnpl-card-gallery, beaucoup partageant un motif de version 35.x.y. La publication se répartit sur des centaines de comptes jetables portant une poignée de paquets chacun, si bien qu’il n’y a aucun auteur prolifique à bannir.

Pas de script d’installation

Les paquets ne déclarent aucun hook de cycle de vie, donc npm install n’exécute rien. L’article de McCarty le dit sans détour : « Les paquets NPM n’utilisent pas de script preinstall ou postinstall. Ils n’en ont pas besoin. »

Ils embarquent un _helpers.js bundlé qui s’exécute à l’import du module, accompagné d’un README qui dit au lecteur de l’importer. Chargez le paquet avec require(), dit la documentation, et la documentation dit vrai. Le premier étage prend l’empreinte du système d’exploitation et de l’architecture du processeur, récupère un second étage natif en HTTPS depuis l’un de trois hôtes Cloudflare Workers, et se rabat sur la reconstitution du binaire à partir de fragments base64 logés dans des enregistrements DNS TXT sous wel1.ru quand la récupération directe échoue.

Le second étage varie selon la plateforme. Linux reçoit un ELF compressé avec UPX qui déploie un implant C2 Sliver. macOS reçoit des contrôles anti-débogage, une sonde VMware, un binaire balise et une persistance par LaunchAgent. Windows reçoit du patching ETW et AMSI, de la détection de bac à sable, de l’exécution réflective en mémoire et une persistance via une clé Run du Registre et une tâche planifiée. McCarty et Sonatype lisent tous deux la campagne comme une évolution de « Moika », la vague de confusion de dépendances d’avril qui avait poussé quelque 250 paquets bâtis autour du vol d’environnement.

L’attaquant a déplacé le déclencheur du gestionnaire de paquets vers le lecteur.

Trois façons d’être méconnaissablele nom est générébigops-api, dolyame-boxy-desktop-bnpl-card-gallery, et ~844 autresune liste de blocage veut le nomle générateur en a d’autresl’éditeur est jetabledes centaines de comptes,quelques paquets chacunbannir l’auteur en bannit quatresur huit centspas de script d’installationni preinstall, ni postinstallle README dit : faites require()_helpers.js part à l’importle déclencheur, c’est le lecteurl’import exécute le loaderempreinte OS et arch, puis étage natifCloudflare Workers, ou DNS TXT sous wel1.ru
La campagne est construite pour déjouer la reconnaissance. L’attaquant génère les noms au lieu de les choisir, fait tourner les éditeurs sur des centaines de comptes jetables et a déplacé le déclencheur d’exécution du hook de cycle de vie vers une instruction de README, ne laissant aucun script d’installation à inspecter et aucun nom ni auteur stable auquel accrocher une liste de blocage.

L’automatisation bat la liste de blocage

Le résumé de Sonatype lui-même est la phrase la plus tranchante de toute la couverture :

Lisez cela comme une déclaration sur les défenses. Toute couche qui vous protège en reconnaissant le mauvais paquet, que ce soit par son nom, par son compte de publication ou par son hook déclaré, affronte désormais un générateur. Ces couches attrapent toujours ce qu’elles ont déjà vu. Elles sont notées sur leur capacité à reconnaître une chaîne précise, et la fourniture de chaînes précises coûte le temps d’exécuter un script.

L’angle agentique aiguise le tout. Le slopsquatting est rentable au départ parce que les modèles de langage inventent des noms de paquets qui sonnent juste et qui n’existent pas, et un attaquant qui enregistre ces noms récolte du trafic d’installation gratuitement. Ajoutez le hook d’installation manquant et vous obtenez un paquet dont le déclencheur est une instruction écrite dans un README. Un agent de code est le lecteur qui va halluciner le nom, trouver un paquet posé dessous, lire la documentation et faire ce que la documentation dit. La campagne est taillée pour l’agent aux deux bouts.

Choisissez une propriété que le générateur ne peut pas falsifier.

L’âge n’est pas un nom

Chaque paquet de cette campagne partage une propriété qu’aucune automatisation ne change : il est neuf. Un nom qu’un modèle a inventé mardi dernier ne peut pas avoir sous lui un paquet datant de 2019. Des centaines de comptes jetables publient des centaines de versions flambant neuves, parce qu’un compte jetable n’a aucun historique d’où publier. Le modèle dépend du fait que le paquet soit là au moment où quelqu’un le réclame, donc l’attaquant le crée quelques jours à l’avance tout au plus.

Bromure Agentic Coding exécute l’agent dans une VM Linux jetable sur Apple Silicon et route chaque octet de son trafic réseau par un proxy sur l’hôte, hors de la boîte où tourne l’agent. Les récupérations de paquets pour npm, PyPI, Cargo, RubyGems, Maven, NuGet, les modules Go et Packagist traversent toutes ce proxy, et la première politique qu’elles rencontrent est une horloge.

La barrière d’âge, active par défaut

Bromure refuse les paquets plus jeunes qu’un âge minimum, fixé à deux jours à la sortie de la boîte. Les références flottantes comme latest ou une plage semver se résolvent vers la version la plus récente antérieure au seuil, si bien que les installations ordinaires continuent de fonctionner et atterrissent sur la version d’hier. Une référence épinglée vers une version trop fraîche reçoit un 451 avec une erreur Bromure claire qui nomme ce qui s’est passé. Huit cents paquets coûtent aussi cher à arrêter que huit, parce que Bromure n’en reconnaît aucun.

Elle s’applique avant que le débat commence

La partie astucieuse de cette campagne, abandonner le hook de cycle de vie et déplacer l’exécution vers require(), décrit ce qui se passe après que le tarball a atteint le disque. La barrière d’âge se tient à la récupération, dans le proxy de l’hôte, en amont de tout cela. Savoir si la charge utile se serait exécutée à l’installation ou à l’import est une question qui porte sur un paquet qui n’est jamais arrivé. Les .npmrc et pip.conf dans la VM peuvent durcir cette politique et ne peuvent pas l’assouplir.

La réputation, derrière l’horloge

Avec une clé socket.dev, Bromure vérifie chaque paquet dans la base de socket.dev avant de laisser passer la récupération et bloque ceux marqués comme compromis, malveillants ou typosquats. Les recherches OSV bloquent les paquets porteurs de vulnérabilités connues à partir d’une sévérité que vous choisissez. Delpi redirige purement et simplement chaque requête npm vers un registre filtrant. Ces couches reconnaissent des choses, ce qui les rend utiles et les place en second.

Le README est noté pour ce qu’il est

« Chargez ceci avec require() » est une instruction écrite par un inconnu, qui arrive dans du texte que l’agent lit. Bromure note les sorties d’outils que l’agent consomme, y compris le contenu de fichiers, les pages web et la documentation récupérée, avec un modèle local d’injection de prompt. Il note les fichiers d’instructions que l’agent traite comme faisant autorité, CLAUDE.md et AGENTS.md et leurs variantes, avec un classifieur dédié plus un scanner de charges utiles en Unicode invisible. Rien ne quitte le Mac pour le faire.

La charge utile profile la mauvaise machine

Supposons que la récupération passe quand même, parce que le paquet a dépassé le seuil d’âge ou qu’une entrée d’allowlist l’exempte. Suivez WEL1DROPPER à travers un profil Bromure et regardez-le faire son travail contre une machine que vous alliez jeter.

Le premier geste du loader est de prendre l’empreinte de l’hôte. Dans un profil Bromure, l’hôte dont il prend l’empreinte est une VM Ubuntu en NAT, à un hyperviseur de macOS, il prend donc la branche Linux et y plante un implant Sliver. Il ne demande jamais la branche macOS : l’anti-débogage, la sonde VMware, le téléchargement de beacon_mac.bin, le LaunchAgent qui survivrait à un redémarrage, l’intérêt pour les domaines de paiement russes. Tout cela est du code pour une machine que le dropper n’a jamais vue. Erase home jette la couche dans laquelle il a atterri et remet le répertoire personnel dans son état d’après clonage.

Puis l’étage infostealer part chercher ce que son ancêtre Moika allait chercher : l’environnement. Dans un profil Bromure, l’environnement contient des placeholders brm_…. Aucune des vraies clés Anthropic, OpenAI et xAI, aucun des tokens GitHub et GitLab, aucun matériel AWS ni identifiant de registre ne se trouve dans la mémoire de la VM ni dans ses dotfiles. Bromure les garde sur l’hôte et les substitue dans la requête au niveau du proxy, sur le fil, pour l’unique hôte de destination auquel chacun appartient. Le vol s’exécute jusqu’au bout et expédie des chaînes qui ne résolvent que sur un seul Mac, lequel n’est pas la machine qui a exécuté le code.

Sur le Mac du développeuril prend l’empreinte de l’hôtemacOS, arm64 : va sur la branche macbeacon_mac.bin, LaunchAgent, persistantil lit l’environnementclés API, identifiants git, matériel cloudtout est réel, tout est utilisableet il restela machine qu’il possède est la vôtreDans un profil Bromurela récupération croise une horlogepublié aujourd’hui, seuil deux jours : 451aucun nom reconnu, aucune signatureet s’il arrive quand mêmeLinux en VM, branche mac inutiliséel’environnement ne tient que des brm_et il ne reste pasErase home réinitialise la couche visée
Le dropper choisit sa charge utile d’après l’hôte qu’il trouve. Dans un profil Bromure, cet hôte est une VM Linux jetable : il ne demande jamais la branche macOS avec sa persistance LaunchAgent, sa sonde VMware et son binaire balise, et l’étage infostealer lit des identifiants placeholder dont les vraies valeurs ne sont jamais entrées dans la VM.

La chasse qu’ils recommandent, déjà en cours

McCarty clôt l’article WEL1DROPPER par des conseils défensifs : chercher les requêtes DNS TXT vers wel1.ru, examiner les logs de proxy pour du trafic vers les hôtes Cloudflare Workers, et guetter les fichiers temporaires et les LaunchAgents égarés.

Relisez le deuxième point. Votre portable ne tient pas de log de proxy. Un profil Bromure en tient un par construction : chaque requête que fait la VM traverse le proxy de l’hôte, et le Security Log et le Trace Inspector enregistrent l’hôte, le statut, la latence et le rapport de substitution de chacune. Trois destinations *.workers.dev inconnues qui apparaissent pendant un build, ce sont trois lignes sur une liste que vous pouvez lire, sur la machine où le build a tourné, sans rien déployer pour les obtenir.

Les registres continueront de retirer ces paquets, et les retraits fonctionnent : 846 d’entre eux ont disparu ou sont en train de disparaître. Mais la campagne dépasse déjà le millier, publiée depuis des comptes qui ne coûtent rien à créer sous des noms qui ne coûtent rien à inventer, et la prochaine vague est à une exécution de script. La reconnaissance est le mauvais concours à disputer.

Un paquet de deux jours ne mérite pas une confiance profonde qui manquerait à un paquet de deux heures. Il se situe hors de la fenêtre dans laquelle opère cette économie, et vous pouvez déplacer votre graphe de dépendances derrière cette fenêtre avec un réglage qui est déjà actif.

Installez Bromure Agentic Coding, et laissez la récupération croiser une horloge avant de croiser votre machine.