NPM 蠕虫卷土重来且变本加厉:TanStack 被黑深度解析

BBetter Stack
컴퓨터/소프트웨어경영/리더십AI/미래기술

Transcript

00:00:00《沙丘》里的沙虫又回来搞第四次续集了。
00:00:02这次它的目标是像 TanStack 这样的软件包,
00:00:04就在我发布那期关于 Next.js 视频的几小时后,
00:00:07我这时间选得真是太“巧妙”了。
00:00:08这是一次大规模的 NPM 供应链攻击,
00:00:11影响范围远不止 TanStack。
00:00:13它还波及了像 UiPath、Mistral
00:00:15以及其他 160 多个软件包,
00:00:17甚至包括像 Guardrails.ai 这样的 PyPy 软件包。
00:00:20更绝的是,
00:00:22它还内置了一个“死亡开关”,
00:00:24一旦它检测到你更换了被盗的密钥,
00:00:26它就会清空你的整台电脑,
00:00:28甚至还自带了一些全球政治倾向。
00:00:30让我们深入了解一下。
00:00:36对于这篇续集,“蠕虫”的目标依然不变,
00:00:39窃取开发者机器和 CI/CD 运行环境中的凭证,
00:00:42然后利用这些凭证去感染更多的包。
00:00:44对于 TanStack 来说,这意味着在几分钟内,
00:00:47它发布了 84 个恶意版本,涉及 42 个 TanStack 软件包。
00:00:51现在,我将讲解它们最初是如何
00:00:52感染 TanStack 的,
00:00:54但首先,让我们看看如果你安装了其中一个受感染的包,
00:00:56恶意软件到底会做什么。
00:00:58在这些恶意包里,
00:00:59你会发现一个名为 routerinit.js 的新文件,
00:01:02以及一个注入的可选依赖项,
00:01:04它指向的看起来像是一个
00:01:05合法的 TanStack 路由器的 GitHub 链接,
00:01:08但实际上是攻击者 Fork 分支上的一个孤儿提交。
00:01:10这就是 GitHub 处理 Fork 链接的方式,
00:01:13所以 URL 看起来
00:01:14就像属于原始项目一样,
00:01:16尽管该提交实际上来自一个 Fork 分支。
00:01:18在这个 Fork 中,有一个生命周期脚本,
00:01:20prepare,它会运行 bun run task runner JS,
00:01:22并在结尾处执行 exit 1。
00:01:24这只是让可选依赖项在有效载荷运行后失败的一种巧妙方法,
00:01:27这样安装过程就能像往常一样继续,
00:01:28并且在你的安装日志中留下的痕迹更少。
00:01:30此外,你可能已经注意到这不是在运行
00:01:33我刚才提到的那个注入到包里的 routerinit.js 文件,
00:01:35但目前,你就把这两个文件
00:01:37看作是发挥同样作用、只是名称不同而已。
00:01:38简而言之,当你安装这个时,
00:01:40它就会运行这个脚本。
00:01:42脚本首先尝试做的是
00:01:44将自身与显眼的安装流程脱钩,
00:01:46所以它会检查自己是否已经在后台运行,
00:01:47如果没有,它就 fork 出一个自身的独立副本,
00:01:50并干净利落地退出父脚本。
00:01:51这样一来,你的 npm 安装日志
00:01:53就不会显示该脚本的任何输出,
00:01:54因为恶意软件已经从该进程中脱离并在后台运行。
00:01:57此后,它做了一件非常聪明的事情。
00:01:58它将自身副本写入
00:02:00你的 Claude Code 钩子目录,
00:02:01然后配置你的 Claude 设置,
00:02:04以便在你在该项目中使用 Claude Code 时运行此钩子。
00:02:06这样,它就能在原始安装之后继续存在,
00:02:07并且每次你打开该项目的 Claude Code 时,
00:02:08它就会持续触发。
00:02:10它实际上对 VS Code 的任务运行器也做了同样的操作,
00:02:12在里面复制自己,
00:02:13所以如果你使用 VS Code 的工作区自动运行功能,
00:02:16你就会面临同样的问题。
00:02:17它甚至建立了一个系统级的服务,
00:02:20名为 GitHub Token Monitor,
00:02:22但我们稍后再回过头来说这个,
00:02:23因为它简直太卑劣了。
00:02:26而且你还没订阅也很卑劣。
00:02:28有效载荷接下来的工作是
00:02:29名为“GitHub Token Monitor”的系统级服务,
00:02:31而且它什么都试。
00:02:32在 GitHub Actions 中,它会寻找凭证
00:02:34和运行环境中的密钥。
00:02:35更具体地说,它会抓取 GitHub Actions
00:02:37运行器工作进程的内存,
00:02:38查找包含掩码密钥的 workflow 密钥,
00:02:40它甚至会插入一个看起来像 CodeQL 的假 GitHub 工作流,
00:02:41将你的存储库密钥序列化
00:02:43并在稍后将其外泄。
00:02:45它也会搜寻 AWS 密钥,
00:02:47首先针对你的环境变量
00:02:48和本地配置文件,
00:02:50但随后也会针对 AWS 元数据服务,
00:02:52甚至还会插入一个伪装成Code QL的GitHub工作流
00:02:55对于 Kubernetes,它会窃取服务账户令牌
00:02:57和证书,这允许它在集群内访问 API,
00:02:58获取该 Pod 的服务账户所拥有的
00:03:00任何基于角色的访问控制权限,
00:03:02这在配置糟糕的集群中
00:03:03可能权限极大,
00:03:06有时甚至是管理员级别的权限。
00:03:09更糟糕的是,
00:03:11它还会针对 HashiCorp Vault,
00:03:14收集你所有与 Vault 相关的环境变量
00:03:17和令牌,
00:03:19然后利用它拥有的 Kubernetes 访问权限
00:03:21来获取你所有由 Vault 管理的密钥。
00:03:22以上这些仅仅是它对
00:03:24你的 CI 部署所做的事情。
00:03:25它还会进一步针对 HashiCorp Vault,
00:03:27它会针对你所有的 SSH 密钥、
00:03:29NPM 凭证、
00:03:30然后利用它所拥有的任何 Kubernetes 访问权限
00:03:32来窃取你所有由 Vault 管理的密钥。
00:03:34云服务提供商凭证、
00:03:35加密密钥、
00:03:37Signal、
00:03:38Slack
00:03:39以及 Discord 文件。
00:03:41除此之外,
00:03:42它还会提取你的 Claude Code 会话历史记录。
00:03:43所以,如果你曾经给过 Claude 凭证
00:03:44或者让它读取过包含凭证的文件,
00:03:45它也能访问那些内容。
00:03:45所以,正如我所说的,
00:03:45它们试图获取
00:03:46它们能触及的一切东西,
00:03:47然后通过 Session 信使网络外泄这些数据。
00:03:49作为备份,
00:03:51它们还会将这些被盗数据以死信投递的方式
00:03:53放入 GitHub 仓库。
00:03:55为了契合它们的所有攻击主题,
00:03:56这些分支都是用《沙丘》的梗来命名的。
00:03:57所以,它已经拿到了你的凭证。
00:03:58不会有比这更糟糕的情况了吧?
00:04:00嗯,有的。
00:04:02真的会更糟。
00:04:02除此之外,
00:04:04如果你还记得我说过它在你的机器上建立的那个服务,
00:04:05嗯,那个服务会监控你的 GitHub 令牌,
00:04:07并持续重新外泄它们。
00:04:09不仅如此,每分钟,
00:04:11它都会检查令牌是否依然有效。
00:04:12如果失效了,
00:04:13它就会在你的用户目录下运行 rm -rf,
00:04:14清空所有内容。
00:04:15它还会尝试用你的凭证
00:04:16创建一个 NPM 令牌,
00:04:18那个服务会监控你的 GitHub 令牌
00:04:19“如果你撤销此令牌,
00:04:21我们将清空机主电脑”,
00:04:22暗示它对 NPM 令牌也做同样的事。
00:04:24所以,如果你在隔离机器
00:04:25并移除那个后台进程之前就撤销了这些令牌,
00:04:27有效载荷就能自毁你的电脑,
00:04:28这真是太卑劣了。
00:04:30顺便说一下,
00:04:31此攻击的 Python 变体
00:04:32操作大致相同,
00:04:33但它还包含一个检查,
00:04:35用来查看你的机器语言是否为俄语。
00:04:36如果是的话,它就直接停止。
00:04:38如果你的机器看起来
00:04:39来自以色列或伊朗,
00:04:40它会生成一个 1 到 6 之间的随机数。
00:04:42如果数字是 2,
00:04:44它就会运行破坏性擦除命令,
00:04:46并尝试以最大音量播放
00:04:47一个 MP3 文件。
00:04:48可惜,我没能查出来
00:04:49那个 MP3 是什么。
00:04:51不管怎么说,
00:04:53做了这些事之后,
00:04:53最坏的情况还远没到来,
00:04:54因为那仅仅是第一阶段。
00:04:55第二阶段是自我传播,
00:04:56这才是这次攻击中最危险的部分。
00:04:58首先,它会在你的机器上寻找
00:04:59任何有效的 NPM 令牌,
00:05:00即无需双重身份验证
00:05:01即可进行发布的令牌。
00:05:03如果找到了,
00:05:04它会扫描该账户有权访问的所有包,
00:05:05然后使用这些凭证
00:05:05将自身添加到那些包中,
00:05:07并发布新的受感染版本。
00:05:07这显然非常糟糕,
00:05:08但你可能本就不该存放
00:05:09能够绕过双重身份验证的令牌。
00:05:11所以,这其中更可怕的版本是
00:05:13当它在你的 CI/CD 中运行时会发生什么。
00:05:15因为在 CI 环境中,
00:05:16攻击者并不需要
00:05:16长期的 NPM 令牌,
00:05:17因为好的基础设施设置
00:05:19通常依赖于 OIDC。
00:05:19且无需双重身份验证。
00:05:21如果它找到了,
00:05:22它会扫描该账户
00:05:24有权访问的所有软件包,
00:05:26然后利用这些凭据
00:05:26将自身添加到这些软件包中,
00:05:28并发布被感染的新版本。
00:05:30这显然非常糟糕,
00:05:32但你可能也不应该
00:05:33随意留着
00:05:33已发布的令牌
00:05:34能够绕过
00:05:35双重身份验证。
00:05:36所以更可怕的情况是
00:05:38当它在你的
00:05:39CI/CD 流水线中运行时。
00:05:41因为在 CI 中,
00:05:42攻击者不需要
00:05:43长期的 NPM 令牌,
00:05:44因为优秀的配置
00:05:45通常依赖于 OIDC,
00:05:47这本来意味着更安全。
00:05:48本质上,
00:05:49不再需要将
00:05:50NPM 令牌作为密钥存储,
00:05:51GitHub Actions 会向 NPM 证明,
00:05:53嘿,
00:05:53我是这个仓库
00:05:54正在运行这个工作流
00:05:55在这个分支上,
00:05:56然后 NPM 就会给它
00:05:57一个短期发布的令牌。
00:05:59但这带来的问题是
00:06:00如果脚本获得了
00:06:01受信任的 GitHub Actions 环境访问权限,
00:06:03它就能站在
00:06:04与合法发布者相同的位置。
00:06:06所以恶意软件可以利用
00:06:07GitHub 暴露给作业的
00:06:08OIDC 相关环境
00:06:10从 GitHub 的令牌端点请求 OIDC JWT 令牌,
00:06:12然后它会与 NPM 交换
00:06:14那个 JWT 令牌,
00:06:16通过 NPM 受信任的
00:06:17发布系统获取短期发布令牌,
00:06:18现在它就可以进行发布,
00:06:19而无需窃取
00:06:20永久的 NPM 令牌,
00:06:22而无需窃取
00:06:22永久的 NPM 令牌
00:06:24看起来完全合法。
00:06:26恶意软件会将
00:06:26那个 router init.js 文件的副本
00:06:27打包进软件包的表中,
00:06:29然后添加恶意的
00:06:30可选依赖项,
00:06:31最后全部发布为
00:06:32该软件包的最新版本,
00:06:33所以当有人
00:06:34或者某个 CI/CD 流水线
00:06:35安装这些软件包时,
00:06:35循环又重新开始了,
00:06:37尽可能地传播开来。
00:06:38这简直太疯狂了,对吧?
00:06:40但现在让我们把焦点
00:06:40放在“零号病人”身上,
00:06:42也就是 TanStack。
00:06:42他们最初是
00:06:43怎么被感染的呢?
00:06:44嗯,
00:06:46TanStack。
00:06:46他们最初是如何
00:06:47被感染的呢?
00:06:48嗯,
00:06:49根据他们自己的事后报告,
00:06:50攻击者滥用了
00:06:51那个 GitHub Actions 流水线。
00:06:53他们在恶意包
00:06:53真正发布的前一天,
00:06:54就开始行动了,
00:06:56他们创建了一个
00:06:57TanStack router 的分支,
00:06:58然后他们向这个分叉
00:06:59添加了一个恶意提交,
00:06:59他们伪造了
00:07:01作者为 Claude,
00:07:02并且提交信息
00:07:04前缀为 skip CI,
00:07:04这样它就不会在推送事件中
00:07:05立即运行 CI。
00:07:06第二天,
00:07:07他们针对 TanStack router
00:07:07发起了一个名为
00:07:08Work in Progress
00:07:09Simplify History Build 的 PR。
00:07:10这才是实际攻击发生的地方。
00:07:11简单来说,
00:07:13TanStack 有一个 bundle-sized
00:07:13GitHub Actions 工作流,
00:07:14它使用了 pull_request_target,
00:07:15这很值得注意,
00:07:16“简化构建历史”
00:07:18这就是
00:07:18实际攻击发生的地方。
00:07:20简而言之就是
00:07:21TanStack 有一个捆绑包大小
00:07:22基础仓库缓存范围
00:07:23及其 GitHub 令牌的访问权限。
00:07:25所以这个工作流
00:07:26检出了该 PR,
00:07:27安装了依赖项,
00:07:28并运行了基准测试构建。
00:07:29但问题在于,
00:07:30那个分叉中
00:07:31包含了恶意代码。
00:07:32在这种情况下,
00:07:33以及它的 GitHub 令牌。
00:07:35毒害了
00:07:35PMPM 软件包存储,
00:07:36使用了与发布操作
00:07:38稍后会使用的缓存键完全一致的
00:07:39缓存键。
00:07:40因为那个分支包含
00:07:40恶意代码。
00:07:41在这个案例中,
00:07:42它是一个设置脚本
00:07:43它污染了
00:07:44PMPM 包存储
00:07:45使用确切的缓存键
00:07:47即发布操作
00:07:48稍后会用到的那个。
00:07:49他们实际上预先计算了
00:07:50这个来自公开的
00:07:51PMPM 锁文件
00:07:52使用完全相同的公式
00:07:54即工作流所使用的公式。
00:07:56一旦那个被污染的缓存
00:07:57被保存,
00:07:57他们实际上重置了
00:07:58那个分支
00:07:59使其与当前的
00:07:59主分支匹配,
00:08:00所以那个可见的 PR
00:08:01看起来就像是一个零文件
00:08:02无操作更改,
00:08:03然后他们关闭了那个 PR
00:08:04并删除了
00:08:05那个恶意分支。
00:08:06所以从表面上看,
00:08:07看起来
00:08:07什么都没有
00:08:08发生,
00:08:09但他们已经污染了
00:08:10那个 GitHub Action 的缓存。
00:08:11这意味着
00:08:12八小时后,
00:08:13当一名普通的维护者
00:08:14将一个不相关的 PR 合并
00:08:15进主分支时,
00:08:16它触发了 Tanstack 的
00:08:17发布工作流,
00:08:18它恢复了
00:08:19PMPM 的被污染缓存,
00:08:20现在攻击者控制的代码
00:08:22正在那个
00:08:23发布操作中运行。
00:08:24它随后使用了相同的逻辑
00:08:25配合 OIDC
00:08:26来获取 NPM 发布令牌,
00:08:28并设法发布了
00:08:2984 个其自身的版本
00:08:30跨越 42 个 Tanstack 包,
00:08:32而且它甚至不需要
00:08:33到达
00:08:34发布包的步骤
00:08:35这一步。
00:08:36有趣的是,
00:08:36该操作实际上失败了
00:08:37因为有些测试失败了,
00:08:39所以它从未到达那一步,
00:08:40但恶意代码还是运行了,
00:08:41并把它们全部发布了,
00:08:43不管结果如何。
00:08:43所以攻击者设法
00:08:44串联了三个信任边界。
00:08:46首先,
00:08:47PR 分支的代码
00:08:47得以污染
00:08:48基础仓库的缓存,
00:08:49然后那个基础仓库的缓存
00:08:51在真正的发布工作流中
00:08:52被还原了,
00:08:53接着真正的发布工作流
00:08:54拥有 OIDC 权限,
00:08:56这转变成了
00:08:57NPM 发布权限,
00:08:58所以他们可以发布
00:08:59看起来完全合法
00:08:59的软件包。
00:09:01这就是我认为
00:09:02正变得非常可怕
00:09:03关于供应链攻击。
00:09:05他们不再局限于
00:09:05窃取单个维护者的令牌,
00:09:06而是滥用
00:09:07整个 CI-CD 系统本身,
00:09:08这意味着我们所有的信任信号
00:09:10这意味着
00:09:11我们所有的信任信号
00:09:12这是一个由真实工作流
00:09:13发布并带有有效来源的
00:09:14签名软件包。
00:09:15所以,以上就是
00:09:16ShaiHalud4 的相关内容。
00:09:18好了,
00:09:19如果你想查看自己是否
00:09:20因为这些软件包中的
00:09:21任何一个而遭到破坏,
00:09:21我会把链接
00:09:23放在下方的博客文章中,
00:09:23其中会介绍
00:09:25你该如何确认,
00:09:25以及如果你已经安装了
00:09:26其中之一该怎么做。
00:09:27在评论区告诉我
00:09:28你对这一切
00:09:29以及 NPM 生态系统
00:09:30有何看法。
00:09:30在看评论的时候,
00:09:31以及NPM生态系统,
00:09:33趁你在看下面的时候,
00:09:33下期再见。
00:09:34下期再见。
00:09:34下期再见。

