At 12:45:20 UTC on 10th November 2026, seven unrelated GitHub accounts opened the same pull request against seven different repositories. All seven appeared in the same second.
Over the next two days, the pattern repeated tens of thousands of times.
The campaign has been called wfx-loot and WFX Phantom. Early reporting put the impact at 4,837 compromised accounts, 61,764 probe pull requests, at least 1,806 cases of confirmed exfiltration, and 8,093 repositories containing malicious commits. The numbers were still moving as we investigated.
Several research teams documented what wfx did once it reached a repository. We wanted to look further back: how had the attacker acquired so many GitHub credentials, and what could the earliest activity tell us?
We searched public GitHub data, examined the first 900 probe pull requests, and traced an overlap with an earlier campaign known as GhostAction. That overlap is significant.
How wfx works

The attacker opens a pull request titled docs: dependency audit notes from a branch named wfx-docs. It adds a two-line file, docs/wfx-audit.md. The pull request tests whether the credential the attacker holds can write to that repository.
Within roughly a minute, the attacker makes one of three changes:
Injects ~ 1,500 lines into an existing GitHub Actions workflow, under the commit message
ci: refresh build cache.Adds a hidden
.ci-xxxxxx.shscript and an npm postinstall hook inpackage.json, underchore: add ci helper.Creates a new workflow on a temporary
chore/branch followed by eight random characters, underchore: add dependency audit workflow.
The code targets CI secrets, workflow tokens, cloud credentials accessible through OIDC, and information available on runners. Exfiltration endpoints included 194.150.166[.]244:8080/exfil (roughly half the samples), 18.143.168[.]96:8080/exfil, and 153.75.82[.]159:8080/exfil. From 01:10 UTC on 12th November, the attacker also used a Telegram chat for exfiltration, first as a fallback and then as the sole channel.
Finally the attacker reverses the changes with commits like ci: restore or ci: cleanup, then deletes the workflow run and its logs. The repository appears normal unless you check the history. One possible clue is a missing Actions run number: run #41 followed by run #43, for example.
By 07:40 UTC on 12th November security researchers had counted more than 63,000 probe pull requests. Our own search returned 64,737. Both figures capture a campaign that was still active at the time of analysis.
The first 74 minutes
We searched GitHub for the probe title, ordered the results by creation time, and pulled the earliest 900 results:
Those 900 pull requests span the first 74 minutes of the campaign. The opening sequence stood out immediately.

The first thirteen pull requests look like a test run. Then activity stops for almost seventeen minutes before the attacker launches at much higher volume.
We cannot see what happened during that pause. The attacker may have been checking whether the first probes succeeded, but the timing alone cannot tell us. What it does show is that the campaign did not begin with a gradual trickle of newly compromised accounts. The attacker appears to have had a substantial pool of usable credentials ready before the larger launch.
What the tokens could do, across those 900 probes:
65% targeted repositories the account owner controls; 26% repositories where they're a collaborator; the rest organization or contributor access.
86% were PR #1: the first pull request that repository had ever received. These are small personal projects, not hand-picked targets.
Account ages run from GitHub's early years to 2026 with no cluster, and every author is a regular user account.
894 of 900 PR bodies are word-for-word identical ("Routine dependency audit notes."). The other six differ only because AI review bots appended summaries. There are no per-victim tracking markers.
The script walked every repository each token could write to. That fits broad-scope credentials, the kind the GitHub CLI stores, rather than narrowly scoped CI tokens, which lines up with the one victim who reported a leaked gh OAuth token.

Following the GhostAction trail
An earlier campaign, GhostAction, offered another lead. Reports had noted that some accounts affected by GhostAction in October were also hit by wfx, and some repositories still contained a malicious workflow called security-audit.yml.
The IP address 193.32.204[.]199 had been identified in reporting as GhostAction infrastructure. We used it to search the public GitHub code index:
The search produced 217 file matches. After reviewing we identified 186 malicious workflow files across 134 accounts and organisations. Four of those accounts were no longer searchable at the time of our analysis.
The filenames were chosen to blend in: security-audit.yml, github_actions_security.yml, audit.yml, ci-check.yml, build-check.yml, code-review.yml, lint.yml, repo-health.yml, cleanup_cache.yml, cleanup-job.yml, cache-cleanup.yml, runner-maintenance.yml, and maintenance.yml.
How much overlap did we find?
Of the 130 GhostAction-affected accounts still searchable, 33 also authored wfx probe pull requests: around 25% of the group. Together, those accounts opened 653 probe pull requests, including 32 against repositories belonging to other users.
Twenty-eight of the 33 accounts first appeared in our wfx probe data on 10th November. The remaining five appeared the following day. In all 32 cross-repository cases examined, the GhostAction infection preceded the wfx activity.
Another ten GhostAction-affected accounts had their repositories probed by other compromised accounts, but did not themselves author wfx probes in our dataset. Counting those gives 43 of 130 accounts, or approximately 33%, touched by both campaigns in some form.
The overlap varied by GhostAction payload family:

The dates inside the payload labels
Some GhostAction files contained auto_YYYYMMDD labels. We compared those labels with the latest push times we observed.

