Poisoned Pipeline / Incidents / AS304
GhostAction steals CI secrets, then autoruns a stealer from a fake font
Git / GitHubGitHub ActionsVS Code / IDEDeveloper workstation
On 9 October 2026 StepSecurity, Socket, and OpenSourceMalware
published the same GhostAction wave. On 8 October two maintainer
accounts pushed a workflow named Security Audit
(.github/workflows/security-audit.yml) into every repository they
could write. StepSecurity times the kitao sweep at 13:20-13:44 UTC
(27 repositories, including kitao/pyxel, about 18,400 stars) and the
henrywoo sweep at 21:10-21:26 UTC (318 repositories: 39 source and
279 forks). uber/athenadriver was the last commit of that sweep, at
21:25:54 UTC, straight to master. henrywoo is the original author
and still had write access after the project moved under Uber.
Socket counts the same burst as 346 repositories by keeping
uber/athenadriver outside the henrywoo namespace. Commit messages
were "Add security audit workflow" and "Update security audit
workflow". Author and committer were the victim identities. The
athenadriver commit e3c0dfa is unsigned. Socket did not observe how
the credentials were taken. StepSecurity's working hypothesis is a
leaked personal access token. That is not established.
The October payload keeps the older named-secret theft and adds a
working-tree and full-history sweep. It triggers on push, with no
branch filter, and on workflow_dispatch. Checkout uses fetch-depth
0. Named secrets found in the repo's own workflows are templated
into the script before the commit. pyxel's copy names
CARGO_REGISTRY_TOKEN, PERSONAL_ACCESS_TOKEN, PYPI_PASSWORD, and
PYPI_USERNAME. Repos with those names POST to
http://193.32.204.199/?c=monami. Repos with an empty secret list
POST to /?c=new. The new half greps the tree for thirteen patterns
(AWS access key ids and secrets, AWS session tokens, Anthropic,
OpenAI, and OpenRouter keys, GitHub PATs, GitLab tokens, Google
API keys, Slack tokens, SendGrid keys) and runs git log -p --all,
capped at 200,000 lines, so a secret deleted from the current tree
is still collected. AWS key ids are shipped with two lines of
context so the secret key can be paired. The body is one cleartext
HTTP POST. There is no DNS lookup.
StepSecurity read the athenadriver Actions log (run 37846728441).
The server answered OK at 21:26:04 UTC, four seconds after the run
started. That copy used /?c=new, so the delivered body was the
tree and history sweep, not a list of named Actions secrets. The
job's GITHUB_TOKEN was contents:read. pyxel's workflow completed
five times, three from the attacker's pushes and two from
workflow_dispatch at 13:36:32 and 13:42:35 UTC, issued from the
kitao session. Socket says no malicious PyPI or crates.io version
was published from this activity. pyxel's last release is v2.9.9
on 12 August 2026. On 7 October the already-compromised
xxyangyoulin account pushed an empty commit and a README edit to
typecho-fans/plugins, infected since 5 September, to re-fire the
older workflow. Both runs stopped in action_required because that
repository requires approval.
This is the same operation GitGuardian published on 7 October, not
a second actor. Between 31 August and 30 September the workflow
github_actions_security.yml was pushed to 772 public repositories
belonging to 373 accounts, targeting 2,577 secret names, and posted
to the same IP. Of 3,669 collected runs, 336 completed and
GitGuardian counts 26 secrets from 13 repositories. A 7 September
variant, security-check.yml, in 7 repositories posts to port 3000
with an injection id. By 5 October, 124 of those 772 repositories
(16 percent) had a public cleanup commit. In 92 cases the attacker
only retargeted a workflow left from an earlier wave. StepSecurity
first sees 193.32.204.199 in a public repo on 5 September and in
honeypot records on 4 September. On 9 October a GitHub code search
for that IP under .github/workflows returned 378 default-branch
hits, 182 with the history-mining marker and 88 with ?c=monami.
Code search omits forks, so the 279 henrywoo forks sit outside
that 378. A Socket update the same day, clock time left blank on
the page, says more than 500 accounts have committed the workflow
to tens of thousands of repositories since 7 October. The two
named accounts remain the ones both labs reconstructed.
The campaign GitGuardian named in September 2025 used the same
workflow shape against 817 repositories and at least 3,325 secrets,
then to bold-dhawan.45-139-104-115.plesk.page and
carte-avantage.com. Later endpoints were 170.39.218.2 and
Interactsh *.oast.fun. OpenSourceMalware treats the October posts
as an escalation of that campaign. my-github.com, registered 9
October, resolves to 193.32.204.199. my-gitlab.com, registered 22
September, is a separate registration. OpenSourceMalware does not
claim common ownership of that second domain.
On 11 October OpenSourceMalware published a phishing email shown
as coming from noreply@my-github.com. It impersonated a GitHub
security alert, named the recipient, and used the path /SDRVuhYw,
the same path public scans saw hours after the domain was
registered. The page is a GitHub reverse proxy. Its script posts
to /login/device/code for client id Ov23ctrqCqZ79UaYKwyq with
scopes repo, workflow, and read:org, then submits the device-flow
authorization through the proxied session. The access token is
sent to my-github.com on TCP/8443. Static markers are a vh_device
cookie, /vh_beacon, and console strings VH1 through VH5.
OpenSourceMalware did not prove the recipient submitted the form.
The HAR they kept does not contain the device-code or token
requests. This is the same server as the workflow exfiltration,
not a second operation.
On 16 October OpenSourceMalware published a second stage that
landed on 15 October. It persists after the workflow is deleted.
The same GitHub access commits .vscode/tasks.json and
public/fonts/fa-solid-400.woff2. The task is labeled eslint-check,
hidden, and set to runOn folderOpen. The command is
node ./public/fonts/fa-solid-400.woff2. The file is 38 lines of
plain Node.js, byte-identical in the 79 repositories they could
clone. It is not obfuscated JavaScript, and it does not use a
blockchain loader. OpenSourceMalware labels it a copy of the
PolinRider launcher, not an extension of that campaign. No shared
indicator with DPRK malware was found.
The cluster is 94 repositories and 66 owners. Fifteen were
deleted, renamed, or made private before analysis. Of the 79
that remained, 65 had gone more than a year without a commit
when the first malicious commit landed. On 8 October, between
13:03 and 22:41 UTC, 72 of those 79 received the first Stage 1
workflow. Four more received it on 12-14 October. Stage 2's
main burst was 15 October, 01:00-10:31 UTC. In 76 repositories
the second workflow, the font, and tasks.json landed together.
The median gap from Stage 1 was 6.3 days. psm-2/ai-artifacts
received Stage 2 at 10:30 UTC with no Stage 1. cashpost and
bangcv-flutter removed the Stage 1 workflow on 9 October and
still received Stage 2 on 15 October, at 02:32 and 07:48 UTC.
Deleting the workflow did not revoke the push credential.
The font reads the first 3,000 bytes of ~/.aws/credentials,
~/.pypirc, ~/.npmrc, ~/.git-credentials, ~/.netrc,
~/.claude/.credentials.json, ~/.gemini/oauth_creds.json, and
~/.kube/config. It reads ~/.ssh/id_rsa and ~/.ssh/id_ed25519
in full. It greps the home directory for AWS, Anthropic, GitHub,
npm, and PyPI token shapes, capped at 30 matches and a 10 second
timeout. It appends environment variables whose names match
AWS_, ANTHROPIC, OPENAI, HF_TOKEN, or GITHUB_TOKEN. The same
expression also lists names that begin with an underscore. One
cleartext HTTP POST goes to 193.32.204.199 port 8080. Batch
tags on the copies are c=reinject, c=auto_20261013, and
c=security. c=security is the 12 October tag on
raihanlimit/ainebot-md, the first move to port 8080.
The same script installs a cron line marked ghc2. Every five
minutes the machine fetches
http://193.32.204.199:8080/c2/ plus its hostname and pipes the
body to bash. OpenSourceMalware had not retrieved what that
path returns. Deleting the font does not remove the cron job.
The same burst adds a second workflow. It is Stage 1 with more
patterns, a wiki scan that uses the job's GITHUB_TOKEN, and a
secret-gist upload if the POST fails. Filenames differ:
lint.yml, ci-check.yml, build-check.yml, repo-health.yml,
audit.yml, code-review.yml, and test-suite.yml, each in 5 to
16 repositories. mammarraza/reactapp also has cache-cleanup.yml,
display name Runner Maintenance, added 15 October at 06:24 UTC,
set to run on self-hosted runners. That copy reads the instance
metadata IAM role, reads SSH keys, and installs the same cron.
It is the only workflow in the set that reaches the host under
the runner.
daxitzadafiya/bagisto-dhs is in this cluster and also has a
PolinRider Tailwind-config loader. The force-push is 25 June
2026. OpenSourceMalware found no shared indicator with the 15
October files. That loader stays on AS45.
Mapped techniques
- T0113 Compromised user account
- T0147 Scan public CI/CD configurations for secrets and vulnerable actions
- T0169 Push implants across repositories
- T0166 Trigger pipeline execution
- T0140 Harvest tokens from environment variables
- T0184 Harvesting sensitive information from files
- T0199 Secret leak
- T0138 Backdoor in code
- T0210 Poison AI coding-agent config or workspace
- T0152 IDE
- T0189 Secrets in configuration files
- T0172 Runtime backdoor
- T0148 Scheduled tasks on self hosted runner
- T0168 Exfiltration to code repositories
supply-chain Stolen maintainer identity, workflow pushed to every writable repo
Commits used the victim accounts, including kitao, henrywoo,
and xxyangyoulin. How the credential was taken was not
observed.
Secret names in the payload match names already referenced
in that repository's workflows. The list is filled in before
the commit.
One credential is used on every repository it can write,
including forks and repositories dormant for years. The 8
October bursts were minutes long.
The workflow runs on push and on workflow_dispatch. The
injection push itself starts a run. On pyxel the attacker
also dispatched the workflow twice by hand.
supply-chain Named secrets plus git history, posted in cleartext
${{ secrets.NAME }} values are placed in the curl body.
This is not a dump of the whole environment. athenadriver's
copy had an empty name list.
The October workflow greps the checkout and git log -p
--all. A secret removed from the current tree is still in
scope. fetch-depth 0 is what makes that history available
on the runner.
athenadriver's run received OK from 193.32.204.199.
GitGuardian counts 26 secrets from 13 repositories in the
August-September window. No malicious registry publish was
observed after the pyxel token names were templated.
supply-chain Fake font autoruns on folder open, then a five-minute cron
tasks.json and the fake font stay in the repo after the
workflow is deleted. cashpost and bangcv-flutter removed
Stage 1 on 9 October and still received these files on
15 October.
.vscode/tasks.json is hidden, labeled eslint-check, and
set to runOn folderOpen. The command is node on
public/fonts/fa-solid-400.woff2.
Opening the folder in VS Code runs that node command.
The file is plain JavaScript with a font extension.
Node does not check the extension.
The script reads AWS, PyPI, npm, git, netrc, Claude,
Gemini, and kube config files, plus both SSH private
keys. A home-directory grep keeps 30 token matches.
Names matching AWS_, ANTHROPIC, OPENAI, HF_TOKEN, or
GITHUB_TOKEN are appended to the same report. This is
the developer process environment, not Actions secrets.
The report is one cleartext POST to 193.32.204.199
port 8080. Tags are c=reinject, c=auto_20261013, and
c=security.
A crontab line marked ghc2 runs every five minutes,
fetches /c2/ plus the hostname, and pipes the body to
bash. The returned script was not recovered. Deleting
the font does not remove the job.
mammarraza/reactapp cache-cleanup.yml, display name
Runner Maintenance, is runs-on self-hosted and installs
the same cron. One workflow, not a campaign-wide runner
compromise.
The second workflow uploads findings to a secret gist
if the POST fails. OpenSourceMalware did not show a
gist that was created.
Framework gaps noted
- The theft of the GitHub credential was not observed, so this is not T0112 or T0153. A leaked token is the labs' hypothesis.
- Direct pushes skipped pull-request review. The labs do not establish an admin role, so this is not T0182.
- Recovering a deleted secret from git log -p is not its own technique. T0184 is the file harvest. The history pass is called out in that comment.
- Plain HTTP to a raw IP is how the post leaves the runner. T0145 is the opposite case, an encrypted channel used to hide traffic.
- No malicious PyPI, crates.io, npm, or container publish is reported from these stolen publishing secrets. RSOLV-dev/rsolv-action is named only as a repo where a later tainted release would spread. That release was not observed.
- A 30 August XMRig commit on kuafuai/DevOpsGPT used an account that later received a GhostAction workflow. GitGuardian does not attribute the miner to this operator. Forged author email, one repository, and a broken Docker Hub image. Not mapped here.
- my-gitlab.com is not confirmed campaign infrastructure.
- The 11 October kit shows how a token with repo, workflow, and read:org would be taken. Submission of that email was not proven, so this is not a new T0112, and it does not establish how kitao or henrywoo were taken. There is no technique for the phishing page or the device-flow consent.
- OpenSourceMalware had not retrieved the body served from the cron URL. The five-minute job is T0172. T0148 is only the one self-hosted workflow. Reading an IAM role from the instance metadata service is not its own technique.
- daxitzadafiya/bagisto-dhs also has a June PolinRider loader. That operation is AS45. This URL is listed there too. The two are not merged.
Sources
- https://www.stepsecurity.io/blog/ghostaction-returns
- https://socket.dev/blog/ghostaction-cloud-credentials
- https://opensourcemalware.com/blog/ghostaction-attack-escalates
- https://opensourcemalware.com/blog/ghostaction-phishing-campaign
- https://blog.gitguardian.com/ghostaction-github-actions-supply-chain-attack-returns/
- https://blog.gitguardian.com/ghostaction-campaign-3-325-secrets-stolen/
- https://www.stepsecurity.io/blog/ghostaction-campaign-over-3-000-secrets-stolen-through-malicious-github-workflows
- https://opensourcemalware.com/blog/ghostaction-copycats-polinrider-by-hiding-a-stealer-in-a-fake-font-file