前言
最近 GitHub 社区的一场供应链攻击闹得沸沸扬扬(禁止中国区 IP 的事,后面有机会再聊),这事儿搞得大家都紧张兮兮地检查自己的项目依赖。作为一个一直对热点安全事件保持关注的工程师,我当然也第一时间跟进了这个事件。这次的攻击手法相当精妙,攻击者展现出的耐心和技巧让人不得不佩服。
从攻击者视角来看,放弃和一线大厂硬刚,二三线中小厂更容易实现我们的目标。
从防御者视角解读,我们不需要做到绝对安全(也做不到),我们只需要比友商安全一点就够了。
事件回顾:神不知鬼不觉的渗透
故事要从2025年3月10日说起。在那个普通的工作日里,一位身份不明的攻击者悄悄地向tj-actions/changed-files这个GitHub仓库推送了恶意代码。这个代码看起来挺无害,但实际上它会将CI运行器内存中的所有凭证打包送人,直接打印到工作流日志中去了。
厉害的是,攻击者并非简单粗暴地提交代码,而是用了一种更加隐蔽的方式:他伪装成了renovate[bot](一个自动更新依赖的合法机器人账号)。然后,这个李鬼提交被添加到了真renovate[bot]创建的PR中,按照工作流配置自动合并。老实说,这招挺高明的,谁会怀疑一个机器人呢?
合并后,攻击者更新了仓库的git标签,让它们全都指向这个恶意提交。这一招够狠,因为许多项目不会指定具体的commit hash,而是用tag引用actions。于是乎,成千上万依赖这个action的项目都中招了。
好在3月14日,StepSecurity的研究人员发现了异常,迅速向tj-actions维护者报告。消息一出,GitHub社区立马行动起来,开始应急。
顺藤摸瓜:进一步发现问题
故事并没有结束。3月16日,研究员Adnan Khan在Twitter上分享了一个更加惊人的发现:这次攻击的源头可能不是tj-actions,而是另一个名为reviewdog的GitHub组织!
我花了点时间梳理这个链条,确实有点东西:
-
tj-actions/changed-files依赖tj-actions/eslint-changed-files -
后者又依赖reviewdog/action-setup -
reviewdog/action-setup被打,攻击者获得了tj-actions的访问权限 -
攻击者利用这个权限在tj-actions/changed-files中植入后门
简直就是一个套娃,通过一层层依赖关系最终达到目标。溯源过程中,研究员还发现攻击者使用了一些技巧来隐藏痕迹。
GitHub Forks的攻击面
这里有个很有意思的点:攻击者利用了GitHub Forks的一个鲜为人知的特性。当你fork一个仓库后,你可以在fork中添加提交,这些提交会成为"fork网络"的一部分,并可以从原始仓库引用。
当你浏览这些 commit 时,它们会显示在原始仓库中,但有个commit 的提示。更绝的是,如果fork被删除且没人记录确切的commit SHA,这些 commit 基本上就无法追踪了。
研究员发现,2025年3月11日,一个名为iLrmKCu86tjwp8的用户fork了reviewdog/action-setup,推送了13个包含各种有效payload的提交,然后该用户账号就从GitHub上消失了。这位幽灵用户同一天还fork了reviewdog/action-typos,推送了另外15个恶意提交。
不禁惊叹攻击者的精心策划:他们使用了自已删除fork的影子commit,其次,他们推送git标签而非普通提交,因为在GitHub中,标签变更不会记录在审计日志中!这简直是完美犯罪的教科书案例。
消失的用户们
调查过程中,研究人员还发现了另外两个可疑用户:2ft2dKo28UazTZ和mmvojwip。这两个账号也已从GitHub消失。观察2ft2dKo28UazTZ的行为,发现他在测试git标签的创建和删除,特别是v39和v47标签。
实际上,这些账号的创建和消失都遵循一个有趣的模式:先用合法邮箱注册,完成操作后,将邮箱改为违反GitHub政策的一次性、匿名邮箱,导致账号被自动隐藏。这种操作让我想起了那些包含自毁程序的间谍信息。
真正的目标:Coinbase
调查的转折点来了!当研究人员查看这些可疑用户的活动时,发现了它们与Coinbase的关联:
-
2025-03-12:2ft2dKo28UazTZ fork了多个Coinbase仓库 -
2025-03-13:mmvojwip也fork了coinbase/agentkit
看到这里,我不禁猜测:Coinbase可能是这次攻击的真正目标。进一步调查显示,攻击者检查coinbase/agentkit的工作流,发现它引用了tj-actions/changed-files的v39标签,正是攻击者此前测试过的标签之一。
更决定性的证据是一个特定的恶意提交,其中包含了明确针对Coinbase的代码:
研究人员在coinbase/agentkit的工作流日志中找到了证据,确认该工作流确实拉取并执行了恶意代码:
这个故事还是挺曲折的:攻击者先针对性攻击Coinbase,然后在接近成功后(没成),可能是为了掩盖踪迹或者扩大影响,误伤到了所有使用tj-actions/changed-files的项目。
攻击的惊人规模
为了直观展示这次攻击的潜在影响,研究人员绘制了一个dependency tree,以reviewdog/action-setup为核心:
感受一下这张图的影响范围:
-
第0级:直接依赖reviewdog/action-setup的有3047个仓库 -
第1级:直接依赖那些依赖reviewdog/action-setup的action有4941个仓库 -
第2级:70538个仓库 -
第3级:159986个仓库
而且这还只是保守估计,因为图表只包含公共仓库,且受搜索限制影响结果不完整。实际受影响的项目很可能远超这个数字。
4月2日更新:深挖攻击源头
事情还有续集,4月2日,研究人员又发布了新的调查结果,揭示了此前未知的攻击手段。
这次攻击的源头可以追溯到2024年11月,攻击者首先攻击了SpotBugs(一个用于Java代码静态分析的开源工具)的GitHub Actions工作流,获得初始访问权限,然后横向移动到reviewdog。
我简单梳理了一下这个复杂的攻击链:
-
2024年11月28日:SpotBugs维护者在spotbugs/sonar-findbugs工作流中添加了自己的PAT(令牌) -
2024年12月6日:攻击者提交恶意PR,利用pull_request_target触发器窃取了这个PAT -
2025年3月11日:攻击者用这个PAT邀请一个名为jurkaofavak的恶意用户加入spotbugs/spotbugs -
jurkaofavak推送恶意工作流到spotbugs/spotbugs,这个工作流泄露了reviewdog维护者的PAT -
攻击者用这个PAT修改reviewdog/action-setup的标签 -
从这里开始触发之前描述的整个链条
这个攻击流程图看着就让人头皮发麻:
保护自己
分析完整个事件后,从开发者的视角出发,我们该如何保护自己?
一些实用建议:
-
固定依赖版本到具体commit hash:永远不要仅仅使用tag或branch引用GitHub Actions -
review 工作流权限:给予GitHub Actions最小必要权限,特别是对于敏感操作 -
定期审计依赖:定期检查和更新第三方依赖,尤其是CI/CD相关的组件 -
警惕pull_request_target:这个触发器很方便但也很危险,使用时需格外小心
个人感受:我现在回去检查自己的项目,真发现了一个用了tag来引用actions的项目,立马改成了commit hash。说实话,这次事件对我的警醒很大,以前总觉得不会这么巧就被攻击,现在看来所有成功的攻击都是因为“巧合”。
思考和启示
从技术角度看,这次攻击是教科书级别的供应链攻击案例。攻击者展现出的耐心、技术能力和对GitHub工作机制的深入理解确实令人佩服。
从时间线来看,攻击者从2024年12月就开始准备,直到2025年3月才发动主要攻击,这种长期潜伏的策略增加了检测难度。
值得一提的是,在这个开源盛行的时代,依赖链的复杂程度远超我们的想象。一个看似无害的action可能牵连数十万个项目。如何平衡安全和便利性是一个业界难题。
(完整的事件分析可以查看Palo Alto Networks的原始报告)
原文始发于微信公众号(RedTeam):针对 Coinbase 的供应链攻击
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论