{"slug": "gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in", "title": "Gheist: 13,500 secret-dumping workflows planted with stolen GitHub tokens — audit yours in 5 minutes", "summary": "A five-week campaign dubbed Gheist used stolen GitHub tokens to plant secret-dumping workflows in 13,500 branches across more than 850 compromised accounts, including private enterprise repositories, according to an autopsy published by Wiz. The workflows dumped environment variables and secrets — including Anthropic, Hugging Face, Groq and NVIDIA API keys — to logs that were then fetched and deleted via the GitHub API, and Wiz confirmed follow-on AWS role assumption from stolen cloud credentials via CloudTrail. Public repositories were largely spared by GitHub's approval hold on first-time contributor workflow runs, a guardrail that does not apply to private repos or GitHub Enterprise Server.", "body_md": "While everyone watched LiteLLM, a quieter campaign spent five weeks doing something worse: taking stolen GitHub tokens and committing a **secret-dumping workflow into every repository they could reach** — **13,500 planted branches across 850+ compromised accounts**, personal projects and **private enterprise repos** included. Wiz published the full autopsy yesterday. The campaign is called Gheist, it dumps your org secrets, your cloud credentials, and — the line that made me write this — it specifically hunts **AI provider keys: Anthropic, Hugging Face, Groq, NVIDIA**. Your agents' keys, straight out of your CI.\n\nThis post is the mechanics and the audit. The whole check runs in five minutes with `gh`, tonight, on your own account and orgs.\n\nGheist's tooling takes a stolen token (sourced from infostealer logs and prior supply-chain incidents — the same family that hit Trivy in March), enumerates every repo it can touch, and plants a branch `ci-sync-XXXXXX` containing one workflow, committed as `ci: sync config`, unsigned. The workflow is two dumb lines: dump `env` and `toJSON(secrets)`, **base32-encoded**, print to the workflow log. Then the tool fetches the log through GitHub's API and deletes the run.\n\nFour details make it clever, and all four are worth internalizing:\n\n`$SECRET` prints safely to the attacker.`permissions: {}` protects nothing here.`secrets` context. Half the hardening advice on the internet answers a different attack.\nOn self-hosted runners the payload went further — searching for 31 token formats (private keys, `.env` files, Terraform state, `/proc/*/environ`, Docker sockets, Kubernetes tokens) and AI provider keys specifically. Wiz notes that variant is buggy — wrong runner paths, broken regex, no GCP/Azure collection — which is somehow worse: the campaign is a product under active iteration, shipping features.\n\nThe single most interesting fact in the autopsy: **public repos were largely saved by one quiet GitHub guardrail**. Since July 28, workflow runs from first-time contributors in public repos sit in `action_required` pending collaborator approval — and every public Gheist run Wiz checked sat there, payload never executed.\n\nThat hold **doesn't apply to private repos or GitHub Enterprise Server**. There, the workflows ran, secrets were dumped, and Wiz proved follow-on AWS role assumption from the stolen cloud credentials via CloudTrail. The guardrail you never configured protected your open source; nothing protected the private stuff, which is where the secrets actually live.\n\nTwo more findings worth the price of admission: `permissions: {}` on the malicious workflow was used *by the attacker* — cosmetically, since secrets don't care — and about two-thirds of the implicated accounts have since vanished from GitHub, suggesting response is underway while the campaign kept evolving through October 24.\n\nThe radar: the commit message is searchable in public history, and it's noisy (legit `sync config` commits everywhere), so treat it as a lead generator, not a verdict:\n\n```\ngh api 'search/commits?q=\"ci: sync config\"' --jq '.total_count'\n# → 321 hits right now. Most are innocent sync scripts. Your job is the branch check below.\n```\n\nThe actual self-audit — planted branches on your account (loop your repos, grep the branch pattern):\n\n```\ngh repo list --limit 200 --json nameWithOwner --jq '.[].nameWithOwner' | while read r; do\n  gh api \"repos/$r/branches\" --paginate --jq '.[].name' 2>/dev/null | grep -E '^ci-sync-[0-9a-z]{6}$' && echo \"  ↑ HIT in $r\"\ndone\n```\n\nFor **organizations** (org owners only — the audit-log API isn't available on personal accounts; the web UI's security log is the personal fallback):\n\n```\n# Workflow runs deleted within a minute of completing — Gheist's cleanup signature\ngh api 'orgs/YOURORG/audit-log?include=all&phrase=workflow_run' \\\n  --jq '.[] | select(.action==\"workflow_run.deleted\")'\n# The tool's user agent\ngh api 'orgs/YOURORG/audit-log?include=all' --jq '.[] | select(.user_agent==\"gheist\")'\n```\n\nAnd the **cloud-side hunt that works even without GitHub logs** — Wiz's best trick: OIDC role assumptions whose session/subject contains `ci-sync-` (GitHub's OIDC subject includes the ref, so the planted branch name travels into your CloudTrail):\n\n```\naws cloudtrail lookup-events --lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \\\n  --start-time 2026-09-15 --max-results 20 | grep -i 'ci-sync'\n```\n\nIf anything hits: **rotate every secret that repo could see** — deleting runs doesn't un-dump them — revoke the pushing tokens, and check the token owners' machines for infostealers, because that's where the tokens came from.\n\nFrom Wiz's list, ranked by how little they cost: require **reviewers on environment-scoped secrets** (kills the environment matrix); scope OIDC trust policies to **protected branches only** (kills `ci-sync-*` role assumption); prefer **GitHub Apps or fine-grained, short-lived tokens** over classic PATs (shrinks what one stolen token reaches); run self-hosted runners **ephemerally, no Docker socket, no unrestricted metadata** (kills the host pivot). And never approve a held `sync-check` run sitting in `action_required` — that's the trap's front door.\n\nGheist = stolen tokens + one two-line workflow + base32 + GitHub-as-C2, at a scale of 13,500 branches. Public repos got lucky via the first-contribution hold; private repos paid. The audit is five minutes of `gh` and one CloudTrail query — and if you run AI agents in CI, the campaign's shopping list literally names your provider. Check tonight.\n\nSources: [Wiz — Gheist: attempted scaled abuse of stolen GitHub tokens](https://www.wiz.io/blog/gheist-attempted-scaled-abuse-of-stolen-github-tokens-stumbles) · [CISA KEV / alerts hub](https://www.cisa.gov/news-events) · commit-search audit via `gh api` (commands above, verified live)", "url": "https://wpnews.pro/news/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in", "canonical_source": "https://dev.to/slabb/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-yours-in-5-minutes-299f", "published_at": "2026-10-26 07:06:31+00:00", "updated_at": "2026-10-26 07:13:04.498997+00:00", "lang": "en", "topics": ["ai-safety", "ai-agents", "developer-tools", "ai-infrastructure"], "entities": ["Gheist", "Wiz", "GitHub", "Anthropic", "Hugging Face", "Groq", "NVIDIA", "AWS"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in", "markdown": "https://wpnews.pro/news/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in.md", "text": "https://wpnews.pro/news/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in.txt", "jsonld": "https://wpnews.pro/news/gheist-13500-secret-dumping-workflows-planted-with-stolen-github-tokens-audit-in.jsonld"}}