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

Le provider s'exécute là où sont les clés

Aikido a trouvé un RAT nord-coréen livré sous la forme de deux providers Terraform sur le registre de HashiCorp. terraform init télécharge un provider, terraform plan l'exécute comme processus local, et le provider lit vos credentials cloud, parce que c'est précisément à cela que sert un provider. La charge ne se déchiffre que pour les noms de variables d'une seule victime, si bien que les scanners qui ont récupéré le paquet n'ont vu que du code inerte. Dix des dix-huit machines atteintes étaient des Mac. Bromure Agentic Coding donne à ce binaire une VM, une fausse clé secrète et une liste d'endroits qu'il a le droit d'appeler.

Tous les contrôles que l'industrie a construits pour les agents de code se placent en amont de la commande : approuver le plan, lire le diff, verrouiller le push. Aucun ne se place entre un binaire de provider téléchargé et ~/.aws.

Vous ajoutez un conteneur à une stack. Vous demandez un provider Docker à l'agent, il écrit quatre lignes de HCL, et vous lancez terraform init. Terraform résout le provider depuis le registre, le télécharge, vérifie sa signature et le met en cache. Puis vous lancez terraform plan, et Terraform démarre ce binaire comme un processus sur votre machine et lui parle en RPC. Il s'authentifie auprès de votre compte cloud au passage, avec les mêmes variables d'environnement et le même profil ~/.aws que vous utilisez vous-même.

Chacune de ces étapes est le fonctionnement documenté, et ce mois-ci c'était aussi une voie de livraison de malware.

Ce qu'Aikido a trouvé

Le 22 septembre, Aikido a signalé un cheval de Troie d'accès à distance écrit en Go, distribué via deux providers Terraform et deux modules Go. The Hacker News l'a couvert le lendemain, en le qualifiant de première utilisation par des attaquants du registre opéré par HashiCorp pour livrer une charge. Checkmarx, JFrog et SafeDep ont ajouté leurs analyses.

Les providers sont gocommunity-io/dockerd, avec 222 téléchargements, et kreuzwenker/docker, avec 1 449. Le second est à une lettre de kreuzwerker/docker, un vrai provider avec 56 millions de téléchargements déclarés. Les modules Go sont gocommunity.io/orderedbtree, publié le 11 août, et gogets.dev/btreex, publié le 8 septembre.

Aikido rattache la campagne à Graphalgo, que ReversingLabs a documenté en février et attribué à des opérateurs nord-coréens. La même clé de signature apparaît dans des charges npm depuis avril. On arrive à ces paquets par une offre d'emploi : une fausse société Web3 aborde un développeur sur LinkedIn ou Facebook et lui envoie un exercice de code. Le dépôt de l'exercice est propre. La dépendance qu'il tire est le malware.

Un provider est un programme, et Terraform lui donne les clés

npm install exécute les scripts d'installation d'un paquet puis s'efface. terraform init récupère un programme entier, et exécuter ce programme est l'événement principal.

Un provider Terraform est un binaire exécutable que Terraform Core invoque en RPC. Terraform l'exécute parce que l'exécuter, c'est ce qu'est un provider. Vous ne trouverez pas de hook d'installation à retirer ni de script de cycle de vie à auditer, parce que l'exécution est ici la fonctionnalité, et non un effet de bord du dépaquetage d'une archive.

Le binaire s'exécute aussi à l'endroit de votre réseau où le chemin vers la production est le plus court. Purple Shield décrit la machine : « Un provider malveillant s'exécute donc sur la machine de la personne qui déploie votre infrastructure, avec les accès dont dispose cette personne. » Et : « un portable qui lance terraform apply a généralement à portée les clés de fournisseur cloud, les jetons Git, les clés SSH et les fichiers d'état Terraform. »

Un voleur npm doit fouiller votre répertoire personnel à la recherche de credentials. Terraform les remet au provider.

Le chemin documenté, et la branche qu'on y a ajoutéele registrekreuzwenker/dockerinitla machine qui déploieTerraform Coreplan → lance, puis RPCle processus providerun binaire téléchargéles credentials~/.aws, jetons, clésla branche ajoutée, à l'intérieur du providersha256(containerName + networkID)match → clé AES → déchiffre → go run . détachétoute autre configuration, et tout scannerle hash ne correspond pas ; rien n'est déchiffréla seule configuration viséeun RAT, qui annonce hostname et matériel, puis attend18 machines, 725 messages : 3 Windows, 5 Linux, 10 macOS
La chaîne ne contient aucun exploit. terraform init résout un provider depuis le registre et met le binaire en cache ; terraform plan le lance comme processus local et lui parle en RPC ; le provider lit la chaîne de credentials ambiante, et c'est le protocole. Les providers de Graphalgo ajoutent une branche : un point d'entrée caché qui hache deux de vos propres variables Terraform, et ce n'est que si le hash correspond à une valeur codée en dur qu'il s'en sert comme clé AES pour déchiffrer une charge cachée dans un fixture d'exemple et l'exécuter avec un 'go run' détaché. Pour toute autre configuration, le code est inerte.

