黑客劫持 4837 个 GitHub 账号,6.1 万+ 仓库成盗窃目标

导语:几千个仓库的 PR 列表里混着一条叫 “docs: dependency audit notes” 的审计请求,几乎没人多看一眼——92% 至今还挂在那儿。可它不是审计,是投毒者的敲门砖:一句话试探”我能写进你这个仓库吗”,问遍半个 GitHub,被回应的地方就被塞进 1500 行偷密钥的脚本,一分钟后连提交带日志一起抹掉。


探针:一条”文档审计”PR 试出谁能写你的仓库

Aikido Security 给这次行动起名 wfx-loot,来源是攻击者自己给恶意文件和”战利品”的命名习惯。11 月 10 日 12:45 UTC 起,攻击者动用 4,837 个已被拿下的账号。

打法第一步克制得近乎礼貌:用其中一个账号,在 wfx-docs 分支上开一个标题为 docs: dependency audit notes 的 PR,内容是个小小的笔记文件。这一个 PR 只做一件事——测试 token 能不能往目标仓库里写。

GitHub 搜索能查到这个标题的 PR 有 61,839 个,散布在 61,764 个仓库。其中 10,431 个 PR 提交到了账号本人并不拥有的仓库,靠的是协作者或组织成员身份。92% 的探针 PR 至今还是打开状态,多数仓库主人根本不知道有人来过。

从探针 PR 到 CI 投毒再到密钥外传的攻击链

投毒与抹痕:整个过程不超过一分钟

探针通过,同一账号才开始动手。三种投放方式:

  • ci: refresh build cache——往已有 workflow 里塞约 1500 行,命中 7,309 个仓库;
  • chore: add ci helper——往 package.json 里塞 postinstall 钩子和 .ci-xxxxxx.sh,命中 771 个仓库。这种方式会在开发者本机 npm install 时执行;
  • chore: add dependency audit workflow——往 chore/xxxxxxxx 临时分支扔一个全新 workflow 文件,命中 16 个仓库。

然后是最狠的一步:回滚提交,同时删掉 Actions 的 run 记录和日志。

所以查仓库时别指望找到残留文件。SafeDep 说得清楚:找不到 wfx 文件、wfx- 缓存条目或 WFX-BEGIN 步骤摘要,不代表数据没被偷走。真正该看的是——攻击者那个提交的 run 编号是空的。run 被删了,说明它跑过。

那个 1500 行脚本拿走了什么

三条路径跑的是同一个约 1500 行的 shell 脚本。Aikido 特意点了一句:脚本注释极其详尽,一点没混淆,日志和变量名里都自报家门 wfx。收集面铺得极广:

  • GitHub Actions secrets 和 variables、GITHUB_TOKEN、OIDC token、Actions runtime token;
  • 运行器完整环境变量,外加按 token、secret、pass、cred、aws、azure、gcp、openai、anthropic、stripe 等关键词过滤出来的一份;
  • AWS、Azure、GCP、Docker、K8s、npm、PyPI、Git、SSH、GPG、Vault、Terraform 的凭据文件路径;
  • AI 工具凭据:OpenAI、Anthropic/Claude、Gemini、HuggingFace、Cursor、Copilot、Codex、Continue、Codeium、Aider、Weights & Biases、Ollama;
  • bash、zsh、mysql、psql、python、node 的 shell 历史;$HOME 下所有 .env、.pem、.p12、.jks、.kdbx、kubeconfig,递归搜;
  • 运行中进程的环境变量和内存,重点是 Runner.Worker 与 Runner.Listener,从里面扒 JWT、GitHub PAT、AWS 密钥和 AI 令牌;
  • 自托管 runner 额外中招:云实例元数据凭据、Docker 容器状态、runner 的 RSA 参数、systemd 单元,还有仓库快照、最近 100 次提交和 workflow 制品。

升级与留后门:密钥只是起点

云上横向是标准套路:把 OIDC token 换成 AWS、GCP、Azure 会话。AWS 侧顺着仓库里发现的 IAM role 链下去,调 iam:CreateAccessKey 给 IAM 用户造长期密钥。GCP 侧用纯 Python RSA 实现签 JWT 换 access token,再 list 服务账号、generateAccessToken、signJwt 去冒充。Azure 侧拿 OIDC 或 client secret 换 management token,列订阅和 Key Vault,还把缓存的 Azure CLI FOCI refresh token 重放到微软一方客户端 ID 上,换 Graph、ARM、Key Vault 令牌。

