Une attaque ciblée par confusion de dépendances npm prise en flagrant délit
Snyk Security Research Team
30 avril 2022
0 minutes de lectureChronologie des événements
10 mai 2022 : CodeWhite, une entreprise spécialisée dans la sécurité Red Team, nous a contactés sur Twitter et a revendiqué la création des packages malveillants, expliquant qu’ils faisaient partie d’une simulation d’attaque pour ses clients. Chapeau pour cette attaque élaborée !
4 mai 2022 : DigitalOcean nous a indiqué que l’adresse IP du serveur C2 appartenait à une entreprise spécialisée dans la sécurité et qu’elle allait vérifier la situation et nous tenir au courant.
1er mai 2022 : npm a supprimé les packages malveillants de son registre.
L’histoire d’un malware sur npm
Ces dernières années, nous avons constaté une augmentation constante du nombre de packages malveillants apparaissant dans différents écosystèmes. De manière générale, la grande majorité de ces packages sont inoffensifs : ils collectent des informations, mais ne nuisent pas à la machine infectée. Il nous arrive toutefois de découvrir un package véritablement malveillant, doté d’un objectif, de moyens et prêt pour la production. Voici l’histoire de l’un d’eux.
Découvrir un véritable malware dans npm
Dans le cadre des travaux de l’équipe de recherche en sécurité de Snyk, axés sur la détection proactive des packages malveillants, notre principal objectif est d’analyser les registres des écosystèmes et de signaler les packages malveillants dès que possible après leur publication. Pour cela, nous avons mis au point un système robuste doté de différents mécanismes de détection.
Peu après avoir mis en place notre dispositif de détection des packages malveillants, de nombreuses bibliothèques nous ont été signalées. Cependant, en examinant les résultats, nous avons constaté que beaucoup de signalements concernaient des packages « légèrement malveillants ».
Par « légèrement malveillant », nous entendons un package qui fait l’une des choses suivantes :
Exfiltrer des informations sur la machine au moyen de requêtes DNS (sans autre action)
Héberger des mineurs de cryptomonnaies (ce qui est mauvais, mais pas vraiment intéressant du point de vue de la malveillance)
Être un package « légèrement malveillant » créé par un autre chercheur, principalement à des fins de test
Présenter d’autres variantes des éléments de cette liste
Nous avons fini par dénicher un package très intéressant — gxm-reference-web-auth-server — que nous avons marqué comme malveillant. Nous y avons jeté un rapide coup d’œil et avons senti que quelque chose clochait. Nous avons donc lancé une machine virtuelle et examiné l’archive tarball.
Comme le signalement concernait un package npm, nous avons commencé par examiner le fichier `package.json`. Sans surprise, il contenait un script post-install qui exécutait un fichier JavaScript du package. Si vous ne connaissez pas les scripts post-install, ils servent très couramment à exécuter des scripts une fois l’installation des dépendances concernées terminée par npm. C’est aussi un moyen facile pour les acteurs malveillants d’exécuter des scripts sur les machines de leurs victimes.
En examinant le fichier exécuté, nous l’avons trouvé obfusqué. L’obfuscation est une manière apparemment sophistiquée de « cacher » le code, mais en réalité, il est généralement assez facile de revenir au code d’origine (du moins en JavaScript).
Le fichier suivant du package a immédiatement attiré notre attention : un fichier chiffré. Nos sens d’araignée se sont mis à frémir : il était temps de creuser.
Rétro-ingénierie d’un malware, partie 1 : le wrapper
Comme indiqué, le package contenait deux fichiers supplémentaires : l’un obfusqué, l’autre chiffré.
Le fichier package.json contenait également un script post-install :
Nous nous sommes d’abord intéressés au script exécuté, mais nous avons aussi remarqué deux dépendances au nom incompréhensible, ldtzstxwzpntxqn et lznfjbhurpjsqmr. Nous les examinerons un peu plus loin dans cet article.
En examinant le fichier confsettingsaaa.js, nous avons vu ceci :
Pour comprendre ce fichier, nous avons utilisé un outil de désobfuscation afin de le rendre lisible. En parcourant le code, nous avons constaté que le fichier (que nous appellerons plus loin P1, puisqu’il s’agit de la première partie du malware) exfiltrait des informations sur le système d’exploitation et le package au moyen de requêtes vers de faux sous-domaines de pkgio[.]com :
Ce code se limite à l’exfiltration. On pourrait penser qu’il ne fait aucun mal, mais ces exfiltrations sont en réalité précieuses pour la reconnaissance de l’attaquant : elles lui permettent d’identifier la cible et sa configuration. Si les acteurs malveillants abusent des requêtes DNS, c’est parce que ce type de requêtes est généralement autorisé par les pare-feu et les filtres réseau. Ils peuvent ainsi envoyer des requêtes à leurs propres serveurs DNS et en consigner les demandes.
La suite du fichier s’est révélée encore plus intéressante. D’abord, tous les blocs try/catch appelaient une fonction `fail` lorsqu’ils interceptaient une exception. Ensuite, nous avons vu que le script utilisait l’une de ses dépendances : lznfjbhurpjsqmr.
Avant d’expliquer le rôle des deux dépendances, examinons d’abord la fonction (ou le mécanisme) fail. Lorsqu’elle est appelée, cette fonction effectue principalement deux opérations. D’abord, elle efface ses traces : la fonction « fail » supprime tous les fichiers associés au package et nettoie le système.
Ce mécanisme est astucieux pour un malware, mais l’étape suivante était inquiétante.
Après le nettoyage, la fonction crée des fichiers leurres package.json et index.js, puis affiche le message suivant dans la console : « Please refer to the private registry instead of the public repo; Security Team »
Après avoir lu ce message, plusieurs questions nous sont venues à l’esprit : pourquoi le package malveillant ne se supprime-t-il pas complètement ? Pourquoi se fait-il passer pour un espace réservé légitime créé par une équipe de sécurité ? Quelle organisation ce package vise-t-il ?
Nous avons pu répondre à la plupart de ces questions. Malgré tous nos efforts pour identifier la cible et l’alerter, nous ne savons toujours pas, à ce jour, quelle organisation est visée. Gardons ces questions à l’esprit et poursuivons.
Les dépendances : ldtzstxwzpntxqn et lznfjbhurpjsqmr
Comme indiqué plus haut, le package installe deux dépendances : ldtzstxwzpntxqn et lznfjbhurpjsqmr. En consultant leurs pages npm, nous avons constaté que les deux avaient été publiées par le même mainteneur. Voici à quoi ressemblait sa page :