Key Takeaway

攻击者通过污染GitHub Actions缓存并滥用OIDC信任机制,在无需获取永久NPM令牌的情况下,成功向TanStack等项目注入恶意版本并实现自我传播。

Highlights

  • 此次攻击感染了包括TanStack在内的42个软件包,并在数分钟内发布了84个恶意版本。

  • 恶意脚本通过伪造Claude作者身份并使用skip CI前缀提交代码,绕过CI流水线的即时执行。

  • 攻击者通过在PR分支中预先计算缓存键,污染GitHub Actions的存储缓存,从而在后续正常的生产发布工作流中注入恶意代码。

  • 恶意代码利用OIDC令牌与NPM交换短期发布权限,无需窃取永久令牌即可发布看起来完全合法的被感染版本。

  • 一旦安装了受感染的软件包,恶意软件会在系统后台运行,建立名为GitHub Token Monitor的服务来持续窃取凭证并具备销毁用户电脑的能力。

  • 该攻击对以色列或伊朗的特定环境设置了条件触发机制,在满足条件时会执行破坏性删除命令。

Timeline

大规模供应链攻击概况

  • 此次供应链攻击波及了TanStack、UiPath、Mistral等160多个软件包。
  • 恶意软件内置“死亡开关”,在检测到凭证被更换时会清空受害者的电脑。