留后门三手,每一手都冲着”密钥轮换也拦不住”:

  • 伪造状态检查。在收集开始前,用仓库 required checks 的 context 名给提交 SHA 灌假成功。分支保护只认 context 名、不认是哪个应用发的,于是门被撬开了,而脚本还在跑。
  • 改 AWS 信任策略。把链式 role 的信任策略改成 "Principal": {"AWS": "*"},等于对全网开放。
  • 注入 Azure 联邦身份。拿 Graph 令牌给 Entra 应用注册新增名为 wfx-ci-… 的 federated identity credential,指向被污染的仓库。之后这个仓库里任何 workflow 都能免密登 Azure。

数据是怎么运出去的

打包成 gzip tarball,渠道备了七八种:Actions Cache API、job summary(小于 700KB 就 base64 塞进 标记之间)、制品上传(命名 wfx-loot-)、直连回调的 HTTP POST、备用回调地址、AES 加密后提交进仓库、base32 写进 run 日志,以及 Telegram Bot。

这次战役实际只开了 HTTP POST 一条路,目标 194.150.166[.]244:8080/exfil 和 18.143.168[.]96:8080/exfil。SafeDep 采样 200 个恶意提交,约一半用第一个服务器,五分之一只用 Telegram。用 Actions 缓存和 summary 藏数据需要 WF_PRIVATE=1,这次关着。

三个已经流出去的包

  • Packagist 的 digitalelvis/neuronai-studio 3.14.3。composer require 不触发,但项目里 npm install 走到 vendor/digitalelvis/neuronai-studio 就会跑 postinstall 钩子。Pin 到 3.14.2 或更早,Packagist 仍把 3.14.3 当最新版挂着。
  • Go 模块 github.com/vkuznet/gocurl 的 v0.0.0-20261111095532-8d5aad0cb0da。payload 只在工作流文件里,Go 工具链不跑 workflow,但攻击者唯独没回滚这个提交。
  • github.com/onuragtas/redock(原 docker-env)1.0.278、1.0.280、1.0.282 中毒,1.0.279、1.0.281、1.0.283 干净。

另有 28 个 WebdriverIO 仓库以 wdio-bot 身份被投毒,没有包发布,但跑过 npm install 的人一样中招。

和 10 月 GhostActions 的关系

两波攻击的账号高度重合:10 月被 GhostActions 打过的人,这次中招概率明显高于基线;100 多个仓库里,wfx-loot 直接塞进了 GhostActions 的工作流文件。但 Aikido 判断这是两个不同团伙——基础设施、工具和手法都不一样,被偷的凭据在黑市里往往流向好几个人。

GhostActions 那边的特征顺手记一下:伪造的 secret 扫描工作流把密钥发往 193.32.204[.]199;public/fonts/fa-solid-400.woff2 里藏的其实是 JavaScript,配一个隐藏的 VS Code task,打开文件夹就执行,再挂一个 #ghc2 标签的 cron 任务去轮询该地址。

排查与堵法

IOC 直接抄:

  • 分支:wfx-docs、chore/ 加八位随机字符、ci-check- 加四位数字;
  • PR 标题:docs: dependency audit notes;
  • 提交信息:ci: refresh build cache、ci: restore、chore: add ci helper、ci: cleanup、chore: add dependency audit workflow;
  • 脚本 SHA256:89695abb392d5cfd4972672961e644bb46baa98aa1ed7173c2a2816c762735cf;
  • 网络:194.150.166[.]244:8080、18.143.168[.]96:8080、153.75.82[.]159:8080;
  • 制品名前缀 wfx-loot-、缓存条目前缀 wfx-、日志里的 <!--WFX-BEGIN-->。

排查顺序:先在 GitHub 搜 docs: dependency audit notes,命中的仓库就是被探测过的;再看被污染提交上 run 编号是否缺失——比找文件残留靠谱得多。

组织层面的防线还是那几条:全员强制 2FA,禁用 classic PAT,fine-grained token 要管理员审批加过期时间。改密码不够用——攻击期间新增的任何 PAT、SSH key、deploy key 在被找到并吊销前一直有效,而这个排查纯靠手工。

结语

这次的教训不是”CI 别放密钥”,密钥该放还得放。是你的 CI 里跑着谁的代码、那些代码能读到什么、读完能送到哪——这条链上任何一环松动,六万个仓库的密钥就一起走了。


参考来源:Aikido Security、SafeDep 原始技术报告,The Hacker News、SecurityWeek 跟进报道。

版权声明:本文由华盟网原创发布,保留所有权利。配图由华盟网授权使用。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容