# wfx-loot: GitHub Actions CI secret stealer pushed to public repositories using stolen GitHub credentials

> A mass wave (precursor Oct 24-29, 2026; main wave from Nov 10, still running Nov 12) used stolen GitHub credentials to push the wfx-loot stealer into public repositories via new workflows, modified workflows, npm lifecycle hooks or appended shell scripts. It exfiltrates repository/org secrets, GITHUB_TOKEN, AI provider keys and OIDC-minted cloud credentials over Telegram and HTTP C2. GitGuardian's dataset holds 28,500 payload patches from 2,367 accounts across 17,715 repositories.

- **Published:** 2026-11-13T17:40:43Z
- **Last reviewed:** 2026-11-13T17:40:43Z
- **Canonical:** https://intel.threadlinqs.com/threat/TL-2026-5982
- **Card image:** https://intel.threadlinqs.com/og/threat/TL-2026-5982.png
- **ID:** TL-2026-5982
- **Severity:** HIGH
- **Category:** SUPPLY_CHAIN
- **Status:** ACTIVE
- **Detections:** 9 (1 shown free below) · **IOCs:** 17 (3 shown free below)

## Description

wfx-loot is a CI secret stealer shipped in four wrappers: (1) a throwaway workflow .github/workflows/build-audit-xxxxxx.yml on a chore/xxxxxxxx branch requesting id-token: write and status: write (the most common form); (2) an added 'Restore cache xxxx' step in an existing default-branch workflow, after a 'Pass runtime' step; (3) an npm lifecycle hook, consisting of a hidden .ci-xxxxxx.sh script at the repository root plus an edited package.json (usually postinstall, or prepended to an existing script such as build); and (4) the payload appended to an existing shell script (start.sh, deploy.sh) that a workflow already calls. A probe pull request titled 'docs: dependency audit notes' from branch wfx-docs adding docs/wfx-audit.md (no payload) was opened in large numbers (66,001 found by GitHub search on Nov 12). 28,148 'ci: refresh build cache' commits landed on default branches between Nov 10 and Nov 12.

The campaign ran in two phases. A small v1 precursor ran Oct 24-29, 2026 (80 payload patches, 7 accounts, 12 repositories, all four delivery forms; first npm-hook commit Oct 25 21:56 UTC). After a pause Oct 30 - Nov 9, the mass wave began Nov 10 12:45 UTC with v2a, replaced by v2b at 16:35. A second C2 IP first appeared Nov 11 05:02. Hook commits peaked at 838 per three hours on Nov 11 (09:00-12:00 UTC) and 1,210 per three hours on Nov 12 (06:00-09:00 UTC). On Nov 12, v3 added Telegram delivery (01:07) and then switched to Telegram-only, with the HTTP callback pointed at a dead address (01:48).

GitHub's approval hold stopped the workflow-file forms: all 7 visible November payload runs were held with conclusion action_required, although the actor deleted most runs (37 of 65 November injection commits show run-number gaps). The npm hook form changes no workflow file, so existing trusted workflows trigger it on npm install / npm ci / pnpm install. Of 30 sampled repositories that ran jobs on hook commits, none was blocked; public logs from 19 repositories showed execution and 18 reported successful exfiltration. The hook also runs on developer machines. The payload exits 42 only if every exfiltration channel fails, which breaks the install or build.

The stealer collects all repository and organization secrets and variables (toJSON(secrets), toJSON(vars)), the job GITHUB_TOKEN, step environment, /proc/*/environ and /proc/*/cmdline, environment variables matching about 90 AI-provider prefixes, a regex memory scan for eight token families (JWT, GitHub, GitLab glpat-, AWS AKIA, Anthropic sk-ant-, OpenAI sk-proj-/sk-svcacct-, Hugging Face hf_), 90+ credential file paths plus a home-directory search (cloud, container, Git, package registry, database, SSH, GPG, AI coding tools), and an archive of up to 60 files matching 92 secret patterns, the last 100 commits as JSON and artifacts of the last 10 runs. On self-hosted runners it also takes cloud metadata credentials, docker inspect output, the runner .credentials and RSA key, _diag logs, systemd units and persistent work-folder secrets. When id-token: write is present it mints OIDC tokens for AWS, Azure and GCP audiences and abuses them: AWS role exchange, chained AssumeRole with session tags, IAM user listing, iam:CreateAccessKey for up to two users and attempted trust-policy rewrites; GCP ADC/token conversion, project listing and service-account impersonation via generateAccessToken and signJwt; Azure ARM tokens, subscription and Key Vault listing, Azure CLI refresh-token replay to Microsoft Graph and addition of federated identity credentials; plus Vault JWT login and Kubernetes API secret reads.