C’était intéressant à constater et cela confirmait certains de nos soupçons concernant ces packages. Les dépendances elles-mêmes n’étaient toutefois que de simples copies de packages légitimes :
Nom du package | Package d’origine | Utilité |
|---|---|---|
|
| Package qui fournit une API simplifiée pour npm install (installe des éléments par programmation). |
|
| Permet d’utiliser npm global comme module Node local. |
Comme nous l’avons confirmé plus tard en auditant le reste du code, ces packages étaient effectivement utilisés conformément à la fonction prévue des packages d’origine. Pourquoi l’acteur s’est-il donné la peine de créer ces packages plutôt que d’utiliser les originaux ? Nous ne le savons pas et ne le saurons probablement jamais.
Filtrage des victimes, registres privés et transition vers P2
Quelques remarques rapides…
Dans la section suivante, lorsque nous écrivons « le code s’interrompt », cela signifie que la fonction
faildécrite plus haut a été appelée pour effectuer le nettoyage.Comme nous ne savons pas quelle organisation est visée, nous l’appellerons
ORG.
L’étape suivante de P1 consiste à récupérer la configuration d’autorisation des registres privés dans le fichier .npmrc (c’est là qu’intervient lznfjbhurpjsqmr). Si le code ne trouve ni ce fichier ni une telle configuration (il effectue une recherche sur toute la machine), il s’interrompt.
S’il trouve ces informations d’autorisation, il tente de télécharger dans le registre privé indiqué dans le fichier de configuration le package portant le même nom, en essayant plusieurs variantes, par exemple :
Si le package est introuvable dans le registre privé ou si le téléchargement échoue, le code s’interrompt. S’il parvient à télécharger l’archive tarball depuis le registre privé, il effectue les opérations suivantes :
1. Installer le module avec npm install dans un sous-répertoire appelé .documentation (l’installation se fait à l’aide de la dépendance ldtzstxwzpntxqn), faute de quoi le code s’interrompt.
2. Remplacer le contenu actuel du package par celui du package nouvellement installé, ou abandonner.
3. Exfiltrer le contenu de deux fichiers liés au réseau ainsi que le nouveau fichier package.json via une requête POST :
4. Déchiffrer le fichier chiffré et l’exécuter dans un nouveau processus détaché (l’algorithme de chiffrement, la clé et le vecteur d’initialisation IV étaient codés en dur. Nous reviendrons sur ce fichier dans la section suivante) :
5. Nettoyer tous les fichiers concernés (cette étape supprime uniquement les fichiers chiffrés et le code de P1).
6. Terminer.
Maintenant que nous avons détaillé ces étapes, nous pouvons constater que si une personne ne possède pas le package gxm-reference-web-auth-server dans son registre privé, ou si celui-ci est inaccessible, le malware passe simplement à la suite.
Cela signifie que :
Le package vise une organisation précise.
L’acteur à l’origine de ces packages connaît l’existence de ce package dans le registre privé de l’organisation.
Nous avons ainsi décrit toutes les étapes de P1. Le « wrapper du malware » va maintenant s’arrêter.
Rétro-ingénierie d’un malware, partie 2 : l’agent
Comme le wrapper contenait les informations codées en dur nécessaires au déchiffrement du fichier (une erreur de l’adversaire, selon nous), nous avons pu le déchiffrer également. Nous l’avons donc fait.
Tout comme le wrapper, ce fichier était obfusqué et se présentait sous la forme d’une seule ligne incompréhensible. Cette fois, cependant, l’outil de désobfuscation a eu plus de mal et a laissé le code truffé d’énigmes :
Nous avons essayé plusieurs autres outils de désobfuscation, mais aucun n’a donné de meilleur résultat. Nous avons conservé le résultat initial et commencé à désobfusquer le code manuellement. Après quelques efforts, nous avons réussi à le rendre lisible. Il était temps d’examiner le fonctionnement de l’agent.
Enregistrement de l’agent
La première instruction donnée à l’agent est de s’enregistrer auprès du serveur de commande et de contrôle (appelé CNC ou C2). L’agent reçoit ainsi trois chaînes de caractères essentielles aux communications ultérieures : une clé et un vecteur d’initialisation IV pour chiffrer les charges utiles, ainsi qu’un UUID.
Une fois cette requête terminée, toutes les communications ultérieures entre le serveur C2 et l’agent sont chiffrées et déchiffrées à l’aide de cette clé et de cet IV (l’algorithme était codé en dur). Juste après cette requête, l’agent envoie au serveur une requête POST contenant des informations sur son environnement :
Une fois cette requête envoyée, l’agent passe à l’étape suivante.
La boucle d’exécution
La boucle d’exécution de l’agent est assez simple. Elle contient quelques instructions if/else qui correspondent à différentes commandes et s’exécute en fonction des indications du serveur C2. Par exemple, l’agent peut s’autosupprimer s’il reçoit une commande delete, ou évaluer un extrait de code envoyé par le serveur C2 :
Au total, l’agent réagit aux commandes suivantes :
Comme le montre cette liste, les commandes exec et eval peuvent exécuter un shell inversé qui donne à l’attaquant le contrôle total de la machine infectée. Notez également que l’option register, lorsqu’elle est appelée à nouveau, permet à l’agent et au serveur C2 de renouveler leurs informations de chiffrement et d’en générer de nouvelles, s’ils souhaitent les modifier.
Cette fonctionnalité peut sembler originale ou sophistiquée, mais dans le monde des agents C2, elle est standard pour un agent que l’on installe sur la machine d’une victime. Quoi qu’il en soit, nous n’avons pas pu établir de lien entre le code source de l’agent et des frameworks C2 connus.
Conclusion sur le malware
Maintenant que nous avons une compréhension complète du malware, voici nos conclusions :
Le malware vise une seule entreprise, dont l’identité reste inconnue. Toutefois, d’après les éléments obtenus lors de la rétro-ingénierie, cette entreprise devrait avoir le package « gxm-reference-web-auth-server » dans son registre privé.
Puisque le package se recherche lui-même dans le registre privé de la victime, nous pouvons également qualifier cette attaque de confusion de dépendances, un type d’attaque de la chaîne d’approvisionnement.
L’attaquant, ou les attaquants, avait probablement connaissance de l’existence d’un tel package dans le registre privé de l’entreprise.
À ce stade, même si nous comprenions clairement le fonctionnement du package, nous avons décidé de vérifier si le serveur C2 répondait et s’il s’agissait d’une campagne encore active. Il était temps de nous faire passer pour quelqu’un d’autre.
Jouer à « Among Us » avec un adversaire
L’idée de notre expérience suivante était simple. Nous voulions savoir si quelqu’un se trouvait à l’autre bout de la ligne et, le cas échéant, s’il était actif. Pour cela, nous devions procéder comme suit :
Utiliser l’agent sans le wrapper (P1 filtrerait notre client en l’absence d’identifiants dans .npmrc, etc.)
Intercepter et consigner tout le trafic HTTP/S (nous voulions voir ce que le serveur C2 envoie et reçoit)
Nous transmettre les données consignées de manière unidirectionnelle, irréversible et intraçable (c’est important, car l’attaquant peut lui aussi accéder à la machine !)
Mais avant de nous lancer, nous voulions recueillir quelques informations sur le serveur lui-même.
Y a-t-il quelqu’un ?
Nous avons recueilli des informations sur le serveur à l’aide de requêtes WHOIS et d’analyses Nmap classiques. Les résultats WHOIS indiquaient que le serveur était hébergé chez DigitalOcean (à qui nous avons signalé l’adresse IP du serveur), et les analyses Nmap ont révélé ce qui suit :
Le serveur n’est pas vraiment sécurisé et il est actif et à l’écoute.
L’imposteur
Pour revenir à notre plan, nous devions préparer les éléments suivants :
Code de l’agent : Nous l’avons récupéré lors de la phase de déchiffrement et pouvons l’exécuter séparément du wrapper (P1).
Intercepteurs : Ils devaient être implémentés dans l’environnement NodeJS, puisque l’agent utilisait lui-même les modules HTTP/S de NodeJS.
Pipeline de journalisation : Nous avons choisi Pipedream, qui permet de gérer les requêtes HTTP/S avec une grande précision et propose déjà plusieurs intégrations prêtes à l’emploi (stockage de valeurs, intégration Slack, et bien plus).
Pour l’interception, nous avons utilisé la bibliothèque @gr2m/http-recorder, qui faisait exactement ce que nous recherchions : capturer et manipuler intégralement les requêtes HTTP/S.
En combinant les étapes 1 et 2, nous avons obtenu l’extrait suivant :
Il ne restait plus qu’à nous envoyer les données. Avec Pipedream, nous avons configuré la fonction send pour transmettre la charge utile à notre point de terminaison afin que nous puissions la traiter.
Pour le traitement, nous avons stocké le vecteur d’initialisation (IV) et la clé afin de pouvoir déchiffrer les messages. Nous avons également configuré le pipeline Pipedream pour l’intégrer à un espace de travail Slack anonyme et y consigner les messages. Ainsi, chaque fois que l’agent et le serveur C2 échangeaient des messages, nous recevions une notification Slack contenant toutes les informations déchiffrées et lisibles. Voici une illustration de la configuration de notre machine infectée :

