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

Le jeton volé n'avait besoin d'aucun serveur

Wiz Research a remonté la piste de Gheist, une campagne automatisée qui prend des jetons GitHub dans des journaux d'infostealers et d'anciennes fuites de la chaîne d'approvisionnement, puis plante un workflow qui vide les secrets dans chaque dépôt que chaque jeton peut atteindre : plus de 13 500 branches issues de plus de 850 comptes. Les secrets sortent par l'API de GitHub elle-même, il n'y a donc aucun serveur d'attaquant à bloquer. Dans Bromure Agentic Coding, le jeton GitHub qu'un stealer trouve sur une machine de développeur est un substitut que GitHub rejette partout ailleurs, et chaque écriture depuis la machine attend une boîte de dialogue sur votre Mac.

Gheist n'a pas de serveur de commande. GitHub exécute la charge utile, GitHub écrit les secrets dans un journal, et GitHub sert ce journal à quiconque détient le jeton.

Le 25 octobre, Wiz Research a publié son analyse de Gheist, une campagne qu'elle suit depuis le 15 septembre. Un outil automatisé prend un jeton GitHub volé, liste chaque dépôt que ce jeton peut atteindre, et y plante un workflow qui affiche les secrets du dépôt. Wiz a retrouvé plus de 13 500 branches plantées, issues de plus de 850 comptes compromis, et au 24 octobre la campagne tournait encore et changeait toujours. The Hacker News l'a couverte hier.

L'outil n'a besoin que d'une seule entrée, et Wiz dit d'où elle vient : les jetons correspondent à des journaux d'infostealers et à des jetons exposés lors de compromissions antérieures de la chaîne d'approvisionnement, comme celle du scanner Trivy en mars. Les conseils de Wiz à chaque victime comprennent cette ligne : vérifiez si les appareils des propriétaires des jetons hébergent des infostealers.

Comment fonctionne Gheist

Pour chaque dépôt, l'outil crée une branche nommée ci-sync- suivie de six caractères aléatoires et y commite un seul fichier, .github/workflows/sync-check.yml, avec le message ci: sync config. Le commit n'est pas signé et il est attribué au propriétaire du jeton. Ses requêtes API portent le user agent gheist, qui a donné son nom à la campagne.

Le push démarre le workflow. Le job affiche ses variables d'environnement et tout le contexte secrets dans le journal d'exécution, les deux encodés en base32. Wiz explique pourquoi cela marche : « GitHub masque les valeurs secrètes dans les journaux uniquement lorsqu'elles apparaissent telles quelles. » La sortie base32 ne correspond pas aux valeurs stockées, donc rien n'est masqué. Une matrice sur les environnements de déploiement du dépôt récupère les secrets propres à chaque environnement, qui sont souvent ceux de la production.

Ensuite, l'outil télécharge le journal par l'API GitHub et supprime l'exécution quelques secondes plus tard. Selon les mots de Wiz : « Comme les secrets sortent par l'API même de GitHub, il n'y a aucun domaine d'attaquant ni aucune connexion sortante à bloquer. »

L'outil n'a cessé de s'améliorer. Une variante a ajouté un délai d'expiration de six minutes, la matrice d'environnements et des modifications de charge utile propres à chaque dépôt, dans les heures qui ont suivi sa première exécution, le 28 septembre. Le 19 octobre, il a gagné un job visant les runners auto-hébergés, qui cherche 31 types de jetons, les identifiants des instances cloud et le jeton du compte de service Kubernetes.

