TL; DR
Un groupe de cinq packages npm, publié sur deux comptes différents, a expédié un postinstall Un hook qui récupère les identifiants cloud depuis l'hôte et les envoie hors du système. Les paquets portent des noms fantômes et pirates. coral-wraith, ecto-corsair-whisper-6f3b9, ecto-corsair-flag-x9m4, ecto-rust-read-f3a9c1, ecto-nightly-spirit — et transformer en faux tout ce qu'ils collectent ecto_module: Manifeste YAML avant transmission. Nous suivons le cluster comme Ectoplasme.
La charge utile ne s'exécute que lorsqu'elle détecte un environnement spécifique : un hôte dont le nom est un chaîne hexadécimale de 12 caractères et un répertoire de travail sous /app/node_modules — la forme d'une construction conteneurisée ou d'un worker CI. Lorsque cette étape est franchie, le hook interroge le Service de métadonnées d'instance AWS (IMDSv2) pour les informations d'identification du rôle IAM, énumère AWS Secrets Manager sur trois régions, extrait les variables d'environnement, lit les fichiers sous /appet récupère les chaînes de caractères de type « capture de drapeau ». Il exfiltre ensuite le résultat de deux manières : une balise vers un webhook.site collecteur et une requête PUT manifeste vers un point de terminaison d'adresse IP brute, avec une liste de replis privilégiant localhost.
Les descriptions ultérieures du package indiquent « Charge utile CTF pour les tests de la chaîne d'approvisionnement de Verdaccio ». Nous rapportons cette auto-description comme un fait observable. Le comportement lui-même — sortie réelle vers une adresse IP publique, lectures réelles d'identifiants IMDS, appels réels au Gestionnaire de secrets — est ce qu'il est indépendamment de l'étiquette, et c'est la raison pour laquelle ces versions ont été classées comme malveillantes.
Un nom dans le groupe, coral-wraith, ne s'est pas arrêté à une seule publication. Il a été republié en succession rapide à travers des dizaines de versions en quelques heures seulement — 1.0.0 grimper à 6.0.0 — et une précédente édition du même nom avait utilisé des chiffres gonflés 9999.0.x numéros de version, la forme classique d'un tentative de confusion de dépendanceAu fil de cette évolution, la charge utile a visiblement mûri : d’une balise d’énumération d’hôtes ponctuelle à un pivot complet d’identification AWS enveloppé dans des contrôles d’environnement qui la maintiennent silencieuse en dehors de sa cible prévue.
| Forfaits | 5 noms ; coral-wraith seul republié dans des dizaines de versions |
| Écosystème | NPM |
| Installer le vecteur | postinstall script de cycle de vie |
| Cible principale | Informations d'identification du rôle AWS IAM + valeurs secrètes de Secrets Manager, variables d'environnement, /app fichiers |
| exfiltrer | webhook.site balise + C2 PUT IP brute |
| Porte de déclenchement | Nom d'hôte hexadécimal à 12 bits + /app/node_modules cwd, plus une vérification de l'environnement qui supprime la charge utile en dehors de ce contexte |
| Gravité | Élevée — divulgation d'identifiants cloud et de secrets gérés à partir de environnements de construction et d'exécution conteneurisés |
Anatomie de l'attaque
Chaque paquet du cluster est construit de la même manière : un paquet presque vide index.js (module.exports = {}), une ligne package.json scénario - « postinstall » : « node postinstall.js » — et la charge utile dans postinstall.jsL'installation du package suffit pour exécuter le hook ; aucune importation ni aucun appel n'est requis.
La porte de ciblage. Avant toute chose, la charge utile de la famille ecto vérifie son environnement :
function isAppWorker(): host = os.hostname() if host does NOT match /^[0-9a-f]{12}$/ -> exit if cwd does NOT contain "/app/node_modules" -> exit if cwd contains "/tmp/npm-safe" -> exit otherwise -> proceed Un nom d'hôte de 12 caractères hexadécimaux est la forme par défaut que Docker attribue à un conteneur, et /app/node_modules est un chemin d'installation classique dans un conteneur. La troisième clause s'interrompt si le chemin ressemble à un répertoire d'extraction isolé. En conséquence, la charge utile reste inactive sur l'ordinateur portable du développeur ou dans un environnement d'analyse isolé et ne s'active que dans un environnement de compilation ou d'exécution conteneurisé — le type d'environnement le plus susceptible de contenir des identifiants cloud actifs. Le premier paquet du cluster, spectre corallien, ne possède pas une telle porte et exécute sa collection (plus simple) sans condition.
CollectionLorsque la porte passe, le crochet sort par le haut. execFileSync(« /bin/sh », [« -c », …]) et exécute une seule commande composée qui, dans l'ordre :
1. PUT /latest/api/token to 169.254.169.254 (IMDSv2 token request) 2. GET .../iam/security-credentials/ (IAM role name) 3. GET .../iam/security-credentials/<role> (temporary credentials) 4. dump env | sort (environment variables) 5. list /app (excl. node_modules) + cat first 15 (application files) 6. aws secretsmanager list-secrets (us-east-1, eu-west-1, eu-central-1) 7. scrape readable files for HTB{...} (capture-the-flag strings) Les étapes 1 à 3 constituent une procédure classique de récupération IMDSv2 : demander un jeton de session, puis l’associer comme… X-aws-ec2-metadata-token En-tête permettant de récupérer le rôle IAM de l'instance et les clés d'accès temporaires de ce rôle. Le choix d'implémenter IMDSv2 plutôt que le plus simple IMDSv1 non authentifié ÉCONOMISEZ Il est important de noter que cela signifie que la charge utile fonctionne même sur les instances configurées pour exiger un accès aux métadonnées basé sur un jeton, ce qui constitue le renforcement de sécurité recommandé par AWS. Les informations d'identification renvoyées à l'étape 3 ont une durée de vie limitée. ID de clé d'accès/Clé d'accès secrète/Token Les triplets sont limités au rôle de l'instance ; tout ce que ce rôle peut faire, le détenteur de ces clés peut le faire pendant toute la durée de vie des informations d'identification.
Les étapes 4 à 6 élargissent la zone de prise. env La commande `dump` capture tout ce que le processus de compilation ou d'exécution a hérité ; en pratique, c'est là que se trouvent le plus souvent les jetons d'enregistrement, les chaînes de connexion à la base de données et les clés API. / app La fonction de parcours de fichiers lit jusqu'à quinze fichiers d'application en dehors de l'environnement. node_modules, qui peut faire surface en configuration, .env fichiers ou source. L'étape 6 appelle liste des secrets AWS SecretsManager dans trois régions ; les informations d’identification récupérées aux étapes 1 à 3 servent précisément à authentifier ces appels, de sorte que la lecture IMDS et l’énumération du gestionnaire de secrets s’enchaînent en une seule escalade : rôle de l’instance → inventaire des secrets gérés. L’étape 7 fait référence au principe de capture du drapeau — lorsqu’un HTB{…} Si le drapeau est détecté, il est envoyé seul ; sinon, le bloc brut collecté est divisé en quatre morceaux et envoyé.
Les versions ultérieures du cluster poussent l'escalade plus loin. Au lieu de s'arrêter à un inventaire, elles analysent la réponse IMDS et exportent les clés temporaires en tant que AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / Jeton de session AWS variables d'environnement, confirmez l'identité avec AWS STS Get-Caller-Identity, puis parcourez chaque secret renvoyé par liste-secrets appel aws secretsmanager obtenir-valeur-secrète sur chacun d'eux — en récupérant le contenu secret, et non seulement leurs noms. Les mêmes versions lisent également les binaires de drapeau substitués par processus (/lire le drapeau et des amis) et tenter un course de fret contre tout projet Rust trouvé sous / app, élargissant ainsi la récolte au-delà des identifiants cloud pour inclure tout ce que l'environnement de construction expose.
Ces versions ultérieures imposent également des restrictions plus strictes. En plus du nom d'hôte hexadécimal à 12 bits et /app/node_modules Lors des vérifications, la charge utile examine la configuration du registre de paquets actif et le chemin du répertoire de travail, et se termine silencieusement si elle détecte un contexte d'analyse ou de mise en miroir plutôt qu'une cible active. De ce fait, la charge utile reste indétectable dans la plupart des environnements d'inspection et n'exécute sa collecte complète que lorsqu'elle s'identifie comme se trouvant sur un véritable hôte conteneurisé.
exfiltrationLes données collectées quittent l'hôte par deux canaux. Premièrement, une balise POSTEZ à une valeur fixe webhook.site Le collecteur contient le nom d'hôte, l'UID numérique, le répertoire de travail et jusqu'à 120 Ko de données collectées. Ensuite, ces données sont intégrées dans un faux « manifeste de module » YAML. PUT à /api/modules/ sur un serveur cible :
ecto_module: name: "<flag-or-chunk-0>" version: "1.0.0" power_level: "<chunk-1>" ship_deck: "<chunk-2>" cargo_hold: "<chunk-3>" Les noms des champs du manifeste (niveau_de_puissance, pont du navire, soute) ne sont que de la décoration — les données volées sont dissimulées dans les valeurs de chaîne, c'est pourquoi un moniteur réseau voit ce qui ressemble à un simple chargement de manifeste de registre de paquets plutôt qu'à un déversement de données évident. Le canal de balise transporte plus : POSTEZ corps à webhook.site Il comprend le nom d'hôte, l'UID numérique, le répertoire de travail et jusqu'à 120 Ko du blob collecté, de sorte que même une seule balise réussie fournit la totalité des données. webhook.site est un service gratuit d'inspection des requêtes ; son utilisation comme collecteur signifie que l'opérateur n'a jamais besoin de mettre en place sa propre infrastructure de réception pour ce canal, et les requêtes enregistrées restent dans la corbeille du service.
Le manifeste PUT parcourt une liste de secours qui commence par plusieurs 127.0.0.1/localhost ports puis tombe à trois adresses publiques dans le `154.57.164.0/24` La plage de requêtes s'arrête au premier point de terminaison qui répond par un code d'état 2xx. L'ordre localhost en premier est cohérent avec l'autodescription « tests verdaccio » (un registre local sur l'interface de bouclage), mais les solutions de repli via une adresse IP publique signifient que les données quittent l'hôte lorsque l'interface de bouclage n'est pas à l'écoute — c'est-à-dire, sur toute machine autre que le poste de test de l'auteur.
Forum
Le groupe présente une croissance progressive des capacités plutôt qu'une chute brutale. Nous le classons selon le comportement observé, et non selon la date de publication.
| Stage | Packages / Versions | Comportement |
|---|---|---|
| Course anticipée | coral-wraith 9999.0.x | Numéros de version gonflés, ce qui suggère une tentative de confusion des dépendances ; énumération et extrusion lors de l'installation |
| graine | coral-wraith 1.0.0 | La commande postinstall collecte les fichiers id/env/flag ; une seule requête PUT vers 154[.]57[.]164[.]71:30782, marqueur ECT-472839 |
| Itération rapide | coral-wraith 1.0.1 → 6.0.0 | Des dizaines de lancements en quelques heures ; la charge utile gagne isAppWorker() porte, récupération des identifiants IMDSv2, la version complète get-secret-value pivot, vérification de l'environnement du registre/chemin et marqueurs de double destination |
| Noms parallèles | ecto-corsair-whisper-6f3b9 1.0.14–1.0.18 | Même charge utile contrôlée ; webhook.site balise et liste de repli multi-points de terminaison |
| Variantes | ecto-rust-read-f3a9c1 1.0.1–1.0.2 | Ajoute des marqueurs d'évier supplémentaires ECT-987654, ECT-654321, ECT-839201 |
| Variantes | ecto-corsair-flag-x9m4 1.0.0, ecto-nightly-spirit 1.1.0 | Même charge utile contrôlée, même C2 et même balise |
La caractéristique principale de ce cluster est son rythme de publication : au lieu d’un seul paquet et d’une seule version, le même nom est republié sans cesse à intervalles rapprochés, chaque version étant une légère variation de la précédente, accompagnée de quelques paquets frères portant des noms différents et contenant la même charge utile. chuchotement Dans cette famille de code, le code se divise en deux empreintes très proches : l’une déclenche deux détections critiques, l’autre trois (une entrée supplémentaire pour la lecture de fichiers). Cependant, les deux aboutissent à la même charge utile ; la différence réside dans une dérive du code, et non dans un changement de comportement. Versions de chuchotement Des valeurs dépassant la plage analysée (jusqu'à au moins 1.0.25 au moment de la rédaction) ont été observées en direct sur le registre, et les spectre corallien Le nom a continué à gravir sa propre échelle de versions par-dessus la même fenêtre.
Indicateurs de compromis
Tous les indicateurs ci-dessous ont été extraits du code source du paquet sur disque. Les indicateurs réseau ont été désactivés.
Réseau
| Indicateur | Rôle |
|---|---|
hxxp://154[.]57[.]164[.]71:30782 | Cible C2 PUT (coral-wraith) |
hxxp://154[.]57[.]164[.]80:30543 | C2 PUT repli (ecto-*) |
hxxp://154[.]57[.]164[.]82:31250 | C2 PUT repli (ecto-*) |
hxxp://154[.]57[.]164[.]71:31289 | C2 PUT repli (ecto-*) |
hxxps://webhook[.]site/602a4c72-7033-4e28-92ea-dc66e59206e5 | Collecteur de balises |
169[.]254[.]169[.]254/latest/... | Lecture des informations d'identification IMDSv2 (côté cible, métadonnées AWS) |
Comportemental/fichier
| Indicateur | Rôle |
|---|---|
"postinstall": "node postinstall.js" | Installer le vecteur |
ecto_module: YAML avec power_level / ship_deck / cargo_hold clés | Schéma manifeste d'Exfil |
Marqueurs d'évier ECT-472839, ECT-987654, ECT-654321, ECT-839201 | Segment de trajectoire C2 /api/modules/<marker> |
isAppWorker() porte : hôte /^[0-9a-f]{12}$/, cwd contient /app/node_modules | Condition d'activation |
aws secretsmanager list-secrets plus de us-east-1, eu-west-1, eu-central-1 | Énumération des secrets |
HTB{...} extraction par expression régulière | Récolte de la capture du drapeau |
Hachages de fichiers (sha256, capturés au moment de l'analyse)
| Fichier | sha256 |
|---|---|
coral-wraith/postinstall.js | ce5ff035cfdfed1d0015446424b352c27b66bcb77e9fdb0a51e4245199146824 |
ecto-corsair-whisper-6f3b9 1.0.18/postinstall.js | b58432acba376aa6976f0490d9a1c04257ccdbc856d8390260c50322d63e31c3 |
Attribution et comportement observé
Les cinq noms de paquets ont été publiés sous deux comptes npm, mais ils partagent suffisamment d'infrastructure pour être considérés comme un seul cluster : le même ecto_module schéma manifeste, le même ECT-472839 marqueur de puits principal, le même webhook.site L'identifiant du collecteur et les points de terminaison C2 sont identiques. Bloc 154.57.164.0/24. Le paquet de départ (« coral-wraith », plus simple et sans barrière) et la famille ecto-classique avec barrière sont donc considérées comme des itérations sur une même boîte à outils plutôt que comme des efforts indépendants.
Dans leurs versions ultérieures, les paquets se décrivent eux-mêmes comme « Charge utile CTF pour les tests de la chaîne d'approvisionnement du verdaccio. » Nous signalons cette étiquette comme un fait observable et ne la réaffirmons pas comme une conclusion quant à la finalité. Le fonctionnement du code est sans ambiguïté et indépendant de son étiquette : il lit les informations d'identification des rôles IAM depuis le service de métadonnées de l'instance, énumère les secrets gérés sur trois régions AWS et transmet les résultats à une adresse IP publique et à un collecteur webhook tiers. Un environnement de test véritablement basé sur la boucle locale n'aurait besoin ni de la liste de secours des adresses IP publiques, ni des lectures d'informations d'identification IMDS, ni des appels interrégionaux à Secrets Manager. Étant donné que le trafic sortant et la portée des informations d'identification sont réels, les versions restreintes ont été classées comme malveillantes.
Le fait qu'il ne soit activé que dans un conteneur est la caractéristique opérationnelle la plus notable. Il s'agit à la fois d'une mesure d'évitement (le système reste silencieux sur les ordinateurs portables et dans les environnements de test d'analyse) et d'une mesure de ciblage (il ne s'active que là où un rôle IAM réel et des secrets actifs sont susceptibles d'être présents). Les analystes exécutant ces packages dans un environnement de test générique ne remarqueraient rien ; ce comportement ne se manifeste que sous un nom d'hôte de type Docker et avec un chemin d'installation dans un conteneur.
Impact, tendances et orientations pour les défenseurs
Le problème ici est la divulgation d'identifiants cloud et de secrets au sein des conteneurs de construction et d'exécution. Un identifiant de rôle IAM extrait d'IMDS contient toutes les autorisations associées à ce rôle ; gestionnaire de secrets : Lister les secrets (et toute suite) ObtenirValeurSecretCela étend la portée aux secrets d'application stockés. Les dumps de variables d'environnement contiennent fréquemment des jetons de registre, des URL de bases de données et des clés API. Dans un contexte d'intégration continue ou de conteneur (exactement ce que le contrôle sélectionne), une simple installation transitive de l'un de ces paquets suffit à divulguer ces informations.
Ectoplasm correspond à un schéma récurrent : des charges utiles installées qui accèdent aux métadonnées du cloud et aux secrets gérés plutôt qu’aux fichiers locaux, et qui ne s’activent que dans des environnements critiques. Deux observations défensives en découlent.
- La forme est détectable. Un hook d'installation npm/PyPI dont le graphe d'appels atteint à la fois une API de secrets cloud (gestionnaire de secrets AWS, secrets de gcloud, coffre-fort de clés AZ) ou l'adresse IMDS et un point de sortie réseau constituent un modèle étroit à signal élevé — cela n'arrive presque jamais dans un script de cycle de vie légitime. Analyse des flux statiques peut le signaler sans dépendre d'un domaine ou d'une adresse IP spécifique.
- Le durcissement de l'environnement l'émousse. L'application d'IMDSv2 avec une limite de sauts de 1 empêche les charges de travail des conteneurs d'accéder aux métadonnées des instances ; la limitation de la portée des rôles IAM au moindre privilège restreint l'impact de toute fuite d'informations d'identification ; et l'exécution des installations avec –Ignorer-scripts Dans CI, le vecteur d'installation est entièrement supprimé pour les paquets qui n'en ont pas besoin.
Pour les équipes de défense, les contrôles pratiques consistent à : alerter sur les connexions sortantes des conteneurs de compilation/CI vers des adresses IP publiques non autorisées pendant npm installer; surveiller les accès IMDS provenant des scripts de cycle de vie des packages ; et considérer comme suspect tout hook d'installation qui s'adresse à une interface de ligne de commande cloud jusqu'à preuve du contraire.
Deux remarques supplémentaires concernant ce cluster. Premièrement, l'activation étant limitée aux environnements conteneurisés, un package qui semble inactif lors de sa vérification sur un poste de travail peut être actif en production. La vérification doit donc reproduire le nom d'hôte et le chemin d'accès du conteneur, ou consulter directement le code source, plutôt que de se fier à un simple constat : « Je l'ai installé et rien ne s'est passé. » Deuxièmement, l'utilisation d'un service public d'inspection des requêtes comme collecteur de balises signifie qu'une partie des données exfiltrées peut être récupérée en cas d'incident. Une organisation qui découvre l'un de ces packages dans son arbre de dépendances peut déduire le contenu d'une balise réussie à partir de la logique de collecte de la charge utile et doit remplacer les informations d'identification des rôles IAM, les jetons de registre et les secrets gérés accessibles depuis l'environnement de compilation ou d'exécution affecté. rotation des titres de compétencesLa solution efficace une fois l'installation terminée est l'installation elle-même, et non la suppression du paquet.