Exfiltration channels: Telegram sendDocument (5 bot tokens tried in turn; v3 only, primary from Nov 12), HTTP POST to WF_CALLBACK/WF_CALLBACK2 (v1-v3), and in-GitHub channels (Actions cache, step summary base64 between <!--WFX-BEGIN--> markers, artifact wfx-loot-<run id>-<job>-<pid>) gated on WF_PRIVATE=1 and therefore unused on public repositories; encrypted commit to ci-* branch (WF_FALLBACK=0) and base32 run-log exfil (WF_EXFIL_LOG=0) were off in samples.

Attribution: the source names no actor. About 431 accounts (roughly 20%) were already compromised in the earlier GhostAction wave, and in several repositories the GhostAction security-audit.yml workflow still ran on wfx commits, but GitGuardian notes credentials are traded and reused so overlap does not prove a single actor. StepSecurity is cited as linking some stolen tokens to the Fly.io GitHub App.

## MITRE ATT&CK

- T1078 Valid Accounts
- T1195.002 Compromise Software Supply Chain
- T1059.004 Unix Shell
- T1546 Event Triggered Execution
- T1098.001 Additional Cloud Credentials
- T1564 Hide Artifacts
- T1528 Steal Application Access Token
- T1552.001 Credentials In Files
- T1552.004 Private Keys
- T1552.005 Cloud Instance Metadata API
- T1003.007 Proc Filesystem
- T1119 Automated Collection
- T1213.003 Code Repositories
- T1071.001 Web Protocols
- T1567 Exfiltration Over Web Service

## Free detection (Splunk SPL, Microsoft KQL, Sigma)

**wfx-loot CI Payload Push - Throwaway Workflow, Hidden Hook Script, Probe PR Branch and Decoy Commit Messages (Exploitation)** (`TL-2026-5982-SPL-001`)

Detects the push of wfx-loot CI secret stealer content into a repository by an account using stolen GitHub credentials: a .github/workflows/build-audit-xxxxxx.yml workflow, a hidden .ci-xxxxxx.sh script at the repository root, the docs/wfx-audit.md probe file or wfx-docs branch, a chore/xxxxxxxx branch, and the campaign's decoy commit messages ('ci: refresh build cache', 'ci: restore', 'chore: add ci helper'). A weighted score keeps single weak signals (a common message alone) from alerting. Also available as KQL (GitHub audit log) and Sigma.

- **Languages:** Splunk SPL, Microsoft KQL, Sigma
- **ATT&CK:** T1195.002, T1078, T1564
- **Data sources:** github:webhook:push (Splunk index=github), GitHubAuditLogPolling_CL, github audit log
- **Severity:** critical
- **Confidence:** high

### Splunk SPL

```spl
index=github sourcetype="github:webhook:push" earliest=-24h
| eval added=mvappend('head_commit.added{}','commits{}.added{}'), msg=lower('head_commit.message')
| eval wf_form=if(mvcount(mvfilter(match(added,"^[.]github/workflows/build-audit-[a-z0-9]{6}[.]yml$")))>0,1,0)
| eval hook_form=if(mvcount(mvfilter(match(added,"^[.]ci-[a-z0-9]{6}[.]sh$")))>0,1,0)
| eval probe_form=if(mvcount(mvfilter(match(added,"^docs/wfx-audit[.]md$")))>0 OR ref="refs/heads/wfx-docs",1,0)
| eval branch_form=if(match(ref,"^refs/heads/chore/[a-z0-9]{8}$"),1,0)
| eval msg_form=if(match(msg,"^(ci: refresh build cache|ci: restore|ci: cleanup|chore: add ci helper|chore: add dependency audit workflow|ci: cache)$"),1,0)
| eval score=wf_form*2+hook_form*2+probe_form*2+branch_form+msg_form
| where score>=2
| stats count min(_time) as first_seen max(_time) as last_seen values(added) as added_files values(msg) as commit_messages values(ref) as refs max(score) as max_score by repository.full_name, pusher.name, sender.login
| convert ctime(first_seen) ctime(last_seen)
```

### Microsoft KQL

```kql
GitHubAuditLogPolling_CL
| where TimeGenerated > ago(24h)
| where action_s == "workflows.created_workflow_run"
| where head_branch_s matches regex @"^chore/[a-z0-9]{8}$"
    or name_s matches regex @"^Dependency Audit [a-z0-9]{4}$"
    or head_branch_s == "wfx-docs"
| project TimeGenerated, actor_s, org_s, repo_s, name_s, head_branch_s, event_s, workflow_run_id_d
| order by TimeGenerated desc
```

### Sigma