D'un jeton volé aux secrets d'un dépôt1 · Un jeton voléd'un journal d'infostealerou d'une fuite de dépendanceplus de 850 comptes2 · Une branche par dépôtci-sync-xxxxxxajoute sync-check.ymlplus de 13 500 branches3 · Secrets dans le journaltoJSON(secrets) | base32le masquage rate lesvaleurs encodées4 · Télécharger, supprimerjournal lu via l'API GitHub,exécution vite supprimée ;aucun hôte d'attaquantDépôts publics · retenus par GitHubdepuis le 28 juillet, les exécutions suspectes attendentun collaborateur ; toutes celles de Gheist vues par Wizrestaient à action_requiredDépôts privés et GHES · exécutésles journaux d'audit montrent des exécutions ci-sync-*terminées puis supprimées ; CloudTrail montre des rôlesAWS assumés depuis ces branches
La chaîne de Gheist, telle que Wiz la décrit. Un jeton issu d'un journal d'infostealer ou d'une ancienne fuite de la chaîne d'approvisionnement permet à un outil automatisé de pousser une branche ci-sync- avec un seul fichier de workflow dans chaque dépôt que le jeton atteint. Le workflow affiche chaque secret en base32, en échappant au masquage des journaux de GitHub, puis l'outil télécharge le journal par l'API de GitHub et supprime l'exécution. Aucun serveur d'attaquant n'intervient à aucune étape.

Là où s'arrête la nouvelle règle de GitHub

Depuis le 28 juillet, GitHub retient les exécutions de workflow qu'il juge malveillantes dans les dépôts publics jusqu'à ce qu'un collaborateur les approuve. Chaque exécution publique de Gheist examinée par Wiz était bloquée à action_required. « Le contrôle a fonctionné, et les charges malveillantes ne se sont jamais exécutées. »

Cette retenue ne couvre que les dépôts publics sur github.com. Dans les dépôts privés et sur GitHub Enterprise Server, Wiz a trouvé des journaux d'audit montrant des exécutions ci-sync-* arrivées à leur terme puis supprimées, et des enregistrements CloudTrail de workflows d'organisation assumant des rôles AWS depuis ces branches.

Le jeton vient d'une machine de développeur

Chaque branche Gheist a besoin d'un jeton valide, et Wiz a fait remonter ces jetons à des journaux de stealers et à des outils de build divulgués. Sur l'ordinateur portable d'un développeur, la connexion de la CLI gh, un jeton d'accès personnel dans ~/.git-credentials, un GH_TOKEN dans un profil de shell : un infostealer copie tout cela en une seule passe. Les agents de code ont rendu ces jetons plus courants, car un agent qui pousse des branches et ouvre des pull requests doit en avoir un à portée de main en permanence.

Le vol peut venir d'un outil de confiance. Le 19 octobre, un attaquant qui contrôlait depuis des mois le compte GitHub d'un développeur d'OVHcloud a remplacé les binaires de cinq versions de la CLI OVHcloud, de v0.11.0 à v0.15.0, par des builds qui, selon les mots d'Octave Klaba, PDG d'OVHcloud, « récupèrent les secrets sur le disque ». BleepingComputer a rapporté que les builds macOS, Linux et Windows étaient tous touchés. Le conseil de Klaba : « Si vous êtes concerné, changez tous les secrets présents sur votre disque. »

Un jeton volé n'a pas besoin d'atteindre le jour même une campagne comme Gheist. Les journaux de stealers s'échangent, et un jeton reste utile jusqu'à ce que quelqu'un le révoque.

Le même jeton sur une machine Bromure

Bromure Agentic Coding exécute Claude Code, Codex, Grok, Kimi et omp dans des VM Ubuntu sur votre Mac. Les vrais identifiants restent sur l'hôte, et l'agent travaille avec des substituts. Suivez un jeton GitHub le long de la chaîne de Gheist sur l'une de ces machines.

Le jeton sur le disque est un substitut

Ajoutez votre jeton GitHub à la machine sous Credentials. Au démarrage, Bromure écrit un faux dans la VM : ghp_ suivi de 36 caractères, la forme qu'attend gh, dans GH_TOKEN, dans la configuration de gh et dans ~/.git-credentials. Le git push et le gh pr create de l'agent fonctionnent comme avant. Quand une requête quitte la VM vers github.com, le proxy du Mac remplace le faux par le vrai jeton, et uniquement pour cet hôte.

Lancez maintenant la CLI piégée dans cette VM, comme le ferait un agent après avoir suivi les instructions d'installation d'un projet. Elle lit le disque, et chaque valeur GitHub qu'il contient est le substitut. Il en va de même pour le reste du butin habituel d'un développeur. ~/.aws/config pointe vers un assistant qui fournit une fausse clé secrète, et le proxy re-signe les requêtes AWS avec la vraie sur le Mac. La VM n'a aucune clé privée SSH à lire : SSH_AUTH_SOCK mène à un agent sur l'hôte, qui signe pour la VM et ne livre jamais la clé.

