WFX Phantom : campagne de vol de secrets via GitHub Actions
Une campagne de jetons GitHub volés a poussé wfx dans des milliers de dépôts, récoltant secrets CI et identifiants cloud depuis le 10 novembre.
En bref
- Une campagne utilisant des jetons GitHub volés pousse un voleur en shell appelé
wfxdans des dépôts depuis le 2026-11-10. Step Security a publié l'analyse le 2026-11-12. - L'attaquant ouvre des pull requests de reconnaissance, cache le voleur dans des workflows ou des scripts shell, puis supprime l'exécution du workflow après son passage. La recherche GitHub affichait plus de 63 000 pull requests de reconnaissance à 07:40 UTC le 2026-11-12.
- Le voleur récolte les secrets du dépôt, la mémoire du runner et les identifiants cloud. Il peut persister dans AWS et Azure. Certains jetons provenaient de l'application GitHub de Fly.io ; aucune compromission de l'infrastructure de Fly.io n'est établie.
- Supprimez les commits et les fichiers, faites tourner chaque secret et chaque rôle cloud accessibles au workflow, et vérifiez la persistance. Aucun CVE n'est attribué, donc il n'y a pas d'entrée dans le KEV de la CISA au 2026-11-12.
Suis-je concerné ?
Vous êtes exposé si l'une de ces conditions est vraie :
- Une pull request intitulée
docs: dependency audit notes, depuis une branche nomméewfx-docs, a été ouverte dans votre dépôt le 2026-11-10 ou après. - Un commit avec l'un des messages de reconnaissance est arrivé sur une branche le 2026-11-10 ou après. Les messages sont
ci: refresh build cache,ci: restore,ci: cleanup,chore: add ci helperetchore: add dependency audit workflow. - Un workflow a exécuté le voleur. Le voleur peut être une étape injectée, un script shell ajouté, un nouveau
.github/workflows/build-audit-<6 characters>.yml, ou un.ci-<6 characters>.shcaché accroché àpostinstall,buildoutest. - La tâche pouvait lire des secrets de dépôt ou d'organisation, un
GITHUB_TOKENcapable de demander des jetons cloud, ou des identifiants sur un runner auto-hébergé. - Vous avez installé l'une des versions de paquet concernées.
Une pull request de reconnaissance signifie que le jeton pouvait écrire dans le dépôt. Elle ne prouve pas que le voleur a été exécuté. L'attaquant ne pousse le voleur que là où les permissions du jeton et la configuration du dépôt le permettent. Le rapport estime que cela représente des milliers de dépôts, pas les 63 000 sondes.
Le signal le plus fort est un numéro d'exécution manquant. Si l'historique Actions saute d'un numéro au suivant juste après l'un des commits de reconnaissance, l'attaquant a probablement exécuté le voleur et supprimé les preuves. Le voleur lit aussi la mémoire du processus Runner.Worker, il peut donc prendre des secrets que le workflow n'a jamais transmis à la tâche.
| Paquet | Écosystème | Version concernée | Version saine |
|---|---|---|---|
digitalelvis/neuronai-studio |
Packagist | 3.14.3 | 3.14.2 ou antérieure |
github.com/onuragtas/docker-env |
Go | v1.0.278 et v1.0.280 | Une version antérieure au 2026-11-11 |
github.com/vkuznet/gocurl |
Go | v0.0.0-20261111095532-8d5aad0cb0da |
Un commit antérieur au 2026-11-11 09:55 UTC |
Les deux modules Go ne contiennent le voleur que dans un fichier de workflow, et les builds Go n'exécutent jamais les fichiers de workflow. Bloquez quand même ces versions, car un scan ou un miroir peut encore les présenter. La version Packagist embarque le script .ci-*.sh et un hook postinstall ; composer install ne l'exécute pas, mais npm install dans le répertoire du paquet le fait.
Vérifiez un clone local pour les commits de reconnaissance, les scripts cachés et les marqueurs du script :
# Probe commits
git log --all --since=2026-11-10 --oneline \
--grep='ci: refresh build cache' \
--grep='ci: restore' \
--grep='ci: cleanup' \
--grep='chore: add ci helper' \
--grep='chore: add dependency audit workflow'
# Hidden scripts and workflows
git log --all --since=2026-11-10 --name-only --pretty=format: \
| grep -E '\.ci-[a-z0-9]{6}\.sh|build-audit-[a-z0-9]{6}\.yml' | sort -u
# Stealer markers
grep -R -E 'WF_CALLBACK|WF_CTOKEN|WF_EXFIL_|WF_TG_BOTS|wfx-loot|WFX-BEGIN' . --exclude-dir=.git
Vérifiez les versions des paquets :
composer show digitalelvis/neuronai-studio 2>/dev/null
go list -m all | grep -E 'onuragtas/docker-env|vkuznet/gocurl'
Comment corriger ?
Supprimez les modifications de l'attaquant, puis faites tourner chaque identifiant accessible au workflow. Supprimer le commit ne suffit pas à annuler le vol, car le voleur a peut-être déjà envoyé les secrets et planté une persistance cloud.
- Annulez le commit malveillant avec
git revert, ou réinitialisez la branche au dernier commit sain. Conservez le commit de l'attaquant et les journaux du workflow comme preuves avant de réécrire l'historique. - Supprimez les branches de l'attaquant, fermez les pull requests de reconnaissance et retirez tout collaborateur ou clé de déploiement ajouté.
- Faites tourner chaque secret que la tâche pouvait lire : secrets de dépôt, d'organisation et d'environnement ; jetons d'accès personnels ; clés de déploiement ; identifiants npm, PyPI, Docker et Vault ; clés SSH ; clés de fournisseurs d'IA.
- Révoquez les sessions et jetons du compte GitHub compromis. Si le jeton appartenait à une application GitHub, retirez l'installation de l'application et reconnectez-la seulement après avoir nettoyé le compte.
- Supprimez la persistance AWS et Azure décrite dans « Que fait le code malveillant ? ».
- Reconstruisez chaque paquet ou version publié entre le commit malveillant et le revert, à partir d'un commit sain.
- Restreignez qui peut pousser sur les branches par défaut, exigez des revues et réglez les permissions de
GITHUB_TOKENen lecture seule par défaut.
Si vous ne pouvez pas faire tourner un identifiant immédiatement, réduisez son périmètre ou désactivez-le. Une clé d'accès AWS de longue durée ou un identifiant fédéré Azure créé par l'attaquant survit à un changement de mot de passe.
Que fait le code malveillant ?
Le voleur est un script shell unique d'environ 1 500 lignes, avec des interrupteurs pour chaque fonction. Il s'exécute en quatre étapes : collecter, élever les privilèges, persister et exfiltrer.
Collecter. Il lit chaque secret et variable du dépôt via ${{ toJSON(secrets) }} et ${{ toJSON(vars) }}, ainsi que le GITHUB_TOKEN de la tâche et sa capacité à demander des jetons cloud. Il vide la mémoire du processus Runner.Worker, qui contient sous forme déchiffrée chaque secret utilisable par la tâche. Il collecte les environnements de processus et les fichiers d'identifiants pour AWS, Azure, GCP, Docker, Kubernetes, npm, PyPI, SSH et Vault. Il prend aussi les clés d'API des fournisseurs d'IA et des agents de code, et, sur les runners auto-hébergés, les identifiants des métadonnées d'instance cloud.
Élever les privilèges. Il utilise le jeton OpenID Connect de la tâche pour ouvrir des sessions AWS, GCP et Azure. Il enchaîne vers d'autres rôles AWS nommés dans le dépôt, usurpe des comptes de service GCP et rejoue les jetons de rafraîchissement Azure CLI en cache pour atteindre Microsoft Graph et Key Vault.
Persister. Il peut réécrire la politique de confiance d'un rôle AWS assumé en "Principal": {"AWS": "*"} et appeler iam:CreateAccessKey pour des utilisateurs IAM, ce qui donne à l'attaquant des clés de longue durée. Il peut ajouter un identifiant fédéré Azure nommé wfx-ci-<n> à une application Azure AD. Les workflows du dépôt compromis peuvent alors se connecter à Azure sans secret stocké. Une troisième fonction peut marquer les vérifications requises comme réussies ; le rapport indique qu'aucune des 101 copies examinées n'avait cet interrupteur activé.
Exfiltrer. Il emballe les données dans une archive gzippée. Le rapport a trouvé trois configurations de livraison. La première envoie un POST HTTP vers 194.150.166[.]244:8080/exfil ou 18.143.168[.]96:8080/exfil. La deuxième ajoute un troisième serveur, 153.75.82[.]159:8080/exfil, et une sauvegarde Telegram. La troisième, vue pour la première fois vers 05:30 UTC le 2026-11-12, n'envoie que vers Telegram, via cinq jetons de bot, dans le chat -1004340163187. Toutes les copies échantillonnées entre 07:00 et 07:40 UTC utilisaient la configuration Telegram uniquement, donc une liste de blocage d'IP construite sur les adresses du 10 novembre ne couvre plus les copies les plus récentes.
Le voleur se cache à quatre endroits :
- Deux étapes ajoutées à un workflow existant. La première transmet les jetons internes de la tâche à l'étape suivante, et la seconde exécute le voleur sous un nom comme
Restore cache x9xx. - Le script ajouté à un script shell existant qu'un workflow exécute déjà, comme
update.shdansnextcloud/dockeroutests/retry_env.shdansPancrePal-xiaoyibao/MinerU-xyb. - Un nouveau workflow,
.github/workflows/build-audit-<6 characters>.yml, sur une branche jetablechore/<8 characters>. - Un
.ci-<6 characters>.shcaché plus un hookpackage.json. Le rapport a trouvé le hook surpostinstall11 fois,builddeux fois ettestune fois.
Après le démarrage du workflow, l'attaquant pousse un revert (ci: restore, parfois ci: cleanup) et supprime l'exécution du workflow avec ses journaux. Dans drunomics/nuxtjs-drupal-ce, le voleur a été commité à 09:17:23 UTC le 2026-11-11 et le revert a suivi 12 secondes plus tard ; l'historique Actions passe directement de l'exécution #1963 à la #1965, et l'exécution #1964 a disparu.
Quand la suppression échoue, le journal de l'exécution montre ce que le voleur a fait. Dans nextcloud/docker, l'exécution survivante a affiché :
memscan_files 3
gcp_gcloud_auth_empty
gcp_live token=0 projects=0 gcloud=0
selfhosted_collected
az_graph_token_fail
oidc_discovery arn=none gcp=none azure=none
archive_bytes 8971
exfil_no_runtime_vars
Ces lignes montrent un runner auto-hébergé, un scan de mémoire qui a trouvé trois fichiers et une archive de 8 971 octets. Elles montrent aussi que les chemins GCP, Azure et OIDC n'ont pas produit de jeton dans cette exécution. L'envoi de l'archive a réussi, car le script affiche un message de nouvelle tentative en cas d'échec et le journal n'en contient aucun. Cette exécution a collecté des données sur la machine elle-même, pas seulement sur le runner hébergé par GitHub.
Comment la faille est-elle exploitée ?
- L'attaquant détient un jeton volé avec accès en écriture au dépôt. Le jeton peut appartenir à un développeur, à un compte bot ou à une application GitHub.
- L'attaquant ouvre la pull request de reconnaissance depuis
wfx-docs. La pull request ajoute un fichier statique,docs/wfx-audit.md, et indique à l'attaquant que le jeton peut écrire. - L'attaquant pousse le voleur dans l'un des quatre emplacements. Le commit ressemble à une tâche de maintenance, et l'auteur est souvent un compte bot qui peut contourner la protection de branche.
- Un workflow s'exécute sur le commit. Le voleur collecte les secrets, demande des jetons cloud et envoie l'archive. L'attaquant annule le commit et supprime l'exécution, généralement en moins d'une minute.
- L'attaquant utilise les identifiants cloud volés ou la persistance plantée après le nettoyage du dépôt. Le rapport indique que le même acteur peut revenir par la persistance même après la révocation du jeton.
Le rapport attribue 173 pull requests de reconnaissance, depuis 19 comptes, à des jetons émis pour l'application GitHub de Fly.io (fly-io, ID d'application 34557). L'application a un accès en écriture aux contenus, aux actions et aux pull requests, ce qui suffit pour chaque étape.
Le rapport indique qu'il n'a pas établi comment ces jetons ont été obtenus et qu'il n'a aucune preuve d'une compromission de l'infrastructure de Fly.io. La majeure partie de la campagne utilise d'autres identifiants, comme des jetons d'accès personnels.
Comment se déroule l'attaque ?
Un jeton volé ouvre une pull request de reconnaissance, et l'attaquant pousse un voleur qui s'exécute dans un workflow normal. L'exécution envoie les secrets et les jetons cloud avant que l'attaquant n'annule le commit et ne supprime l'exécution.
flowchart LR
A["Jeton GitHub volé"] --> B["Pull request de reconnaissance<br/>docs: dependency audit notes"]
B --> C["Pousser le voleur<br/>workflow, script shell ou hook npm"]
C --> D["Exécution GitHub Actions<br/>lit les secrets et la mémoire de Runner.Worker"]
D --> E["OIDC cloud<br/>AWS, GCP, Azure"]
D --> F["Archive exfiltrée<br/>serveur HTTP ou Telegram"]
E --> G["Persistance<br/>politique AWS, identifiant Azure"]
F --> H["L'attaquant annule le commit<br/>et supprime l'exécution"]
Que peut faire un attaquant ?
Un attaquant disposant d'un jeton d'écriture peut lire chaque secret accessible au workflow, y compris les secrets que le workflow ne référence jamais, car le voleur vide la mémoire du processus du runner. Il peut utiliser le jeton cloud de la tâche pour assumer des rôles dans AWS, GCP et Azure, et planter une persistance qui survit à la rotation des jetons.
L'impact sur un dépôt dépend de ce que le workflow peut faire. Un dépôt qui n'a qu'un accès en lecture à son propre code perd moins qu'un dépôt doté d'un rôle de déploiement cloud ou d'un jeton de publication npm. Un runner auto-hébergé qui détient des identifiants de production est le pire cas. L'exécution survivante de nextcloud/docker citée par le rapport montre le voleur en train de lire la machine du runner elle-même.
Une pull request de reconnaissance seule ne donne rien de tout cela à l'attaquant. Le voleur doit s'exécuter, et le jeton doit avoir les permissions utilisées par le workflow. Le rapport ne publie ni nombre de victimes ni liste de vols de données confirmés.
Que faire si mon dépôt a été touché ?
Si un workflow a exécuté le voleur, traitez le dépôt, le runner et chaque identifiant accessible à la tâche comme compromis. Supprimer le paquet ou le commit n'est que la première étape.
- Isolez le runner auto-hébergé et conservez son disque, sa liste de processus et ses journaux. Ne le réutilisez pas avant de l'avoir reconstruit à partir d'une image saine.
- Conservez les journaux de l'exécution du workflow et les commits de l'attaquant. Le numéro d'exécution manquant et l'historique des commits sont des preuves pour la revue d'incident.
- Faites tourner chaque secret et jeton que la tâche pouvait lire, depuis une machine saine. Commencez par les rôles cloud et les clés de longue durée, car le voleur les cible en premier.
- Supprimez la politique de confiance AWS qui autorise
"AWS": "*", supprimez les clés d'accès IAM que vous n'avez pas créées et retirez les identifiants fédérés Azure nomméswfx-ci-<n>. - Cherchez de nouveaux dépôts, collaborateurs, clés de déploiement, webhooks et fichiers de workflow créés avec le jeton volé.
- Reconstruisez tout paquet publié pendant la présence du voleur et publiez-le à nouveau depuis un commit sain avec un nouveau numéro de version.
Si vous avez seulement installé digitalelvis/neuronai-studio 3.14.3 et que le hook postinstall ne s'est pas exécuté, retirez le paquet et épinglez 3.14.2 ou antérieur. Si le hook s'est exécuté, suivez les mêmes étapes d'incident.
Comment vérifier si j'ai été compromis ?
Commencez par les recherches GitHub et les vérifications locales de « Suis-je concerné ? ». La pull request de reconnaissance, les messages de commit et les scripts cachés sont des preuves publiques.
# On GitHub: probe pull requests in your org
https://github.com/search?q=%22docs%3A+dependency+audit+notes%22+is%3Apr&type=pullrequests
Cherchez un numéro d'exécution manquant dans l'onglet Actions juste après l'un des commits de reconnaissance. Une exécution qui a disparu est le signe le plus clair que le voleur s'est exécuté et que l'attaquant a nettoyé.
Cherchez dans le code les serveurs de collecte et les marqueurs du script :
grep -R -E '194\.150\.166\.244|18\.143\.168\.96|153\.75\.82\.159|WF_CALLBACK|WF_CTOKEN|WF_TG_BOTS' . --exclude-dir=.git
Vérifiez la persistance cloud :
# AWS: a role that trusts every account is the planted back door
aws iam get-role --role-name <role-name> --query 'Role.AssumeRolePolicyDocument'
aws iam list-access-keys --user-name <user-name>
# Azure: federated credentials named wfx-ci-<n>
az ad app federated-credential list --id <application-id>
Le rapport liste ces indicateurs :
- Serveurs de collecte :
194.150.166[.]244:8080/exfil,18.143.168[.]96:8080/exfilet153.75.82[.]159:8080/exfil. - Jetons de rappel :
b42d70a7aab9140ca19e8249et15f44heehnax8jdf41yx, envoyés commet=. - Telegram :
api.telegram.orgsendDocument, chat-1004340163187, ID de bots8708988906,9196217635,9180059428,8902935164et8876515636. - Pull request de reconnaissance : titre
docs: dependency audit notes, branchewfx-docs, fichierdocs/wfx-audit.md, blob gitee6e5addfe591923fc385553cb66cf0d25634a6b. - Branches et fichiers :
chore/[a-z0-9]{8},ci-check-[0-9]{4},.ci-[a-z0-9]{6}.shet.github/workflows/build-audit-[a-z0-9]{6}.yml. - Marqueurs du script : étapes nommées
Pass runtimeetRestore cache <4 characters>ouRun audit; variablesWF_CALLBACK,WF_CTOKEN,WF_EXFIL_*etWF_TG_BOTS; dossier de staging.wfx-$$; notes de journalmemscan_files,archive_bytesetdeliver_retry.
Le rapport ne publie pas de nombre de victimes, et une exécution supprimée peut ne laisser que le numéro manquant comme preuve.
Chronologie
- 2026-11-10 : la campagne commence. Les premières copies envoient des données à
194.150.166[.]244. - 2026-11-11 09:17 : le voleur est commité dans
drunomics/nuxtjs-drupal-ce; le revert suit 12 secondes plus tard et l'exécution #1964 est supprimée. - 2026-11-11 10:02 à 10:36 : une rafale de pull requests de reconnaissance utilise des jetons émis pour l'application GitHub de Fly.io.
- 2026-11-12 01:10 : un canal de sauvegarde Telegram apparaît dans les échantillons.
- 2026-11-12 05:30 : les nouvelles copies passent à une livraison Telegram uniquement.
- 2026-11-12 07:40 : la recherche GitHub renvoie plus de 63 000 pull requests de reconnaissance.
- 2026-11-12 10:47 : Step Security publie son analyse.
FAQ
Une pull request de reconnaissance signifie-t-elle que mon dépôt a été compromis ?
Non. La sonde montre seulement que le jeton volé pouvait écrire dans le dépôt. Cherchez le commit du payload, le script caché et une exécution de workflow manquante. Si le voleur s'est exécuté, faites tourner les secrets.
GitHub ou Fly.io ont-ils subi une compromission ?
Le rapport indique qu'il n'a aucune preuve d'une compromission de l'infrastructure de Fly.io. L'application GitHub de Fly.io a émis des jetons aux utilisateurs qui ont connecté leur compte, et l'attaquant a utilisé certains de ces jetons. La façon dont les jetons ont été obtenus n'est pas établie. La plateforme GitHub elle-même n'est pas mise en cause : la campagne abuse de jetons GitHub valides et de fonctionnalités normales des workflows, ce qui explique aussi l'absence de CVE.
Existe-t-il un CVE pour cette campagne ?
Non. La campagne abuse de jetons GitHub valides et de fonctionnalités normales des workflows, donc aucun CVE n'est attribué. Les versions de paquets concernées sont listées par leurs registres, pas par un CVE.
La campagne est-elle dans le KEV de la CISA ?
Non. Aucun CVE n'est attribué, et le KEV de la CISA liste des CVE. Il n'y a pas d'entrée KEV au 2026-11-12.
Quels paquets contiennent le voleur ?
digitalelvis/neuronai-studio 3.14.3 sur Packagist, github.com/onuragtas/docker-env v1.0.278 et v1.0.280, et la pseudo-version v0.0.0-20261111095532-8d5aad0cb0da de github.com/vkuznet/gocurl. Les deux modules Go ne le contiennent que dans un fichier de workflow.
L'attaquant a supprimé l'exécution. Puis-je encore savoir si elle s'est exécutée ?
Le numéro d'exécution manquant et le commit de reconnaissance sont les preuves. Une exécution qui disparaît entre deux exécutions numérotées juste après le commit est le motif. Faites tourner les identifiants même si vous ne pouvez pas récupérer le journal.
Références
- Step Security, « WFX Phantom: The GitHub Actions Secret Stealer That Deletes Its Tracks », publié le 2026-11-12 : https://www.stepsecurity.io/blog/wfx-phantom-the-github-actions-secret-stealer (consulté le 12 novembre 2026)
- Packagist, versions 3.14.2 à 3.14.4 de digitalelvis/neuronai-studio : https://packagist.org/packages/digitalelvis/neuronai-studio (consulté le 12 novembre 2026)
- Proxy de modules Go, github.com/onuragtas/docker-env v1.0.278 et v1.0.280 : https://pkg.go.dev/github.com/onuragtas/docker-env (consulté le 12 novembre 2026)
- Proxy de modules Go, pseudo-version de github.com/vkuznet/gocurl : https://pkg.go.dev/github.com/vkuznet/gocurl (consulté le 12 novembre 2026)
- GitHub, pull request de reconnaissance citée par le rapport : https://github.com/EbukaLovesTech/2gether/pull/1 (consulté le 12 novembre 2026)