Voici un exemple du résultat dans Slack :

Et voilà, notre pseudo-pot de miel était prêt.
Allô, c’est moi
Peu après que notre faux agent a été activé et a commencé à communiquer avec le serveur C2, les commandes ont commencé à arriver. La première était un ls, suivi d’une tentative de l’auteur de la menace d’exécuter cat sur tous les fichiers visibles et de parcourir le système. Voici un exemple de commande reçue (après décodage du base64) :

À ce stade, nous avons décidé d’interrompre notre expérience, car nous avions atteint notre objectif : déterminer si quelqu’un se trouvait à l’autre bout de la ligne.
Cela laisse penser qu’une campagne est en cours contre les propriétaires du dépôt privé d’origine « gxm-reference-web-auth-server ».
Mais ce n’est pas tout
Juste avant de conclure notre expérience, nous avons remarqué un élément intéressant. Outre l’adresse C2, quelques autres valeurs étaient codées en dur. L’une d’elles était la valeur de la propriété « engine » dans l’appel initial /register :
Comme nous ne cherchions plus à passer inaperçus, nous avons décidé de faire varier cette valeur pour voir s’il existait d’autres types de cibles. Après quelque temps, nous avons découvert d’autres types de moteurs :
Cela indique qu’il existe au moins un agent de navigateur et un agent Golang, et il est tout à fait possible qu’il y en ait d’autres.
Divulgation et prochaines étapes
Bien que nous connaissions le fonctionnement interne de ce malware, nous ne pouvons pas déterminer quelle organisation ou entreprise est ciblée. Nous souhaitons donc profiter de cette plateforme (ainsi que de Twitter, etc.) pour demander à la communauté de signaler ce package et de prévenir les autres. N’hésitez pas à nous contacter (ou à nous envoyer un message privé à @snyksec) si vous avez des questions ou des préoccupations à ce sujet.
Nous avons également contacté DigitalOcean pour leur demander de retirer le serveur C2 de leur service, ainsi que npm pour les informer de l’existence des packages malveillants et leur demander de les supprimer.
Voilà qui conclut notre enquête. Les attaques étant complexes et sophistiquées, nous tenons à souligner que le vecteur d’infiltration initial est une simple confusion de dépendances de packages, facile à atténuer. Pour découvrir le déroulement complet de cette attaque, consultez l’image ci-dessous. Prenez soin de vous et restez en sécurité !

Sécurisez vos dépendances open source
Les outils Snyk, conçus pour les développeurs, génèrent en un clic des pull requests correctives pour vos dépendances open source vulnérables et leurs dépendances transitives.
