BlogThreat Research

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.

12 min readBy TigerGate Research, Security Research TeamPublished Updated

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.Worker process memory), Escalate (OIDC → AWS / GCP / Azure sessions), Persist (wildcard trust policies, federated identities named wfx-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:

  1. 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.
  2. Infostealer malware on developer workstations. Commodity infostealers (RedLine, Vidar, Lumma, StealC, Atomic macOS) scrape the local git-credential-manager cache, ~/.git-credentials, keychain entries for GitHub, browser-saved github.com cookies, 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.
  3. 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 .env files, Dockerfile ARGs, screenshotted terminals, gists, Jupyter notebooks — is substantial.
  4. PATs leaked in CI logs. Any CI run that echos an env var, prints a full curl invocation, or logs a git clone https://user:token@… URL puts the token in a build-log retention window that is often months long.
  5. 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.
  6. 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.
  7. Session-cookie hijacks via XSS on developer-facing SaaS, malicious browser extensions, or clipboard poisoning on dev laptops.
  8. 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_TOKEN and 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) and push. They do not use pull_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:

  1. Workflow injection. An existing .github/workflows/*.yml file is modified with roughly 1,500 lines of shell under a commit titled ci: refresh build cache.
  2. npm postinstall hook. A hidden script named .ci-[a-z0-9]{6}.sh is added and wired as a postinstall step. Commit title: chore: add ci helper.
  3. 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.
  4. 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]/mem of the Runner.Worker process, 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 via iam:CreateAccessKey. Sessions are named wfx-<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/exfil

On 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-studio version 3.14.3 — malicious release pushed via a compromised maintainer account.
  • Go modules: github.com/onuragtas/docker-env versions v1.0.278 and v1.0.280 — workflow-file modification, not source modification, so go install consumers 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

DateEvent
Oct 2026GhostAction campaign exposes a large pool of GitHub credentials; most are not rotated.
10 Nov 2026wfx-loot activity begins. First recon PRs from wfx-docs branches; payload drops into initial batch of repositories.
10–11 Nov 20264,837 compromised accounts push the payload to 61,764 repositories.
12 Nov 2026Confirmed payload execution in at least 1,835 repositories; operators pivot exfiltration from HTTP endpoints to Telegram API.
13 Nov 2026Supply-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}, or ci-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}.sh at 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.sh or 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:8080 from any CI runner.
  • Outbound POST to api.telegram.org/bot*/sendDocument from CI — not normal for most teams.

Cloud-side indicators

  • AWS: CloudTrail events for UpdateAssumeRolePolicy and CreateAccessKey in the recent window, especially trust policies containing "Principal": {"AWS": "*"}; session names matching wfx-*.
  • Azure: federated identity credentials with names matching wfx-ci-* on app registrations.
  • GCP: unexpected iam.ServiceAccounts.GenerateAccessToken calls 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

  1. 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.
  2. 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.
  3. 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 — search CreateAccessKey in CloudTrail from 10 Nov onward. Review GCP service-account keys created from 10 Nov onward.
  4. 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.
  5. 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: write on 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.
  6. 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-docs branches, suspicious workflow modifications, unexpected .ci-*.sh files, 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, and sendDocument traffic to api.telegram.org from 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-studio and Go module github.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.