wfx-loot: Compromised GitHub Accounts Push a CI Secret-Stealer to 61,764 Repositories
Beginning 10 November 2026, a coordinated campaign dubbed wfx-loot has used 4,837 compromised GitHub accounts to inject a four-stage GitHub Actions secret stealer into 61,764 repositories. At least 1,835 repositories have confirmed payload execution. The stealer is notable for two things: it reads decrypted secrets directly out of the runner's own process memory — so secrets the workflow never asked for are in scope — and it plants long-lived persistence into AWS, Azure, and GCP by rewriting role trust policies and adding federated identities. If you run any GitHub Actions workflow in a repo an outside contributor can touch, this is your week.
TL;DR
- · Campaign name: wfx-loot (some researchers call the payload wfx-phantom). Active since 10 Nov 2026.
- · 4,837 compromised GitHub accounts → 61,764 targeted repositories → ≥1,835 confirmed payload executions.
- · Four stages: Collect (including from
Runner.Workerprocess memory), Escalate (OIDC → AWS / GCP / Azure sessions), Persist (wildcard trust policies, federated identities namedwfx-ci-*), Exfiltrate (first HTTP POST to three IPs, pivoted to Telegram API on 12 Nov). - · Leverages valid credentials — probably sourced from October's GhostAction campaign. Nothing is being exploited on GitHub's side; everything is identity abuse of unrotated secrets.
- · If you ran a CI job on an affected repo after 10 Nov 2026, treat every secret the job could touch — and every secret any job on the same runner touched — as compromised.
The shape of the campaign
wfx-loot is an identity-abuse campaign, not a vulnerability campaign. The operators appear to have a bulk pool of valid GitHub credentials — most likely from the October GhostAction leak plus smaller feeder breaches — and are using them at scale to push a secret-stealer payload into any repository the stolen account can write to. There is no GitHub CVE to patch. The exploit is your secrets, sitting unrotated in a CI workflow, on a repository that one of your contributors' stolen credentials can still modify.
The reach is unusual for a CI campaign: 4,837 unique compromised GitHub accounts have pushed malicious changes to 61,764 repositories, with telemetry confirming at least 1,835 repositories where the payload actually ran. Approximately 92% of the campaign's reconnaissance pull requests remain open, meaning most repository owners have not yet noticed the probe at all.
How the operators got repo-write access in the first place
The critical question most defenders ask on day 1 is: how did the attacker get permission to open a PR / push a branch in my repo at all? The short answer is that they did not exploit anything on GitHub's side. They logged in as a legitimate user who already had that permission. The longer answer is a credential-supply-chain story.
What a stolen GitHub identity gives an attacker
When an attacker holds a working credential for a GitHub account, they inherit every repo permission that account has — directly and transitively. For a typical developer that is more than people realise:
- Direct collaborators on personal repos — including every side project, every fork, every contract-work repo.
- Org members with a role that includes write (
write,maintain,admin) on repos they have been granted access to, either individually or through team membership. - Open-source maintainership — one compromised maintainer on a popular project can hold write on dozens or hundreds of forks, derivatives, and ecosystem-adjacent repos.
- Inherited access via nested teams — "all engineers" teams, "contractors" teams, legacy ones nobody remembers.
- Public-repo push rights via collaborator invitations that were accepted years ago and never cleaned up.
This is why the 4,837-account pool reached 61,764 repos: each stolen account is a fan-out multiplier equal to its historical access graph. The average is roughly 13 repos per compromised account; the long tail is much higher.
Where the credentials actually came from
The operators did not phish a single victim pool. The credential supply is heterogeneous:
- GhostAction leak (October 2026) — the primary feeder. A large collection of GitHub Personal Access Tokens, OAuth refresh tokens, and account session cookies went public in October. Everything that was not rotated in the following four weeks is still exploitable, and wfx-loot's actor-account list overlaps heavily with that dump.
- Infostealer malware on developer workstations. Commodity infostealers (RedLine, Vidar, Lumma, StealC, Atomic macOS) scrape the local
git-credential-managercache,~/.git-credentials, keychain entries for GitHub, browser-savedgithub.comcookies, and PATs hard-coded into dotfiles. Logs from these sell on criminal marketplaces for pennies per record; a buyer gets a reusable session or token. - PATs leaked to public repos. GitHub's own secret-scanning catches the obvious ones in near real time, but the long tail — tokens in archived
.envfiles, DockerfileARGs, screenshotted terminals, gists, Jupyter notebooks — is substantial. - PATs leaked in CI logs. Any CI run that
echos an env var, prints a fullcurlinvocation, or logs agit clone https://user:token@…URL puts the token in a build-log retention window that is often months long. - OAuth / GitHub-App token theft. If an attacker holds a token issued to a third-party app your developer authorised (IDE plugins, CI integrations, chat bots, deploy platforms, "login with GitHub" products), the app's token grants whatever scopes the user approved — including
repo, which is write access on every repo the user can see. In wfx-loot specifically, some of the stolen tokens were issued to the Fly.io GitHub App, which requests write access to contents, actions, and pull requests — exactly the three scopes the attack chain needs. See the dedicated subsection below. - Phishing for device-code / OAuth consent. The 2024–2025 era of GitHub device-code phishing has not stopped; a well-crafted "authorize this build" prompt still harvests tokens at reasonable click-through rates against engineering audiences.
- Session-cookie hijacks via XSS on developer-facing SaaS, malicious browser extensions, or clipboard poisoning on dev laptops.
- Password reuse. Classic. A credential from an unrelated breach + no MFA on the GitHub account = a working session.
The Fly.io GitHub App angle
The single most-cited concrete-source lead in the public analysis is the Fly.io GitHub App. Fly.io, like many deploy platforms, offers "connect your GitHub account" as part of its onboarding, so it can clone your repo, run deploys against your source, and post status back to pull requests. Under the hood, that connection installs the Fly.io GitHub App on the user's account and asks for permissions. The app currently requests write access to contents, actions, and pull requests — which is exactly the three permission scopes every stage of the wfx-loot attack chain needs.
GitHub Apps installed on a user account get installation tokens tied to that user. When those tokens act on the API, GitHub attributes the action to the user, not to the app. Public analysis of the campaign captured this quote on how it surfaced:
"The pull request still shows the account owner as its author. That means the attacker was using tokens that GitHub issues to the Fly.io app when a user connects their GitHub account to Fly.io, which let the app, and so the attacker, act as that user."
Three implications for defenders follow directly:
- Why 92% of recon PRs stayed open. Victim org admins saw a PR authored by a familiar account name, with a plausible title (
docs: dependency audit notes), on a familiar deploy-tooling workflow — so it did not look like an attack. - Why MFA didn't help here. GitHub App installation tokens are issued once the user authorises the app and refresh independently of the user's MFA. If the attacker holds the installation token, every MFA prompt on that user's GitHub login is irrelevant.
- Why rotating the user's password is not sufficient. Password rotation does not invalidate GitHub App installation tokens. The correct action is to uninstall the app from the affected user (or at the org level), re-authorise it if needed, and rotate the user's PATs, OAuth grants, and SSH keys in the same pass.
An important caveat the researchers call out: there is no confirmed breach of Fly.io's own infrastructure. The exact mechanism by which the Fly.io-issued tokens ended up in the operators' hands is still under investigation — plausible paths include an intermediate compromise (dev machine of a Fly.io customer, an infostealer that picked up installation tokens from a workstation session, or a credential-manager cache), not necessarily anything Fly.io did. Fly.io itself may be an upstream victim or a neutral bystander in this campaign.
The general lesson generalises past Fly.io. Any GitHub App installed on your developers' accounts with contents: write + actions: write + pull_requests: write permission is equivalent to a long-lived PAT with those scopes — against every repo the installing user can reach. Inventory them and treat them like any other high-privilege machine identity.
Why the push just worked, mechanically
With a valid session cookie, SSH key, or PAT with repo scope, the attacker's push looks like any other push the user has ever made. The specific mechanics seen in wfx-loot:
- Direct branch push. If the stolen account has push access to a non-protected branch — and most repos do not protect every branch — the operator simply pushes a new branch and opens a PR against it.
- Protected-branch bypass via PR + required-checks forgery. On repos that require status checks before merge, the payload includes a (disabled-in-samples-but-present) capability to mark required checks as passed. Defenders should assume that capability is a slider the operators can flip on later, especially where the stolen identity is also a CODEOWNER.
- PR from a feature branch, not a fork. This is the detail many threat models miss. Fork-originated PRs from untrusted contributors do not have access to workflow secrets by default — GitHub Actions runs them with a read-only
GITHUB_TOKENand no repo secrets. The wfx-loot operators do not need that. Because they are logged in as an account that already has write to the repo, they push the branch into the base repo itself, so the workflow run gets full access to secrets. The branch name (wfx-docs,chore/*, etc.) is the giveaway that the push came from a member identity, not an outside contributor. - Workflow-trigger choice. The injected workflows use
pull_request(in-repo PRs run with full secrets for members) andpush. They do not usepull_request_target, because they do not need to — they already have the member identity. - MFA bypass isn't needed for most of these paths. A stolen PAT, SSH key, or OAuth refresh token is already past the MFA gate at the point it was issued. MFA on login protects session creation; it does not retroactively protect tokens that were issued before the compromise.
What this means for your audit
When you look for wfx-loot victims in your own GitHub org, the question is not "did anyone on our team get phished?" — the question is "which of our users' long-lived GitHub-reaching credentials got picked up by the October GhostAction leak or by an infostealer in the last twelve months, and have not been rotated since?" Enumerate PATs, OAuth-authorised apps, SSH keys, and active sessions per user. Anything older than the GhostAction disclosure date needs rotation. Anything issued by a developer whose laptop was ever reported suspect needs rotation. See the 72-hour response section below for the mechanics.
How the payload is delivered
The operators probe each target with a benign pull request first — typically titled docs: dependency audit notes from a branch named wfx-docs — to confirm that the stolen account has write access. If the PR is accepted or the account can push directly, one of four delivery variants is used:
- Workflow injection. An existing
.github/workflows/*.ymlfile is modified with roughly 1,500 lines of shell under a commit titledci: refresh build cache. - npm postinstall hook. A hidden script named
.ci-[a-z0-9]{6}.shis added and wired as a postinstall step. Commit title:chore: add ci helper. - Throwaway workflow. A dedicated new workflow — commonly
.github/workflows/build-audit-[a-z0-9]{6}.yml— is dropped onto a temporary branch. Commit title:chore: add dependency audit workflow. - Legitimate build-script hijack. Code is appended to an existing script in the repo, most often
update.sh, so the stealer runs inside a trusted CI step.
Within about 60 seconds of the payload running, the operators typically revert the commit and delete the workflow run to erase the audit trail. The gap in Actions run numbers is the forensic giveaway — see detection below.
The four-stage payload
The script is roughly 1,500 lines of commented shell with feature toggles. Each instance embeds the target repository details and randomised naming, so hashes vary per victim — but the structure is consistent.
Stage 1 — Collect
The stealer harvests credentials from four distinct surfaces in parallel:
- Workflow-declared secrets via
${{ toJSON(secrets) }}and${{ toJSON(vars) }}passed into the malicious step. - Runner process memory — the novel piece. The payload reads
/proc/[pid]/memof theRunner.Workerprocess, which holds every secret the job can access in decrypted form. That means secrets other workflow steps touched — or secrets the attacker's step never declared — are still exfiltrated. The architectural assumption that "the workflow can only see what it declares" does not hold. - Credential files on the runner filesystem:
~/.aws,~/.azure,~/.ssh,~/.docker,~/.kube,~/.npm, PyPI config, HashiCorp Vault tokens, and AI-provider configs for OpenAI, Anthropic, and Gemini. - Environment harvesting — any env var matching keywords like
token,secret,aws,azure,anthropic, plus shell histories and selected process memory beyond the runner worker.
Stage 2 — Escalate
Collected credentials are immediately exchanged for cloud sessions:
- GitHub-issued OIDC tokens are exchanged for AWS, GCP, and Azure sessions without needing stored cloud credentials.
- From those sessions, the stealer chains into other AWS roles named in the repository configuration (via
sts:AssumeRole). - On GCP it impersonates service accounts via
iam.ServiceAccounts.GenerateAccessToken. - On Azure it replays cached CLI refresh tokens for post-session access.
Stage 3 — Persist
Everything so far is session-scoped. Persistence is the point: ensure long-lived access that survives rotation of the originally-stolen secrets.
- AWS: rewrite the trust policy of a reachable IAM role to
"Principal": {"AWS": "*"}, then create fresh long-lived access keys viaiam:CreateAccessKey. Sessions are namedwfx-<pid>. - Azure: add federated identity credentials named
wfx-ci-*to reachable Azure AD applications, allowing future workflow sign-in without any stored secret. - GCP: service-account keys and workload-identity-pool manipulation observed in a subset of victims.
- GitHub: capability to forge branch-protection "required checks passed" markers is present in the code but disabled in analysed samples. Likely a later-phase feature.
Stage 4 — Exfiltrate
Early campaign HTTP POSTs went to three static servers:
194.150.166.244:8080/exfil
18.143.168.96:8080/exfil
153.75.82.159:8080/exfilOn 12 November 2026 the operators pivoted exfiltration to the Telegram Bot API — api.telegram.org/sendDocument to chat -1004340163187 — which defeats IP-based egress blocklists because the destination is a legitimate Google-fronted FQDN. A comment in the latest payload reads "no callback IP anywhere in the payload".
Why the runner-memory read matters
The most important technical pivot in this campaign is step 1's use of /proc/[pid]/mem. GitHub Actions' security model assumes a workflow can only see the secrets it explicitly declares. In practice the runner process holds every secret available to the job in decrypted memory once the job starts — including ones pulled by sibling steps or action dependencies. A step with filesystem read on the runner can read that memory and lift decrypted secrets regardless of what the attacker's own step declared.
The practical consequence for defenders: a workflow that declares no secrets on the attacker-controlled step is not safe if any other step in the same job touched one. The rotation surface is the whole job, not the step.
Supply-chain amplification
Two downstream package compromises have been confirmed as of writing:
- Packagist:
digitalelvis/neuronai-studioversion3.14.3— malicious release pushed via a compromised maintainer account. - Go modules:
github.com/onuragtas/docker-envversionsv1.0.278andv1.0.280— workflow-file modification, not source modification, sogo installconsumers may not be directly affected but anyone who runs the module's CI on a fork is.
Expect more as research teams unwind the maintainer-account graph.
The GhostAction connection
wfx-loot is not using zero-days. It is consuming credentials leaked earlier — most prominently from October 2026's GhostAction incident, plus smaller feeder leaks. The overlap between GhostAction victim accounts and wfx-loot operator accounts is high. The direct lesson: a credential leaked in October was still exploitable in November because it had not been rotated. For non-human identities — PATs, deploy keys, OAuth app installs, cloud service accounts — rotation discipline is the whole defence, and this campaign is a mass-audit of who has it.
Timeline
| Date | Event |
|---|---|
| Oct 2026 | GhostAction campaign exposes a large pool of GitHub credentials; most are not rotated. |
| 10 Nov 2026 | wfx-loot activity begins. First recon PRs from wfx-docs branches; payload drops into initial batch of repositories. |
| 10–11 Nov 2026 | 4,837 compromised accounts push the payload to 61,764 repositories. |
| 12 Nov 2026 | Confirmed payload execution in at least 1,835 repositories; operators pivot exfiltration from HTTP endpoints to Telegram API. |
| 13 Nov 2026 | Supply-chain amplification confirmed on Packagist and Go modules; 92% of recon PRs still open on unaware repositories. |
Detection playbook
Searches to run against every repository you own or operate:
Repo-level signals
- Branches named
wfx-docs,chore/[a-z0-9]{8}, orci-check-[0-9]{4}. - Pull requests titled
docs: dependency audit notes. - Commit messages:
ci: refresh build cache,chore: add ci helper,chore: add dependency audit workflow. - Hidden files:
.ci-[a-z0-9]{6}.shat the repo root; new workflow files matching.github/workflows/build-audit-[a-z0-9]{6}.yml. - Appends to legitimate build scripts — in particular lines added at the end of
update.shor equivalents. - Gaps in GitHub Actions run numbers. Workflow runs are numbered sequentially per repo; a missing number between two legitimate runs means a run was deleted. This is the single most reliable forensic signal.
Payload signatures
- Shell variables set by the stealer:
WF_CALLBACK,WF_CTOKEN,WF_EXFIL_*,WF_TG_BOTS. - Function-name signatures in logs if any part of the run was captured:
memscan_files,archive_bytes,deliver_retry. - Known script SHA-256 hash (randomisation varies per victim, but the base script is consistent):
89695abb392d5cfd4972672961e644bb46baa98aa1ed7173c2a2816c762735cf.
Egress indicators
- Outbound to
194.150.166.244:8080,18.143.168.96:8080,153.75.82.159:8080from any CI runner. - Outbound
POSTtoapi.telegram.org/bot*/sendDocumentfrom CI — not normal for most teams.
Cloud-side indicators
- AWS: CloudTrail events for
UpdateAssumeRolePolicyandCreateAccessKeyin the recent window, especially trust policies containing"Principal": {"AWS": "*"}; session names matchingwfx-*. - Azure: federated identity credentials with names matching
wfx-ci-*on app registrations. - GCP: unexpected
iam.ServiceAccounts.GenerateAccessTokencalls originating from unfamiliar source IPs, especially on service accounts that historically don't get impersonated. - HashiCorp Vault: unusual access to CI-scoped tokens.
72-hour response
- Enumerate affected repos. Run the repo-level signals above across every org. The fast query is "any repo with a workflow run-number gap in the last 10 days." Prioritise repos that hold real cloud credentials.
- Rotate like you were breached. Every GitHub Actions secret, every cloud OIDC-backed role, every workflow-used PAT, deploy key, and OAuth token. Every AI-provider key stored as a secret. Every Docker Hub / npm / PyPI publishing token on affected repos. Rotation at the step level is not enough — rotate every secret that was available in the job.
- Revoke persistence. Delete Azure federated identity credentials named
wfx-ci-*. Reset any AWS IAM role trust policy containing"Principal": {"AWS": "*"}or wildcard principals. Revoke AWS access keys created during the window — searchCreateAccessKeyin CloudTrail from 10 Nov onward. Review GCP service-account keys created from 10 Nov onward. - Egress-block the known IOCs on CI runner egress and any developer VPN. Block the three exfil IPs; alert on Telegram-API POSTs from any CI runner identity.
- Lock down compromised GitHub accounts. If any user on your org appears on the actor list, revoke their PATs, OAuth grants, SSH keys, and active sessions; enforce MFA re-enrolment; audit their recent push history across all orgs they belong to, not just yours. Also uninstall any installed GitHub Apps with
contents: write/actions: write/pull_requests: writeon those accounts — including Fly.io, which has been named as a token-source in this campaign — and re-authorise them fresh only if they're needed. Password rotation does not invalidate GitHub App installation tokens. - Preserve evidence. Snapshot Actions logs, CloudTrail, Azure Activity, and GCP audit before the standard retention windows age out. If customer data was accessible via any rotated secret, loop in legal / DPO — regulatory clocks start at discovery, not at incident public disclosure.
Structural lessons
- Treat CI secrets like production secrets. Rotation cadence, allow-listed egress, least-privilege scopes, and an audit trail. Many teams still treat the Actions secret store as a lower-tier vault; this campaign forecloses that stance.
- Non-human identity inventory is not optional. You cannot rotate what you cannot enumerate. CI tokens, cloud service accounts, federated identities, publishing tokens, AI-provider keys — the inventory is the prerequisite to the rotation.
- OIDC-to-cloud without additional guardrails is a step up in blast radius, not down. Short-lived credentials are a good thing, but they still exchange into real sessions. Pin the OIDC subject claim to a specific repo + branch + workflow + env before granting a cloud role; bind roles to narrowly scoped resources.
- Runner-memory reads break the "step-level secrets" mental model. If the job needs a secret, the whole job can expose it. Split sensitive steps into their own jobs with dedicated minimal runners.
- Behavioural detection on NHIs matters. Credentials that never expire, haven't been rotated in years, sit idle, or hold far more access than they use are the fuel. Alert on idle credentials suddenly reactivating, roles accessing secrets they never previously touched, access keys appearing from new geographic sources.
How TigerGate helps
- Code Security & Secrets Scanning catches the four payload delivery patterns above in every repository you operate —
wfx-docsbranches, suspicious workflow modifications, unexpected.ci-*.shfiles, build-script appends — and flags the leaked credentials that originally fed GhostAction so they get rotated rather than reused. - Identity Security (CIEM) inventories every non-human identity — CI tokens, cloud service accounts, federated identities, publishing tokens, AI-provider keys — and alerts on the exact anomalies this campaign generates: idle credentials reactivating, roles touching secrets they never touched before, access keys appearing from new source IPs.
- CSPM catches the cloud-side persistence: AWS roles with wildcard trust policies, Azure federated identities with suspicious naming, GCP service accounts with new keys, Vault tokens issued outside policy.
- eBPF runtime detection flags CI runner processes reading from
/proc/[pid]/mem, outbound connections to the known exfil IPs, andsendDocumenttraffic toapi.telegram.orgfrom workload identities — the runtime side of the detection playbook. - VAPT AI Engine and continuous VAPT run an agentic sweep of your GitHub org for the campaign's IOCs daily, with attack-path linking so you see which compromised PAT would reach which production cloud account.
- Compliance evidence maps the rotation and persistence-removal drill to SOC 2 CC6.1 / CC7.3, ISO 27001 A.5.17 (authentication information) and A.8.24, and the NHI-specific pieces of the upcoming ISO/IEC 27110 identity-governance guidance.
Sources
- Public campaign telemetry from multiple independent security research teams, 10–13 November 2026
- GitHub Security Advisories and account-compromise notifications, 11–13 November 2026
- CVE tracking for Packagist
digitalelvis/neuronai-studioand Go modulegithub.com/onuragtas/docker-env, pending at NVD - Public IOC lists shared by independent security researchers, 12–13 November 2026
Audit your GitHub org for wfx-loot — in one connector install
14-day trial. TigerGate inventories every GitHub repo, every CI secret, every non-human identity and every cloud role across AWS, Azure, GCP, and Vault — then runs the full detection playbook above against your estate. First findings in the dashboard inside an hour.