Threat feed

Gheist

Gheist: stolen GitHub tokens dump repo secrets into Actions logs with no C2 server

Posted · Updated · Spectrum Delta Research Team

Summary

Wiz Research (Oct 25, A/3) designates Gheist, an automated campaign tracked since September 15, 2026 that performs mass abuse of stolen GitHub tokens: for every repository a token can reach, it pushes a branch named ci-sync- plus six random characters carrying a single new file .github/workflows/sync-check.yml (commit message 'ci: sync config', unsigned, authored as the token's owner). The triggered workflow prints the job environment and the repository's secrets — base32-encoded, which defeats GitHub's verbatim-only log masking — into its own Actions log; the tool then downloads the log through the GitHub API and deletes the run seconds later. There is no attacker server: the secrets leave through GitHub itself. Wiz recovered more than 13,500 planted branches from more than 850 compromised accounts, from personal projects to enterprise organizations' private repositories. GitHub's public-repository approval hold stopped every public run checked (action_required), but the hold does not apply to private repositories or GitHub Enterprise Server, where audit logs show runs completing and being deleted by the 'gheist' user agent and CloudTrail shows organizations' own workflows assuming AWS roles from ci-sync- branches. A later variant adds a sync-host job on self-hosted runners that searches the host for 31 token formats, private keys, .env files, Terraform state and cloud instance-role credentials.

Breakdown

Adversary

Gheist — unattributed automated campaign tracked by Wiz Research since September 15

Procedures (4)

  1. Stolen token pushes ci-sync- branch with sync-check.yml

    Valid accounts: stolen GitHub tokens implant workflows at repository scale

    Logs

    • GitHub push and commit events · ci-sync- branch adds only sync-check.yml; commit 'ci: sync config' unsigned

    Detectors

    • Wiz Defend: Commit Created With Known Malicious Commit Message · commit with known malicious message (ci: sync config)
Click to expand remaining (3) procedures
  1. Workflow prints env and secrets base32 into Actions log

    Unsecured credentials: secrets printed to Actions log as base32, defeating log masking

    • T1552Unsecured Credentials

    Logs

    • GitHub audit logs · ci-sync-* runs deleted within a minute; gheist user agent activity

    Detectors

    • Wiz Defend: Workflow Pushed With Secret Dump · pushed workflow printing env/secrets to job log (base32 encoded)
  2. Log downloaded via API; run deleted seconds later

    Indicator removal: log retrieved via GitHub API, run deleted seconds later

    Logs

    • GitHub audit logs · ci-sync-* runs deleted within a minute; gheist user agent activity

    Detectors

    • Wiz Defend: New Workflow Completed And Deleted In Short Time Span · workflow run completed and deleted within a minute on ci-sync-* branch
  3. sync-host job pivots self-hosted runner host

    Escape to host: sync-host job searches runner host for tokens and cloud credentials

    • T1611Escape to Host

    Logs

    • Self-hosted runner job logs and host telemetry · gdmp-pivot markers; .gdmp_pat/.gdmp_patg/.gdmp_sweep files; secret-search stages

    Detectors

    No known detector

Indicators of compromise

Hunt only

(Wiz (https://www.wiz.io/blog/gheist-attempted-scaled-abuse-of-stolen-github-tokens-stumbles), A/3) — user agent 'gheist'; branch pattern ^ci-sync-[0-9a-z]{6}$ (Tool A suffix hexadecimal, Tool B suffix base36); workflow file .github/workflows/sync-check.yml; commit message 'ci: sync config'; secret-dump marker "echo -n '${{ toJSON(secrets) }}' | base32 -w 120"; separator marker LAFA==== (base32 of 'X', added Oct 19); self-hosted stage headers '===== gdmp-pivot: <stage> =====' with helper files .gdmp_pat, .gdmp_patg, .gdmp_sweep; workflow blob SHAs — plain f3da2b8b357dcda02fd455d314be2a95007516a6, timeout d09535857a741bbb976bdc084a9a021713133291, separator 1dc5af9cce950aad1eb1546ecce8565b117540c4, self-hosted job c665b778b2d822453293003e7ca0bbb2cb08e39d. Overlaps: PolinRider (DPRK-linked) victims re-pushed Gheist commits Oct 19-20 with a loader appended to vite.config.js behind whitespace padding, plus api.js run by package.json before every dev/build; Oct 6 lure-repo wave (391 accounts renamed ~1,461 repositories to cracked-software lures such as fl-studio-crack, solana-sniper-lite); Oct 18 free-compute attempt (25 accounts each created a 'Windows RDP Runner' repository within nine minutes); Megalodon overlap (57 first-phase accounts, May); Trivy compromise (March) as credential source. Assessed 2026-10-26, hunt-only — the branch/commit/markers are durable campaign signatures; roughly two-thirds of implicated accounts were already removed from GitHub as of Oct 24, likely response activity, so account-level IOCs perish while the tooling signature persists.

Detailed

Campaign signature: user agent 'gheist'; branch names ci-sync- plus six characters (Tool A hexadecimal suffix, Tool B base36 suffix; regex ^ci-sync-[0-9a-z]{6}$); workflow file .github/workflows/sync-check.yml; job 'sync'; commit message 'ci: sync config' (unsigned, authored as token owner); secret-dump commands 'env | base32 -w 120' and "echo -n '${{ toJSON(secrets) }}' | base32 -w 120"; separator 'echo X | base32 -w 120' printing LAFA==== (added Oct 19); self-hosted job 'sync-host' with runs-on: self-hosted; stage headers '===== gdmp-pivot: <stage> ====='; helper files .gdmp_pat, .gdmp_patg, .gdmp_sweep; timeout-minutes: 6 (Tool B). Workflow blob SHAs: plain f3da2b8b357dcda02fd455d314be2a95007516a6, timeout d09535857a741bbb976bdc084a9a021713133291, separator 1dc5af9cce950aad1eb1546ecce8565b117540c4, self-hosted job c665b778b2d822453293003e7ca0bbb2cb08e39d; environment-matrix variants differ per repository. Scale: 13,500+ planted branches, 850+ compromised accounts, tracked since Sept 15; ~2/3 of accounts removed as of Oct 24; campaign ongoing and evolving. Adjacent waves sharing the account pool: Oct 6 — 391 accounts renamed ~1,461 repositories to cracked-software lures (fl-studio-crack, solana-sniper-lite), wiping 610 ci-sync branches as a side effect, 166 of those accounts never in Gheist activity; Oct 18 — 25 accounts each created a 'Windows RDP Runner' repository within nine minutes (free-compute attempt); Megalodon overlap — 57 first-phase accounts also Megalodon victims in May; PolinRider overlap — three victims re-pushed Gheist commits Oct 19-20 with a loader appended to vite.config.js behind whitespace padding, two also gained api.js run by package.json before every dev and build; Trivy compromise (March) as credential source. All from the primary (Wiz (https://www.wiz.io/blog/gheist-attempted-scaled-abuse-of-stolen-github-tokens-stumbles), A/3).

Sources

Questions, detections or sightings of Gheist? Bring them to the community.

Discuss in the Tradecraft Slack