Gheist ne peut rien faire du substitut

L'outil Gheist tourne sur l'infrastructure de l'attaquant. Il appelle api.github.com avec le jeton qu'il a acheté et s'attend à voir des dépôts. Avec un substitut Bromure, GitHub répond 401 Bad credentials. Bromure dérive le substitut du vrai jeton avec un sel qui reste sur votre Mac, et seul le proxy de ce Mac sait quel vrai jeton il remplace. L'outil ne peut pas lister un seul dépôt, il ne crée donc jamais de branche.

Toute la méthode de Gheist consiste à demander ce qu'un jeton peut atteindre. Hors de la VM où il a été écrit, un substitut Bromure n'atteint rien.

Un substitut en route vers un stealer déclenche une alarme

Le stealer doit encore envoyer ce qu'il a trouvé quelque part, et depuis une VM Bromure chaque connexion HTTPS passe par le proxy du Mac. Le proxy examine chaque requête sortante, en-têtes et corps, à la recherche de substituts envoyés vers un hôte pour lequel ils n'ont pas été créés. Un substitut ghp_ destiné à autre chose que GitHub est bloqué avec un HTTP 451 avant qu'un seul octet ne quitte le Mac. La VM se met en pause, et une alerte affiche This environment may have been compromised, avec le nom de l'identifiant, l'hôte auquel il appartient et l'hôte vers lequel il a été envoyé.

Vous pouvez éteindre la machine, ou conserver son disque et son dossier personnel pour enquête. Une machine marquée comme compromise ne redémarre plus tant que son disque et son dossier personnel n'ont pas été effacés, donc le stealer ne survit pas au redémarrage.

Réglez Unmatched traffic sur Deny dans le panneau Guardrails de la machine, autorisez les hôtes dont le projet a besoin, et l'hôte d'envoi du stealer est refusé d'emblée, archive chiffrée ou non. La connexion refusée devient une ligne dans la Security Timeline.

Les écritures depuis la machine vous attendent

Supposons que le malware renonce au vol et utilise le substitut là où il fonctionne, depuis la VM, à travers le proxy. Il lui faudrait faire ce que fait Gheist : créer une branche et commiter un workflow.

Les nouvelles machines Bromure règlent la politique d'écriture GitHub sur Prompt before write. Les lectures passent. Un git push ou une écriture REST vers github.com s'arrête sur une boîte de dialogue sur votre Mac, qui affiche l'opération telle quelle :

Allow write on "GitHub" from workspace "web-app"?
POST /repos/<owner>/<repo>/git/refs

Une branche ci-sync- que vous n'avez pas demandée, apparue pendant que vous regardiez une pull request, se refuse facilement. Don't allow renvoie à l'agent la même erreur qu'une politique de blocage, et Allow once n'accorde rien au-delà de cette seule écriture.

Portable : un jeton valide sur le disqueLe stealer lit le disqueconnexion gh, ~/.git-credentials, GH_TOKEN : le vrai jetonJournal de stealer, puis l'outil gheistfonctionne depuis n'importe quelle machine sur Internetapi.github.com l'acceptebranches ci-sync- dans chaque dépôt atteint ;les secrets sortent par l'API même de GitHubBromure : un substitut dans la VMVM de l'agent (Ubuntu)config gh, ~/.git-credentials, GH_TOKEN = substitut ghp_Proxy de l'hôte sur le Macvrai jeton inséré pour github.com uniquement ;substitut ailleurs : 451, VM en pause ; écritures validéesDepuis l'infrastructure de l'attaquantle substitut n'est pas un jeton GitHub ;api.github.com le rejette, rien n'est planté
Gheist sur un portable et sur une machine Bromure. Sur un portable, un stealer copie sur le disque un jeton GitHub valide, et l'outil gheist l'utilise depuis n'importe où. Sur une machine Bromure, la VM détient un substitut ghp_ : GitHub le rejette depuis l'infrastructure de l'attaquant, le proxy le bloque en route vers l'hôte du stealer et met la VM en pause, et toute écriture depuis la VM attend une boîte de dialogue sur le Mac.