The labels closely track when those batches were pushed. Some activity crossed midnight UTC while retaining the previous day's label, consistent with a batch named when it started.
We also saw signs of repeat targeting. Files carrying auto_20261013 and auto_20261021 labels sometimes replaced earlier malicious files, and some files first planted in September 2025 were updated on 20th – 21st October 2026. GhostAction was not always a one-time intrusion: some repositories were revisited.
What the GhostAction workflows actually did
We examined a sample from each variant, with the destination address defanged here as hxxp://193.32.204[.]199. We have omitted live secrets from the examples.
The workflows we examined were configured to run on push and workflow_dispatch. That means the malicious code could execute as soon as it was committed.
og1: send a repository secret to an external server
The simplest sample called itself Github Actions Security. Its job was named send-secrets:
It attempted to send a Cloudflare API token from the repository's secrets to a hard-coded IP address over HTTP.
The combination of set +e and silent curl output (-s) makes failures less obvious.
og2 and oidc_og2: turn OIDC access into AWS credentials
The OIDC variants go further. One sample requests a GitHub OIDC token with an AWS audience, searches environment variables for values shaped like IAM role ARNs, and attempts to exchange the token for temporary AWS credentials.
A shortened excerpt shows the mechanism:
In a repository whose AWS trust policy permits the exchange, the result can be live, temporary cloud credentials. The payload attempts to send those results to the attacker. The sample also included a separate request designed to transmit repository secrets; the victim example in our dataset referenced 61 named secrets.
The distinction matters: even repositories without long-lived AWS keys in their secrets may still expose cloud access through overly permissive OIDC trust.
p16n: search code history and use the victim's own gist as a fallback
The p16n variant checks out full Git history rather than a shallow copy:
Its patterns look for AWS credentials; keys and tokens associated with Anthropic, OpenAI, OpenRouter, Groq, xAI, and Google; and credentials for Slack, SendGrid, GitLab, and GitHub.
The sample also attempts to clone and search the repository's wiki. If the main exfiltration endpoint cannot be reached, it can create a private GitHub gist inside the compromised account, with the description build cache and the collected material in c.dat.
That fallback is especially easy to miss. The data may remain in the victim's own account rather than appearing in an obvious outbound network transfer. It is worth checking private gists during incident response.
reinject: go after the self-hosted runner itself
1. Disguise and trigger
The workflow calls itself "Cache Cleanup" in order to look like routine housekeeping. It runs on every push, so it starts the moment it's committed. The line runs-on: self-hosted: instead of GitHub's short-lived cloud runners, it targets the victim's own machines, which hold valuable credentials. set +e stops the script on no error, so a failed step never stops the theft.
2. Identifying the machine
It starts building a report with the repo name, the hostname, the user it's running as, and the IP addresses. This information is to inform the attacker which machine and which network they have gained access to.
3. Stealing the machine's cloud credentials
169.254.169.254 is the AWS instance metadata service, which every EC2 machine can reach. The script asks for a session token, finds the IAM role attached to the machine, and requests that role's temporary AWS access keys. If the runner is on AWS, the attacker now has whatever cloud access the machine has.
4. Reading credential files
It goes through a list of files where developer tools store logins and copies up to 3,000 bytes of each one it finds:
Cloud: AWS, Kubernetes, Oracle Cloud
Package registries: PyPI, Cargo (Rust), RubyGems, npm
Git:
~/.git-credentialsand~/.netrcAI tools: Claude, Gemini, OpenAI, Hugging Face
Docker
~/.config/gh/hosts.yml, where the GitHub CLI stores its login token. That's the same token type a wfx victim confirmed was used to take over their account.
5. Copying SSH keys
It copies the machine's SSH private keys, plus the SSH config and known_hosts. This allows the attacker to push to GitHub as the owner, or log in to other servers.
6. Google Cloud and Azure logins
It reads the JSON files where the Google Cloud and Azure command-line tools keep their login sessions.
7. Searching the disk for AWS keys
It searches every file under /home and /root for anything that looks like an AWS access key ID (AKIA… or ASIA…).
8. Searching the disk for AI service keys
The same disk-wide search, now for API keys from Anthropic (sk-ant-), OpenAI (sk-proj-, sk-svcacct-), OpenRouter (sk-or-v1-), Google (AIza, and ya29. OAuth tokens), Groq (gsk_) and xAI (xai-). It covers /opt and /srv too, where server software tends to live.
9. Searching the disk for GitHub and GitLab tokens
This is the most important part for the wfx connection. It searches for every kind of GitHub token:

