Back to Newsroom
Threat Intel

Gheist: Stolen GitHub Tokens Turn Actions Logs into a Secret-Exfiltration Channel

An automated campaign is using stolen GitHub tokens to plant a one-file workflow that prints every secret a repository can read into its own Actions log, then downloads the log and deletes the run. With over 13,500 planted branches across 850+ accounts, the exposure is concentrated in private repositories and GitHub Enterprise Server, where GitHub's hold on suspicious runs does not apply.

Gheist: Stolen GitHub Tokens Turn Actions Logs into a Secret-Exfiltration Channel

Gheist: Stolen GitHub Tokens Turn Actions Logs into a Secret-Exfiltration Channel

A fully automated campaign, tracked under the name Gheist, is using stolen GitHub tokens to plant a single-file workflow in every repository those tokens can write to. The workflow prints every secret the repository can read into its own GitHub Actions log, the attackers download the log through the GitHub API, and the run is deleted seconds later. No attacker server is ever contacted: the secrets leave through GitHub itself.

The operation has been running since at least mid-September and was still active and evolving as of 24 October. More than 13,500 planted branches have been identified across more than 850 compromised accounts, ranging from personal side projects to the private repositories of enterprise organisations. How many of those organisations actually had secrets taken is not known.

How the attack works

The tool needs exactly one thing: a GitHub token with write access to a repository. With it, the sequence is always the same.

  1. Plant a branch. A new branch named ci-sync- followed by six random characters is pushed to the repository. It contains a single commit, with the message ci: sync config, that adds one file: .github/workflows/sync-check.yml. The commit is unsigned and carries the name of the token's owner. The push itself triggers the workflow.
  2. Dump the secrets. The workflow prints the job's environment and every secret the job can read, encoded in base32. GitHub masks secret values in logs, but masking is not guaranteed once a value has been encoded or otherwise transformed. The base32 output no longer matches the stored secret, so it is written to the log in full.
  3. Collect and erase. The tool downloads the workflow log via the API and deletes the run within seconds.

Two details make this variant harder to spot and harder to contain. The workflow declares an empty permissions block, which restricts the job's GITHUB_TOKEN (which the attackers do not need) but does nothing to stop it from reading secrets. And one version of the tool runs an extra job for every deployment environment configured in the repository, so environment-scoped secrets, frequently production credentials, are printed as well.

Deleting the branch or the run does not undo the exposure. By the time either is gone, the log has already been downloaded.

The campaign takes its name from the user agent string gheist, which the tool sends with every GitHub API request.

Why private repositories and Enterprise Server carry the real risk

Since 28 July, GitHub has held workflow runs it identifies as potentially malicious until a collaborator with write access approves them. Every public-repository Gheist run examined so far, across all versions of the tool, was still waiting in that hold, so the payload never executed.

That safeguard currently applies only to public repositories on github.com. It does not cover private repositories, and GitHub Enterprise Server does not have it at all. In those environments the picture is different:

  • GitHub audit logs show runs on the planted branches finishing and then being deleted by the gheist user agent.
  • AWS CloudTrail records show the organisation's own workflows also executing on the ci-sync- branches and assuming AWS roles, which confirms that Actions ran there without any approval step.
  • In enterprise organisations, the tool cycled through tokens belonging to several users and automation accounts within minutes, cloning private repositories before planting the workflow. Hours later, cloud credentials from the same organisation were used to enumerate AWS Secrets Manager. Where those credentials came from, and who used them, has not been established.

The hold also only catches runs that GitHub's heuristics flag. A separate campaign, GhostAction, kept running in public repositories until GitHub began blocking it on 9 October. Gheist is distinct from GhostAction and uses different workflow files.

A new self-hosted runner variant

