GitHub held Gheist on public repos. The private run already printed the secrets.
Wiz 25 October. Stolen GitHub tokens plant sync-check.yml on every reachable repo. Secrets leave as base32 in Actions logs through GitHub's own API. The July hold does not cover private repositories or GitHub Enterprise Server. Deleting the branch is not the rotation.
Bottom line up front
CONFIRMED On 25 October 2026 Wiz Research published Gheist, a campaign it has tracked since 15 September. Stolen GitHub tokens plant a workflow named sync-check on every repository the token can write. The job prints the runner environment and the GitHub secrets context as base32, so GitHub's log mask does not match. The tool then downloads the log through the GitHub API and deletes the run seconds later. API requests carry the user agent gheist. Wiz recovered more than 13,500 planted branches from more than 850 compromised accounts, including enterprise private repositories.
Do not collapse five facts.
- The July hold is public-only. Since 28 July 2026, GitHub Actions holds some workflow runs in public repositories on github.com until a write collaborator approves them from an authenticated web session. Wiz: every public Gheist run it checked, including the later self-hosted job, sat at
action_required. The payload did not run. GitHub's own changelog: the hold does not apply to private repositories, and GitHub Enterprise Server does not have it. - Private and GHES already executed. Audit logs show
ci-sync-*runs completing, then being deleted by thegheistuser agent. CloudTrail showed the organization's own workflows running on those branches and assuming AWS roles. That is Actions without the hold, not a scan. - Deleting the branch is not the close. The secrets left in the log before the run was deleted. Rotate every repository, organization, and environment secret the repository could read. Revoke the token that pushed. A clean git history is a hygiene step, not a rotation.
- This is not a CVE and not yesterday's LiteLLM wheels. Gheist is stolen-token abuse. Anyone with write access can read the secrets the workflow can see; GitHub documents that. It is distinct from GhostAction (different workflow files; GhostAction exfil to 193.32.204.199). It is distinct from [LiteLLM 1.107.0](/posts/2026-10-25-litellm-11070-cook-pth/). Same class of GitHub workflow theft, different operator, different payload, different blast radius.
- This is not Collabora, not Arista, and not a PLC. CISA KEV is still catalog
2026.10.23/ 1756. [CVE-2026-110447](/posts/2026-10-24-collabora-code-cve-2026-110447-kev/) and [CVE-2026-114625](/posts/2026-10-23-arista-velocloud-cve-2026-114625/) are due today, 26 October. No Gheist CVE is on that list.
The operator sentence: if any private repo or GHES instance has a ci-sync- branch whose only commit is .github/workflows/sync-check.yml with message ci: sync config, assume the secrets printed, rotate them from a clean identity, revoke the pushing token, and do not approve a held sync-check run.
What happened
Gheist is not a GitHub product bug. It is an automated harvester sitting on tokens that already had write access.
For each reachable repository the tool creates a branch named ci-sync- plus six random characters, then commits .github/workflows/sync-check.yml with message ci: sync config. The commit is unsigned and authored as the token owner. The push starts the workflow.
High-level, from Wiz. This is not a how-to.
- Job name
sync-check,on: [push],runs-on: ubuntu-latest. - The build step base32-encodes
envand'${{ toJSON(secrets) }}'. - GitHub masks secret values in logs only when they appear verbatim. Encoded output does not match, so it prints.
- An empty
permissions:block restricts the job'sGITHUB_TOKEN, which the attacker does not need. It does not restrict thesecretscontext. - When the repository has deployment environments, a later variant lists them in a job matrix (
environment: ${{ matrix.environment }}) so environment-scoped secrets — often production — print too. - The log leaves through GitHub's API. There is no attacker domain and no outbound C2 to block.
Two tools ran in parallel.
| Variant | Distinguisher | Notes |
|---|---|---|
| Tool A | Hexadecimal branch suffix | Plain payload |
| Tool B | Base36 suffix | From 28 September: timeout-minutes: 6 and the environment matrix, payload varied per repository |
On 19 October, Tool B inserted echo X | base32 -w 120 between dumps, which prints LAFA====, assessed as a parser separator. Hours later it pushed a second job, sync-host, runs-on: self-hosted, apparently only to repositories with a registered self-hosted runner. That job writes .gdmp_pat, .gdmp_patg, .gdmp_sweep and logs stages under ===== gdmp-pivot:. Wiz describes it as buggy (wrong runner key file, broken URL-credential regex, does not collect GCP or Azure tokens) and reports no successful run.
In enterprise organizations Wiz saw the tool cycle through tokens of several users and automation accounts within minutes, clone private repositories before planting the workflow, and hours later use cloud credentials from the same organization to enumerate AWS Secrets Manager.
As of 24 October: the campaign was ongoing and evolving. Roughly two-thirds of implicated accounts were no longer on GitHub. Wiz reported the activity to GitHub and assesses the disappearances as response, not as the operator going quiet.
Why it matters
Three operational reasons this is the daily.
First, the control you already have does not cover the repos that hold production secrets. The July hold worked on public github.com. Private repositories and GHES — the places with environment secrets, OIDC cloud roles, and self-hosted runners — executed. A dashboard that only shows held public runs is a false green.
Second, exfil rides a channel defenders allow. There is no webhook.site, no pastebin, no odd ASN on the workflow's egress. The job talks to GitHub. Network allow-lists that "only GitHub" do not catch it. Detection is audit-log and CloudTrail, or the branch itself.
Third, the account is already in a shared credential pool. Wiz traces some tokens to infostealer logs and to tokens exposed in earlier supply-chain events, including the March Trivy compromise. The same accounts show other operators: 6 October lure-repo renames (391 accounts, ~1,461 repos, wiping 610 ci-sync branches as a side effect); 18 October "Windows RDP Runner" free-compute attempts on 25 Gheist accounts; PolinRider loaders re-pushed onto three Gheist commits on 19–20 October; 57 first-phase accounts were also Megalodon victims in May. That is overlap on compromised identities, not attribution of Gheist to those campaigns.
Who / what is affected
- Confirmed in this retrieval: more than 850 GitHub accounts, more than 13,500 planted
ci-sync-branches, personal projects through enterprise private repositories. Successful workflow execution on private github.com repositories and on GitHub Enterprise Server. Follow-on AWS Secrets Manager enumeration from credentials tied to the same organizations. - Not a census: Wiz does not publish how many organizations had secrets actually dumped, how many tokens still work, or a victim list. Branch count is not secret-dump count. Account count is not tenant count.
- Public github.com: Wiz: every checked Gheist run remained held. Treat a held
sync-checkas evidence of token abuse, not as "nothing happened." Do not approve it. - Self-hosted runners: the
sync-hostjob is in the toolkit as of 19 October. Wiz reports it buggy and no successful run. If aci-syncbranch did run on a self-hosted runner, treat the host and its instance role as compromised anyway. - Token class: write access to the repository. Fine-grained vs classic PAT vs GitHub App vs stolen
GITHUB_TOKENof a different workflow is not fully enumerated in the public write-up. Hunt the push, not a single token type. - OT / ICS: UNKNOWN for process impact. This is source control and CI. Do not merge it with Siemens Software Center, a plant historian, or today's Collabora / Arista KEV due dates.
Technical context
Defensive mapping, not an exploit recipe. Wiz already printed the public workflow. RWP is not publishing the self-hosted job body.
ATT&CK (defensive, not attribution): T1078 valid accounts (stolen GitHub tokens); T1552.001 credentials in files / CI secrets; T1195 supply-chain adjacent (workflow as the collection step, not a poisoned package); T1562 impair defenses (delete the run); T1098 account manipulation only where other operators later rewrite the same repos. Actor identity: UNKNOWN. Nation-state for Gheist itself: UNKNOWN. Wiz did not attribute Gheist to a DPRK actor. PolinRider on three overlapping victim repos is a separate infection on already-stolen accounts.
Published indicators (Wiz / THN):
| Indicator | Value | |
|---|---|---|
| User agent | gheist | |
| Branch | ^ci-sync-[0-9a-z]{6}$ | |
| Commit message | ci: sync config, adding only .github/workflows/sync-check.yml | |
| Workflow strings | `toJSON(secrets) }}' \ | base32 -w 120; ===== gdmp-pivot:` |
| Host files | .gdmp_pat, .gdmp_patg, .gdmp_sweep | |
| Git blob SHA (plain) | f3da2b8b357dcda02fd455d314be2a95007516a6 | |
| Git blob SHA (timeout) | d09535857a741bbb976bdc084a9a021713133291 | |
| Git blob SHA (separator) | 1dc5af9cce950aad1eb1546ecce8565b117540c4 | |
| Git blob SHA (self-hosted job) | c665b778b2d822453293003e7ca0bbb2cb08e39d |
Environment-matrix variants differ per repository and have no single hash.
OIDC detection that does not need GitHub logs: if workflows federate to a cloud provider, look for role assumptions or token exchanges whose subject includes a ci-sync- branch. GitHub's OIDC subject includes the branch name when the job is not bound to a deployment environment. Wiz used that to prove Actions ran.
Threat / exploitation status
| Claim | Status | Evidence |
|---|---|---|
Stolen GitHub tokens plant sync-check.yml on ci-sync-* branches | CONFIRMED | Wiz 25 Oct |
| >13,500 branches, >850 accounts, including enterprise private repos | CONFIRMED | Wiz |
| Secrets printed as base32 in Actions logs, log pulled via GitHub API, run deleted | CONFIRMED | Wiz; THN 26 Oct |
Public github.com runs held at action_required; payload did not run in those checks | CONFIRMED | Wiz; GitHub changelog 28 Jul 2026 |
| Hold does not cover private repos or GHES | CONFIRMED | GitHub changelog; Wiz audit-log and CloudTrail |
Private / GHES runs completed and were deleted by user agent gheist | CONFIRMED | Wiz |
| Cloud credentials from the same orgs used to enumerate AWS Secrets Manager | CONFIRMED (follow-on) | Wiz; origin of those cloud creds not fully named |
| Campaign ongoing and evolving as of 24 Oct | CONFIRMED | Wiz |
| Distinct from GhostAction | CONFIRMED | Wiz; GitGuardian GhostAction uses different workflows and 193.32.204.199 |
Distinct from LiteLLM 1.107.0 cook.pth | CONFIRMED | different artifact, different vendor, different dates |
sync-host self-hosted job successful in the wild | NOT shown | Wiz: buggy; no successful run reported |
| Gheist attributed to DPRK / PolinRider operator | NOT shown | three overlapping victim repos; Wiz does not attribute Gheist |
| Victim-org census, ransomware use | UNKNOWN | not in this retrieval |
Gheist CVE on CISA KEV catalog 2026.10.23 / 1756 | CONFIRMED absent | KEV JSON this run |
What defenders should do
- Hunt the branch, not the hash. List branches matching
^ci-sync-[0-9a-z]{6}$whose only change is.github/workflows/sync-check.ymlwith commitci: sync config. Environment-matrix copies will not match the four blob SHAs. Unsigned commit, authored as a real org member, is expected — that is the stolen token. - Audit log. Workflow runs on
ci-sync-*deleted within a minute of completion. Any API activity with user agentgheist. Do not treat a missing run as a miss; deletion is the design. - CloudTrail / equivalent without GitHub logs. OIDC role assumption or token exchange whose subject contains
ci-sync-. That is the private-repo proof Wiz used. - Self-hosted runners. Files
.gdmp_pat,.gdmp_patg,.gdmp_sweep. Stringgdmp-pivotin job logs. If present, isolate the runner, rotate its instance role, and treat Docker socket / host filesystem as in-scope even though Wiz has not confirmed a successfulsync-hostrun. - If you find a hit: rotate first. Every repository, organization, and environment secret that repo could read — including production environment secrets and cloud keys the workflows federate. Revoke the pushing credential. Check that identity's laptop for infostealers. Rotate downstream (PyPI, npm, cloud, AI provider keys) that the dumped secrets could mint. Deleting the branch is last.
- Do not approve held
sync-checkruns. Do not check out or run code fromci-syncbranches. - Harden so the next stolen token prints less. Required reviewers on environments that hold production secrets (a job cannot read those secrets until a reviewer approves). Scope OIDC trust policies to protected branches or named environments, not
*. Prefer GitHub Apps or short-lived fine-grained tokens over classic PATs. Run self-hosted runners as ephemeral VMs without Docker socket or open cloud metadata. - Do not close adjacent tickets on this row. [Collabora 110447](/posts/2026-10-24-collabora-code-cve-2026-110447-kev/) and [Arista VCO 114625](/posts/2026-10-23-arista-velocloud-cve-2026-114625/) are due today. [LiteLLM 1.107.0](/posts/2026-10-25-litellm-11070-cook-pth/) is still
cook.pthon a dirty interpreter. GhostAction is a different workflow. Huntress's Microsoft 365 token-spray (79 orgs, 14 July–22 September, published 22 October) is a different identity plane.
RWP assessment
High confidence that Gheist is a real, ongoing stolen-token campaign that plants sync-check.yml on ci-sync-* branches, that public github.com runs Wiz checked were held, and that private repositories and GitHub Enterprise Server executed the payload and deleted the runs.
High confidence that encoded toJSON(secrets) defeats verbatim log masking, that an empty permissions block is not a secrets control, and that deleting the run or branch does not retract secrets that already printed.
High confidence that this is not GhostAction, not LiteLLM 1.107.0, not a GitHub zero-day CVE, and not on KEV catalog 2026.10.23 / 1756.
Moderate confidence in the AWS Secrets Manager follow-on as Gheist-derived rather than a second operator on the same stolen cloud keys. Wiz observed it in the same organizations, hours later. Origin of those cloud credentials is not fully named.
Low confidence in actor identity, in how many of the 13,500 branches produced a usable secret dump, and in whether the 19 October self-hosted job ever ran cleanly after the public write-up.
Confidence that this is not OT process impact: high. No evidence in this retrieval that a controller, safety system, or physical process was affected.
This outranked the rest of the window because it is a new campaign, never a Daily Top, with a primary technical report in the last 24 hours, confirmed execution in private/GHES, a built-in false green (the public hold), and a built-in false close (delete the branch). Collabora and Arista are Friday's KEV rows; their federal clock runs out today. Huntress's M365 token-spray is real BEC tradecraft, but the campaign window ended 22 September and the Huntress post is 22 October — secondary THN pickup today is not a material increment. BlackX is a 2022 Baton Drop bootkit on diplomatic endpoints from early 2026, disclosed 22 October; the interesting control lesson (manual Secure Boot revocation) is not today's CI incident. PHP2Shell remains off KEV.
What we are watching
- Whether GitHub extends the malicious-workflow hold to private repositories and GHES, or ships an equivalent control.
- Payload rename off
ci-sync-/sync-check/ user agentgheist. String IOCs will rot; the behavior (one new workflow, run deleted inside a minute, unsigned commit, OIDC subject on a random branch) will not. - A named enterprise victim or a confirmed successful
sync-hostrun. - CISA KEV catalog still
2026.10.23/ 1756. Collabora 110447 and Arista 114625 due 26 October. PHP2Shell 109618 / 109619 still absent. - Huntress M365 token-spray: 79 orgs, 497 successful sign-ins,
193.26.115.0/24(AS210558, 1337 Services GmbH), 48 rogue Entra devices. Campaign 14 July–22 September. Password reset without removing attacker MFA methods and devices is the false close there — separate identity plane. - Kaspersky BlackX (22 October): UEFI bootkit on patched Windows at diplomatic agencies in Tajikistan, Uzbekistan, and Turkmenistan; CVE-2022-21894 (Baton Drop) plus manual Secure Boot revocation of Windows Production PCA 2011. APT41 moderate confidence per Kaspersky. Not this story.
Sources
Primary: Wiz Research, 25 October 2026 (campaign mechanics, variants, hold vs private/GHES, IOCs, AWS follow-on). Independent secondary: The Hacker News, 26 October. Control documentation: GitHub changelog 28 July 2026 (hold scope); GitHub Actions security and OIDC docs. Catalog check this run: CISA KEV JSON 2026.10.23 / 1756. Adjacent, not this story: GitGuardian GhostAction (distinct); Huntress M365 token-spray 22 October; LiteLLM Daily Top 25 October; Collabora Daily Top 24 October; Arista Daily Top 23 October.
Sources
- Wiz Research — Gheist: Attempted Scaled Abuse of Stolen GitHub Tokens Stumbles, 25 Oct 2026
- The Hacker News — Gheist Uses Stolen GitHub Tokens to Steal Private Repository Secrets via Actions Logs, 26 Oct 2026
- GitHub Changelog — GitHub Actions holds potentially malicious workflows for approval, 28 Jul 2026
- GitHub Docs — Security hardening for GitHub Actions
- GitHub Docs — OpenID Connect
- CISA — KEV JSON feed catalog 2026.10.23
- GitGuardian — GhostAction returns, 7 Oct 2026 (distinct campaign)
- Huntress — M365 Token Spray and Replay Campaign, 22 Oct 2026
- RWP Ventures — LiteLLM Daily Top 2026-10-25
- RWP Ventures — Collabora CODE Daily Top 2026-10-24
- RWP Ventures — Arista VeloCloud Daily Top 2026-10-23