```yaml
title: wfx-loot Throwaway Dependency Audit Workflow Run on chore Branch
id: 5c1e7a52-3b0d-4f0a-9a43-6d1f2b8e0a11
status: experimental
description: Detects GitHub Actions workflow runs created by the wfx-loot CI secret stealer - a throwaway workflow named 'Dependency Audit xxxx' (file build-audit-xxxxxx.yml) running from a chore/xxxxxxxx branch, or the wfx-docs probe branch.
references:
    - https://blog.gitguardian.com/wfx-loot-a-ci-secret-stealer-pushed-with-stolen-github-credentials/
author: Threadlinqs Team
date: 2026/11/13
tags:
    - attack.initial_access
    - attack.t1195.002
    - attack.t1078
    - attack.defense_evasion
    - attack.t1564
logsource:
    product: github
    service: audit
detection:
    selection_action:
        action: 'workflows.created_workflow_run'
    selection_name:
        name|re: '^Dependency Audit [a-z0-9]{4}$'
    selection_branch:
        head_branch|re: '^chore/[a-z0-9]{8}$'
    selection_probe:
        head_branch: 'wfx-docs'
    condition: selection_action and (selection_name or selection_branch or selection_probe)
falsepositives:
    - Legitimate bots that name branches chore/xxxxxxxx and run a workflow titled Dependency Audit
level: high

```

**Known false positives:**

- Developers legitimately adding a .ci-*.sh helper script or a workflow named build-audit-*.yml
- Automation bots that commit 'ci: cache' or 'ci: cleanup' to chore/* branches

**Tuning notes:** Raise score threshold to 3 if the decoy-message and branch signals alone are noisy; allow-list known automation accounts via sender.login. Webhook field names assume the standard GitHub push payload ingested as JSON; adjust to your HEC mapping. For the KQL form confirm the GitHubAuditLogPolling_CL column names (head_branch_s, name_s) in your workspace.

### 8 more detection rules (Splunk SPL, Microsoft KQL, Sigma), unlock with TL Creds

- wfx-loot npm Lifecycle Hook Executing Hidden .ci-xxxxxx.sh Payload (Exploitation) (locked: unlock with TL Creds)
- wfx-loot Azure Federated Identity Credential wfx-ci-* Added for Persistence (Post-Exploitation) (locked: unlock with TL Creds)
- wfx-loot Telegram Bot sendDocument Exfiltration from CI Hosts (C2 / Exfiltration) (locked: unlock with TL Creds)
- wfx-loot Hidden Payload Script and Loot Staging Artifacts on Disk (Exploitation) (locked: unlock with TL Creds)
- wfx-loot CI Runner Credential Harvesting - /proc Environ, Metadata Credentials and Runner Key (Post-Exploitation) (locked: unlock with TL Creds)
- wfx-loot GitHub OIDC Workload Identity Impersonating GCP Service Accounts (Cloud Lateral Movement) (locked: unlock with TL Creds)
- wfx-loot GitHub OIDC AWS Abuse - wfx-* Sessions, CreateAccessKey and Trust Policy Rewrite (Post-Exploitation) (locked: unlock with TL Creds)
- wfx-loot HTTP C2 Exfiltration Server Connections - v1 to v3 Callback IPs (C2 / Exfiltration) (locked: unlock with TL Creds)

Sign up free to unlock the detection rules: https://threadlinqs.com/?modal=signup

## Sample indicators (3 of 17)

Defanged, TLP:CLEAR.

- IP address: `45.61.182[.]54`
- Domain: `api[.]telegram[.]org`
- IP address: `18.143.168[.]96`

Full set of 17 indicators unlocks with TL Creds: https://threadlinqs.com/tl-creds

## Sources

- [wfx-loot: a CI secret stealer pushed with stolen GitHub credentials (GitGuardian, Gaetan Ferry)](https://blog.gitguardian.com/wfx-loot-a-ci-secret-stealer-pushed-with-stolen-github-credentials/)
- [GhostAction campaign: 3,325 secrets stolen (GitGuardian)](https://blog.gitguardian.com/ghostaction-campaign-3-325-secrets-stolen/)
- [GhostAction Returns (StepSecurity)](https://www.stepsecurity.io/blog/ghostaction-returns)
- [GhostAction campaign: over 3,000 secrets stolen through malicious GitHub workflows (StepSecurity)](https://www.stepsecurity.io/blog/ghostaction-campaign-over-3-000-secrets-stolen-through-malicious-github-workflows)
- [New GhostAction Wave Hits Hundreds of Repos, Expanding Beyond CI/CD Secrets to Cloud Credentials (Socket)](https://www.socket.dev/blog/ghostaction-cloud-credentials)
- [GhostAction Hacking Wave Fizzles, But Attackers Lurk on in GitHub (Dark Reading)](https://darkreading.com/application-security/ghostaction-hacking-wave-fizzles-attackers-github)
- [Wiz Threat Center: GhostAction campaign](https://threats.wiz.io/all-incidents/ghostaction-campaign)

## Full data

The other 8 detection rules (Splunk SPL / Microsoft KQL / Sigma) unlock with TL Creds; a free account includes daily creds: https://threadlinqs.com/?modal=signup
Indicator values unlock with TL Creds, at a lower price on Red and Purple; AI agents can query rules and IOCs through the Threadlinqs MCP server (Purple): https://intel.threadlinqs.com/mcp

Canonical: https://intel.threadlinqs.com/threat/TL-2026-5982