Around 19 October, one version of the tool started pushing a second job aimed at self-hosted runners, the machines organisations operate themselves to process Actions jobs, and appears to target only repositories with such a runner registered. The job searches the runner for credentials across 31 token types: private keys, process environments, AWS instance role credentials, runner credential files and the Kubernetes service account token. It currently looks buggy, and no successful execution has been observed, but the intent is clear: pivot from CI secrets to the infrastructure behind the runner.

Where the tokens come from, and who else is using them

The stolen tokens that could be traced originate from infostealer logs and from tokens exposed in earlier supply-chain incidents, including the compromise of the Trivy vulnerability scanner in March. The same compromised accounts are being shared with other operators:

  • On 6 October, 391 accounts, many of them also used by Gheist, renamed roughly 1,461 repositories to pose as cracked software and distribute malware. That renaming wiped 610 of the planted branches.
  • Some victims also appear to be infected with PolinRider, malware that has been associated with North Korean operators. On 19 and 20 October, three of them re-pushed their Gheist commits with a loader added to a vite.config.js file.
  • Fifty-seven of the accounts had already been hit by Megalodon, a campaign that pushed secret-stealing workflows into thousands of GitHub repositories in May.

How to check your repositories

  • Look for branches matching ci-sync- plus six characters where the only change is a new .github/workflows/sync-check.yml file with the commit message ci: sync config.
  • In GitHub audit logs, look for workflow runs on ci-sync-* branches that were deleted within a minute of finishing, and for any activity from the user agent gheist.
  • If your workflows authenticate to a cloud provider with OIDC, search cloud audit logs for role assumptions or token exchanges whose subject includes a ci-sync- branch. This works even if you do not retain GitHub logs. Note that, by default, the OIDC subject includes the branch name only when the job does not use a deployment environment.
  • On self-hosted runners, look for the files .gdmp_pat, .gdmp_patg and .gdmp_sweep, and for the string gdmp-pivot in job logs.

Indicators to feed into your detection tooling:

User agent:                 gheist
Branch:                     ^ci-sync-[0-9a-z]{6}$
Commit message:             ci: sync config   (adds only .github/workflows/sync-check.yml)
Log string:                 toJSON(secrets) }}' | base32 -w 120
Log string:                 ===== gdmp-pivot:
Files (runner):             .gdmp_pat  .gdmp_patg  .gdmp_sweep
Blob SHA (plain):           f3da2b8b357dcda02fd455d314be2a95007516a6
Blob SHA (timeout):         d09535857a741bbb976bdc084a9a021713133291
Blob SHA (separator):       1dc5af9cce950aad1eb1546ecce8565b117540c4
Blob SHA (self-hosted job): c665b778b2d822453293003e7ca0bbb2cb08e39d

Versions of the workflow that enumerate a repository's deployment environments differ per repository and therefore have no fixed hash.

What to do if you find a ci-sync branch

  • Rotate every repository, organisation and environment secret the repository could reach. Assume all of them were read.
  • Revoke the tokens that pushed the commits, and check the owners' devices for infostealer malware.
  • If a ci-sync- branch ran on a self-hosted runner, treat that machine and its cloud instance role as compromised.
  • Do not approve held sync-check runs, and do not run code from ci-sync- branches.

Making a stolen token worth less

  • Require reviewers on environments that hold production secrets. A job cannot read an environment's secrets until a required reviewer approves it.
  • Restrict OIDC trust policies to protected branches or environments, so a branch an attacker created cannot assume your cloud roles.
  • Prefer GitHub Apps or short-lived, fine-grained tokens over long-lived personal access tokens.
  • Run self-hosted runners as ephemeral machines, without Docker socket access and without open access to cloud metadata.

Gheist is a reminder that a CI platform is a credential store with a build button attached. The root of this campaign is not a GitHub vulnerability but a steady supply of developer tokens harvested by infostealers and leaked in earlier breaches. Knowing when your organisation's tokens surface in those stealer logs and leak dumps, before someone automates their use, is exactly what our Deep/Dark Web monitoring service is built to catch.

SHARE