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.
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.
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.