Adressé à une seule personne

Ces providers attaquent une cible et ignorent tous les autres qui les installent.

Tous deux portent un point d'entrée caché dans /internal/provider/resource_docker_container_funcs.go qui ne se déclenche que lorsque le SHA256 des variables Terraform containerName et networkID, concaténées, égale une valeur codée en dur. Le code utilise alors ce hash comme clé AES pour déchiffrer un chemin de fichier à l'intérieur du paquet, dézippe ce qu'il y trouve, déchiffre chaque fichier, et exécute le résultat comme paquet Go avec un go run . détaché. Ils ont caché l'archive dans le répertoire des exemples, dans un fichier appelé import-resource.sqlite3.

L'attaquant doit donc connaître les noms que vous avez donnés à deux variables, et cet attaquant les connaissait : le leurre de l'offre d'emploi, c'est la façon dont il les a appris. Tous les chercheurs et tous les scanners qui ont récupéré le paquet ont obtenu du code qui reste là et ne fait rien. On ne peut pas détecter ce qui ne se déchiffre jamais.

Le second étage est un cheval de Troie d'accès à distance. Il remonte d'abord des informations système, dont les caractéristiques matérielles, l'OS, le hostname, et la présence ou non de node sur la machine, puis attend des instructions à deux endroits. Le premier est Slack : un espace de travail appelé portfolio-devs, un canal appelé frontend-devs, et plus tard un second espace avec un canal nommé qa-announcements. Aikido y a compté 1 240 messages chiffrés remontant au 16 juillet. L'autre est un smart contract sur le testnet Arbitrum Sepolia, à une adresse codée en dur, contenant 1 402 transactions remontant au 6 août. Le malware interroge la blockchain toutes les trois secondes et Slack toutes les dix.

Les deux canaux sont du HTTPS ordinaire vers des destinations à bonne réputation. Vos machines de développement parlent à Slack toute la journée, et l'une d'elles qui parle à un endpoint RPC Ethereum public passe pour normale n'importe quelle semaine où l'équipe livre quelque chose on-chain. La même équipe a utilisé un contrat Sepolia dans le paquet npm que Checkmarx a décrit la semaine dernière, et elle le réutilise au grand jour parce que cela marche au grand jour.

Aikido a récupéré la liste des victimes dans le Slack de l'attaquant lui-même : 18 hostnames uniques sur 725 messages de check-in. Trois Windows, cinq Linux, dix macOS.

Ce qui change dans un workspace Bromure

La campagne en voulait à un credential. Dans Bromure Agentic Coding, le ~/.aws/config de la VM ne contient pas votre clé d'accès secrète. Il pointe vers un helper credential-process qui délivre, par une socket virtio, votre véritable identifiant de clé d'accès associé à un secret factice de 40 caractères. Le manuel liste les outils qui reprennent cela sans la moindre configuration : tous les SDK AWS, la CLI aws, boto3, et terraform. Le provider lit la chaîne de credentials comme il s'y attend, obtient un profil bien formé, et signe ses requêtes avec un secret qui ne vaut rien.

L'hôte répare cette signature en sortie. Son resigner reconnaît le trafic destiné à *.amazonaws.com, retire la signature SigV4 condamnée du guest, et re-signe avec le vrai secret avant que la requête ne quitte votre Mac. Votre terraform plan fonctionne. AWS répond à une copie volée du credential par InvalidSignatureException.

Puis le faux secret essaie de sortir. Le proxy analyse les en-têtes et le corps de chaque requête sortante à la recherche des valeurs factices propres au workspace, avec un automate Aho-Corasick assez peu coûteux pour tourner sur toutes. Envoyez un faux vers un hôte pour lequel il n'a pas été frappé, un webhook Slack par exemple, et le proxy répond HTTP 451 et ne transmet pas un octet. La VM se met en pause. Une ligne rouge Credential brokering apparaît dans la Security Timeline, et Bromure marque le workspace comme compromis, si bien que votre prochain lancement propose d'effacer son disque et son home tout en conservant vos jetons, clés SSH et réglages.

Le C2 doit encore sortir de la machine. L'egress est un jeu de règles ordonné, d'autorisation et de refus, évalué de haut en bas, se terminant par default allow ou default deny. Un workspace qui déploie de l'infrastructure a besoin de registry.terraform.io, releases.hashicorp.com et de votre API cloud, ce qui laisse portfolio-devs.slack.com et un endpoint RPC Arbitrum hors de la liste. L'hôte applique les règles deux fois : le switch virtuel filtre sur l'IP de destination et le hostname capté par DNS, en TCP comme en UDP, et le proxy filtre de nouveau sur le nom de serveur TLS. Le switch détourne le port 80 et le port 443 vers le proxy sans aucune variable d'environnement et sans rien que le guest puisse désactiver. Chaque verdict devient une ligne Firewall dans la Security Timeline.