It also searches for GitLab tokens (glpat-).
10. Reading .env files
It finds .env files in the home folders and copies them.
11. Reading environment variables
It takes any environment variables that look like creds: starting with AWS_, contain Anthropic, OpenAI, Gemini, Google or Azure, or ending in _API_KEY or _TOKEN.
12. Uploading everything
The report goes to the attacker's server, over HTTP. ?c=reinject is the attacker's label. -s keeps it silent and || true hides failures, so the workflow run looks successful.
13. Installing a backdoor
It then adds a job to the machine's crontab that, every 5 minutes, downloads a script from the attacker (/c2/<hostname>, unique to each machine) and runs it with. This means the attacker will have ongoing remote control after the workflow finishes, even if the malicious workflow is deleted. To check a runner, look for ghc2 in crontab -l. If it's there, treat the machine as compromised.
The numbers, together
Measurement | Finding |
|---|---|
Reported accounts compromised in wfx | 4,837 |
wfx probe PRs returned by our search on 12 November 2026 | 64,737 |
Malicious GhostAction workflow files found | 186 |
Accounts and organizations hosting those files | 134 (130 still searchable) |
Searchable GhostAction-affected accounts that authored wfx probes | 33 of 130 (25%) |
Searchable GhostAction-affected accounts touched by wfx in any way | 43 of 130 (33%) |
Cross-repository cases where GhostAction preceded wfx | 32 of 32 |
GhostAction workflow files later injected into by wfx | 18 |
ssh1 / ssh19 accounts that later authored wfx probes | 21 of 43 (49%) |
These figures represent different populations and methods. The reported campaign-wide counts should not be treated as directly interchangeable with our GitHub search results.
What we know, and what we don't
Our data establishes the early timing of wfx, the large-scale probe pattern, the surviving GhostAction workflows, and the measurable overlap between the two campaigns. We can also inspect what the GhostAction payloads were designed to collect.
The first 13 probes and the later burst suggest that the attacker started with a ready-made set of credentials. The access pattern is consistent with broadly authorised user tokens. But we have not established where all those credentials originated, or whether GhostAction and wfx share an operator.
The strongest connection is narrower, and more defensible: many accounts compromised through GhostAction were later used in wfx, and at least one GhostAction variant was built to steal the types of tokens that wfx uses.
How to check whether your repositories were affected
Repository and runner checks to prioritise during investigation.
Start with the evidence the attacker may have left behind:
Review GitHub Actions run history for unexplained missing run numbers, especially near commits titled
ci: refresh build cache,chore: add ci helper,chore: add dependency audit workflow,ci: restore, orci: cleanup.Search pull requests for
docs: dependency audit notes, branches namedwfx-docs, and the filedocs/wfx-audit.md.Inspect .
github/workflowsfor unexpected changes, outbound requests to the hosts below, or?c=parameters associated with exfiltration.Look for hidden
.ci-xxxxxx.shfiles and unexpected npmpostinstallhooks.Review private gists, particularly any described as build cache with a file named
c.dat.On self-hosted runners, check scheduled jobs for the
ghc2marker and investigate suspicious credential access or network traffic.
If you find evidence of compromise, assume that all secrets available to the affected workflow or host may have been exposed. Revoke affected GitHub personal access and OAuth tokens, rotate reachable secrets and cloud credentials, and review AWS OIDC trust relationships. Inspect account security events and authorized OAuth apps.
Remove malicious workflow files and hooks, but do not stop there: review commit history, including reverted changes and deleted runs. For a compromised self-hosted runner, isolate the machine and rebuild it from a trusted image. Finally, reduce workflow permissions, require review for sensitive workflow changes, and favour short-lived, narrowly scoped credentials.
Indicators of compromise
Indicator | Context |
|---|---|
193.32.204[.]199 | Reported GhostAction infrastructure; our samples used port 8080 and ?c= labels |
194.150.166[.]244:8080/exfil | wfx exfiltration endpoint; seen in roughly half the samples |
18.143.168[.]96:8080/exfil | wfx exfiltration endpoint |
153.75.82[.]159:8080/exfil | wfx exfiltration endpoint |
wfx-docs | Probe source branch |
docs/wfx-audit.md | Two-line probe file |
docs: dependency audit notes | Common probe PR title; not unique to wfx |
ci: refresh build cache, chore: add ci helper, chore: add dependency audit workflow | Observed payload commit messages |
ci: restore, ci: cleanup | Observed cleanup commit messages |
ghc2 | Cron marker in the examined reinject sample |
A Note From Ossprey
What makes this attack particularly interesting is how it turns seemingly harmless GitHub activity into large-scale credential theft, while covering its tracks along the way. Our research also uncovered a significant overlap with an earlier campaign, raising questions about where the stolen credentials came from and how attackers might be reusing access.
At Ossprey, we help developers and security teams identify malicious and risky behaviour across open source packages, repositories and pull requests, including threats that traditional scanners might miss. By looking at what code actually does, rather than relying solely on known vulnerabilities or threat lists, Ossprey helps you spot potential threats before they reach your development environment or production.
Want to see what Ossprey can uncover in your software supply chain? Try Ossprey for free

Real World Attacks

Binance - The Engineer's Laptop Is the New Front Door

Mary Welton
30 Oct 2026
Real World Attacks

Blankets, worms, and RATs: New worm targets NPM and AUR

Ellie Jevon
29 Sept 2026
Real World Attacks

How Ossprey protects from novel AI-accelerated malware development, and cross-ecosystem supply-chain attacks.

Ossprey Research Team
24 Sept 2026
