Shai-Hulud 入侵开发机、突袭 GitHub 组织权限:事后复盘
一名工程师因在开发机上安装了被恶意 npm 包 Shai-Hulud 2.0 感染的依赖,导致 GitHub 凭证被盗。攻击者在潜伏 17 小时进行侦察后,发起了 10 分钟的破坏性攻击,包括大规模克隆仓库和强行推送分支。团队在发现异常后 4 分钟内撤销了访问权限,并在 7 小时内恢复了所有受损分支。此次事件并未影响生产环境资源或已发布的 npm 包,但暴露了 npm 生命周期脚本的严重安全风险。为此,公司采取了禁用全局 npm 脚本、升级包管理器和启用更严格的分支保护等多项安全措施。
攻击的开始与潜伏
事件始于一名工程师在实验性项目中运行 pnpm install 命令。依赖树中一个被感染的包执行了恶意的 preinstall 脚本,其行为包括:
- 下载并执行 TruffleHog:一个合法的安全工具被用于恶意扫描凭证。
- 扫描本地凭证:在工程师的电脑上搜索 GitHub 令牌、AWS 凭证、npm 令牌和环境变量。
- 窃取并上传数据:将所有发现的敏感信息打包并上传。
攻击者在获得凭证后,并未立即行动。在长达 17 小时 的时间里,他们进行了 methodical 的侦察。
攻击者在工程师不知情的情况下,从美国和印度的服务器大规模克隆了 669 个代码仓库,甚至在工程师正常工作时也在监控其活动。
这种长时间的潜伏和监控行为表明,攻击是有预谋且高度组织的,而非随机的自动化攻击。
10 分钟的破坏行动
在完成侦察后,攻击者从潜伏转为主动破坏。在短短 10 分钟 内,他们对多个代码仓库发起了自动化攻击。
- 强行推送 (Force-push):攻击者试图用一个来自“Linus Torvalds”的“init”提交覆盖多个分支的历史记录。
- 关闭拉取请求 (PR):在 3 秒内关闭了 12 个 PR,这显然是自动化脚本所为。
- 尝试绕过保护:多次尝试对受保护的分支进行强行推送,但被 GitHub 的分支保护规则成功阻止。
这次攻击波及了 16 个代码仓库,共影响了 199 个分支,并关闭了 42 个拉取请求。
发现、响应与损失评估
幸运的是,团队的 Slack 频道集成了 Git 活动通知。当 #git 频道被大量“init”提交的强推信息刷屏时,团队立即意识到了问题的严重性。
“Urmmm guys? what's going on?”
从发现异常到采取行动,整个过程非常迅速:
- 5 分钟内:团队从发现第一次恶意推送到确认账户被盗。
- 4 分钟内:确认被盗账户后,立即将其从 GitHub 组织中移除,攻击随之停止。
- 1 小时内:全面撤销了该工程师在 AWS、Vercel、Cloudflare 等所有系统中的访问权限。
最终损失评估:
- 代码仓库:669 个公共和私有仓库被克隆,包含基础设施代码和内部文档。
- 分支和 PR:199 个分支被强行推送,42 个 PR 被关闭。
- 关键资产:npm 发布包和生产数据库未受影响。由于工程师本地没有 npm 发布令牌,且发布流程需要双因素认证(2FA),攻击者无法污染已发布的包。
恢复过程与私钥风险
恢复工作在攻击停止后立即展开。由于 GitHub 不提供服务端的 reflog,被强行推送覆盖的历史记录无法直接从服务器找回。团队通过以下方式在 7 小时 内恢复了所有 199 个分支:
- 利用 GitHub Events API:通过 API 获取攻击发生前的最后一次提交哈希值。
- 利用公开仓库的分叉:在公开仓库的分叉中找到了原始提交记录。
- 利用本地 reflog:未执行
git fetch --prune的开发者本地仍保留着旧的提交记录。
在调查中还发现了一个严重风险:一枚 GitHub App 的私钥文件位于被盗电脑的回收站中。尽管攻击者还需要数据库中的安装 ID 才能使用该私钥,这增加了攻击难度,但团队还是立即轮换了密钥,并通知了所有可能受影响的客户。
恶意软件 Shai-Hulud 工作原理
Shai-Hulud 2.0 是一种复杂的供应链攻击蠕虫,其核心在于利用了 npm 的 preinstall 脚本。
- 启动:在
npm install期间,恶意包的setup_bun.js脚本会下载或定位 Bun 运行时。 - 后台执行:它会启动一个分离的、无输出的 Bun 进程来执行真正的恶意负载,而
npm install命令则正常成功退出,不留任何痕迹。 - 凭证扫描与传播:后台进程使用 TruffleHog 扫描整个用户主目录以寻找凭证。如果找到 npm 发布令牌,它会用恶意代码感染该账户维护的所有包,并发布新版本,从而实现蠕虫式传播。
- 焦土策略:如果未找到任何可利用的凭证,恶意软件会尝试删除用户的整个主目录。
采取的改进措施与教训
这次事件促使团队重新审视并彻底强化了开发流程中的安全措施。
任意包在安装过程中能够执行代码,这本身就是攻击面。
核心改进措施:
- 全局禁用 npm 脚本:通过
npm config set ignore-scripts true命令,从根本上阻止了此类攻击。对于必须运行脚本的包,则通过白名单机制进行管理。 - 升级 pnpm 至版本 10:新版本的 pnpm 默认禁用脚本,并引入
minimumReleaseAge设置,防止安装刚发布不久的包,为发现恶意包提供缓冲时间。 - 采用 OIDC 进行发布:彻底放弃在开发者机器上存储长期有效的 npm 令牌,转而使用基于 GitHub Actions OIDC 的可信发布机制,令牌短暂且范围受限。
- 全面启用分支保护:为所有代码仓库(而非仅仅是关键仓库)启用了分支保护,防止未经审查的代码被直接推送到主干。
- 强化 AWS 访问管理:采用 Granted 工具来加密 AWS SSO 会话令牌,避免其在本地以明文形式存储。
这次攻击明确显示,依赖包的安全性是整个软件供应链中最脆弱的环节之一。在便利性和安全性之间,团队必须做出更谨慎的选择。