Sur la machine qui déploiele processus provider, sous votre identitélit ~/.aws → vraie clé secrètelit jetons git, clés SSH, tfstatevotre home est le rayon d'explosionsortant, sans questionportfolio-devs.slack.com · toutes les 10 sArbitrum Sepolia RPC · toutes les 3 sles deux destinations ont bonne réputationil n'y a aucune anomalie à signalerreprise : renouveler tout ce qu'il pouvait atteindreDans un workspace Bromure Agentic Codingle processus provider, dans le guestlit ~/.aws → access key ID + faux secretUbuntu 24.04, sous l'hyperviseurun disque qu'on remplace par un menusortant, confronté à vos règlesslack.com → aucune règle → deny par défautSepolia RPC → aucune règle → deny par défautet si le faux secret sort de son périmètreHTTP 451, aucun octet transmis, VM en pausereprise : effacer, relancer, ne rien renouveler
Le même binaire de provider, deux machines. Sur un portable il s'exécute sous votre identité, lit une vraie clé secrète dans la chaîne de credentials, et atteint Slack et un endpoint RPC public parce que personne n'a écrit de règle contre lui. Dans un workspace Bromure il s'exécute dans un guest Ubuntu sous l'hyperviseur, trouve un identifiant de clé d'accès associé à un faux secret, et rencontre un jeu de règles d'egress ordonné en sortie ; emporter le faux quelque part où il n'a pas été frappé renvoie HTTP 451 et met la VM en pause. Les appels AWS qui ont légitimement leur place en amont sont re-signés sur l'hôte, donc terraform fonctionne toujours, et les actions AWS destructrices s'arrêtent sur un dialogue qui s'affiche sur le Mac.

C'est vous qui approuvez les suppressions

Un contrôle de plus se place entre le provider et votre compte, et c'est celui qui compte quand l'attaquant veut votre infrastructure plutôt que vos secrets.

Les credentials AWS portent une politique d'écriture, et les nouveaux workspaces sont par défaut en Prompt before write. Le proxy classe chaque appel d'après le nom d'action dans l'en-tête X-Amz-Target ou le paramètre Action= : Get*, List* et Describe* sont des lectures et passent sans un mot, tandis que Delete*, Terminate*, Revoke* et leurs apparentés comptent comme destructives. Une écriture s'arrête sur un dialogue intitulé Allow write on "<scope>" from workspace "<name>"?, qui montre l'opération telle quelle plutôt qu'un résumé, avec Allow for 15 minutes, Allow once, Allow for the rest of the session, et Don't allow. Refusez, et le guest reçoit une AccessDeniedException que l'agent signale avant de passer à autre chose.

C'est vous qui cliquez dans ce dialogue, sur votre Mac. Un processus à l'intérieur de la VM ne peut pas l'atteindre, et celui qui est à l'autre bout du canal Slack non plus.

Traitez la chaîne d'outils comme un exécuteur

terraform init et terraform plan sont du télécharger-et-exécuter. Demandez ce qu'un provider exécute à l'intérieur, parce qu'il exécutera.

Ne laissez aucun secret vivant à portée

Donnez au provider une chaîne de credentials qui le satisfait et un secret qui ne vaut rien. L'hôte signe, de l'autre côté de la frontière, cadré sur la destination à laquelle la clé appartient.

Nommez d'abord les destinations

Un workspace de déploiement a besoin d'une liste courte et stable : le registre, l'hôte de releases, votre API cloud. Écrivez-la une fois, mettez le trafic non apparié sur Deny, et une boîte aux lettres morte n'a plus rien à interroger.

Faites demander l'irréversible

Laissez les lectures passer en silence. Faites que Terminate* vous coûte un dialogue sur un écran que la VM ne peut pas atteindre.

Personne n'a trouvé de vulnérabilité ici

Le registre de HashiCorp a servi le paquet qu'on lui a demandé. Terraform a vérifié la somme de contrôle qu'on lui a donnée et a exécuté le provider comme il exécute tous les providers. Aucun sandbox n'a cédé, aucun agent n'a lu d'instruction empoisonnée, et aucun modèle ne s'est mal comporté.

Deux providers totalisant 1 671 téléchargements ont atteint au moins 18 machines, des Mac pour la plupart, et sont restés là à interroger un smart contract toutes les trois secondes. Un comportement documenté a porté toute la chaîne. La seule partie inédite, une charge qui reste chiffrée à moins que vos variables ne portent un nom bien précis, existe précisément pour que rien ne puisse la trouver.

Décidez donc de ces choses avant la session, parce que vous ne les attraperez pas pendant. On ne peut pas scanner un provider qui n'a pas déchiffré sa charge, et vous ne distinguerez pas l'exfiltration dans du trafic vers Slack. Faites tourner le déploiement là où les clés sont des leurres, où les destinations sont une liste que vous avez écrite, et où la machine qui se fait rooter est une machine que vous jetez.