La rotation se fait en un seul endroit

La première étape de réponse selon Wiz consiste à renouveler chaque secret qu'un dépôt touché pouvait atteindre et à révoquer les jetons qui ont poussé les commits. « Supprimer l'exécution ou la branche n'annule pas l'exposition. »

Sur un Mac Bromure, le volet développeur de cette rotation tient en un seul champ. Le vrai jeton GitHub vit dans une seule entrée chiffrée sur l'hôte. Collez le nouveau dans le panneau Credentials de la machine, et le lancement suivant écrit le substitut correspondant ; aucune copie à traquer dans des profils de shell, des fichiers .env ou des configurations d'agent. Pour une clé enregistrée comme référence 1Password (op://vault/item/field sous Other API keys), Bromure la résout de nouveau toutes les deux minutes, si bien qu'une rotation dans 1Password atteint les machines en cours d'exécution sans redémarrage.

Le coffre lui-même est protégé par une clé du trousseau Data Protection de macOS, liée à l'identité de signature de Bromure et jamais synchronisée. Un stealer qui copie le dossier de Bromure hors du Mac n'obtient que du texte chiffré.

Confiez la traque à un agent, en lecture seule

Les étapes de traque de Wiz représentent un travail fastidieux sur de nombreux dépôts, ce qui en fait un bon travail pour un agent : lister chaque branche correspondant à ^ci-sync-[0-9a-z]{6}$, trouver les commits qui n'ajoutent que sync-check.yml avec le message ci: sync config, extraire du journal d'audit les exécutions supprimées moins d'une minute après leur fin ainsi que le user agent gheist.

Confiez la tâche à une machine Bromure dont la politique d'écriture GitHub est réglée sur Read-only. L'agent peut lire chaque dépôt et chaque journal d'audit que le jeton autorise, et tout push ou écriture REST est refusé sur le Mac avant d'atteindre GitHub. Si l'agent extrait une branche ci-sync- pour l'examiner, y compris l'une des branches que Wiz a vues repoussées avec des chargeurs PolinRider, tout ce qui s'exécute le fait dans une VM remplie de substituts. Chaque échange d'identifiant apparaît comme une ligne Credential brokering dans la Security Timeline, ce qui permet de voir ensuite quels hôtes la traque a touchés.

Les stealers trouvent des substituts

GH_TOKEN, la configuration de gh et ~/.git-credentials contiennent un faux ghp_. Le vrai jeton reste chiffré sur le Mac.

Gheist reçoit un 401

Hors de la machine, le substitut n'est pas un jeton GitHub. L'outil ne peut lister aucun dépôt, il ne plante donc rien.

Les fuites mettent la VM en pause

Un substitut envoyé ailleurs que vers GitHub est bloqué avec un 451, et la VM reste en pause jusqu'à ce que vous décidiez de son sort.

Les écritures demandent d'abord

Les nouvelles machines demandent avant chaque push et chaque écriture REST vers GitHub, avec la requête exacte à l'écran. Une traque peut tourner en lecture seule.

Retirez le jeton du portable

La retenue par approbation de GitHub a fonctionné sur les dépôts publics, et la liste de durcissement de Wiz est la bonne : relecteurs obligatoires sur les environnements de production, confiance OIDC limitée aux branches protégées, GitHub Apps ou jetons à granularité fine de courte durée plutôt que des jetons d'accès personnels. Tout cela réduit ce qu'un jeton volé peut faire une fois que quelqu'un le détient.

Bromure Agentic Coding agit une étape plus tôt. Vos agents poussent des branches et ouvrent des pull requests toute la journée, et le jeton qu'ils utilisent ne se trouve jamais sur un disque qu'un stealer peut lire. Gheist ne peut planter des workflows qu'avec des jetons encore valides, et celui d'une machine Bromure ne fonctionne qu'à travers le proxy de cette machine.

Pour voir comment ces mêmes substituts résistent à un stealer conçu pour les fichiers des agents de code, lisez Le stealer a emporté les substituts. Pour un seul jeton GitHub volé qui a abouti à une release empoisonnée, lisez Le jeton de l'agent était celui du propriétaire.