攻击目标主要是开发者机器和CI/CD环境中的凭证,通过这些凭证进行后续的感染传播。攻击者通过巧妙利用GitHub Fork的链接处理方式,将恶意载荷伪装成合法项目的一部分。

恶意软件的行为机制与危害

  • 恶意代码通过修改VS Code任务运行器和Claude Code钩子目录,实现长期的持续存在。
  • 该软件扫描并外泄GitHub Actions密钥、AWS凭证、Kubernetes服务账户令牌以及HashiCorp Vault管理的机密。
  • 通过Session信使网络外泄被盗数据,并以死信投递方式将备份放入GitHub仓库。

安装脚本在后台脱离进程运行,并建立系统级服务实时监控并重新外泄GitHub令牌。如果受害者尝试撤销令牌,恶意程序会触发清除程序,删除用户目录下的所有文件。

基于CI/CD流水线的自我传播策略

  • 恶意软件利用GitHub OIDC环境,向NPM申请合法的短期发布令牌。
  • 受感染的软件包自动将自身作为依赖项植入到其他包中,形成循环传播。

在CI/CD环境中,攻击者无需窃取长期的NPM令牌。通过获得GitHub Actions环境的受信任访问权限,恶意程序能冒充合法发布者身份,使恶意版本看起来完全正常。

TanStack零号病人溯源

  • 攻击者通过污染GitHub Actions的缓存,将恶意提交带入生产环境的发布工作流。
  • 此次攻击成功串联了从代码分支到缓存还原,再到OIDC发布权限的三重信任边界。

攻击者先在分叉仓库中预计算并污染缓存,随后删除恶意分支使PR看起来正常。数小时后,正常的生产构建合并触发了被污染的缓存还原,从而在合法的发布流程中运行了恶意代码。

Community Posts

No posts yet. Be the first to write about this video!

Write about this video