你排查的是错的那四十分钟
2026 年 8 月 14 日,SOCRadar 公布了对 LiteLLM 供应链入侵事件的重新归因:受影响的组织中有 95% 早在几天前就已经被收割干净,而下手的是 Aqua Security 自家的扫描器 Trivy。五个月的事件响应全都按错的时间窗口来划定,因为那条链上的每一道控制,都得先等有人把恶意包叫出名字。Bromure Agentic Coding 的供应链层和凭据层不需要那个名字。
有人让你去查,3 月 24 日那四十分钟的窗口里,你有没有装过某个 Python 包。五个月 后,SOCRadar 发现 95% 的受害者早在五天前就已经被掏空了,而掏空他们的,是他们自 己的漏洞扫描器。
8 月 14 日,SecurityWeek 的 Ionut Arghire 公布了 SOCRadar 的重新归因, 对象是今年规模最大的几起开发基础设施入侵之一。SOCRadar 手里握有记录级别的数据集, 覆盖 2,188 个实体逐一的收集记录,并且提了一个没人问过的问题:每个组织的数据究竟 是什么时候离开的?
其中 2,085 个组织,收集在所有人被警告的那些恶意包发布之前就已经结束了。这是 95%。SOCRadar 的结论只需要一句话:
“这个时间点对得上的是上游的 Trivy 入侵,而不是 LiteLLM 的安装窗口。所有人报道 的那四十分钟是收尾的一幕,不是整出戏。”
研究人员早在 3 月就把载荷拆解过了。SOCRadar 改变的是时间线。五个月里,整个行业拿 着在有人指认那个坏东西之前什么都做不了的控制,对着错的窗口做事件响应。
收尾的一幕
那著名的四十分钟属于 LiteLLM。3 月 24 日,1.82.7 和 1.82.8 两个版本在 10:39 UTC 上了 PyPI,撑了大约四十分钟后被 PyPI 隔离。当时的指引让你把那天 16:00 UTC 之前所 有的安装都当作可疑。
这个载荷值得停下来看看它击败了什么。它是一个放进 site-packages 的 .pth 文件,
litellm_init.pth。Python 会在解释器启动时执行 .pth 文件,不管有没有东西去
import 安装它的那个包。没有 setup.py 可检查,也没有安装脚本可剥掉,所以
--ignore-scripts 以及所有建立在安装脚本执行模型之上的控制,全都从旁边擦身而过。
The Hacker News 在 8 月 12 日
报道了这个机制,
并提到了生态系统级别的标识符 CVE-2026-33634。收割到的东西送往
models.litellm[.]cloud。
攻击者用一枚偷来的 PyPI 发布令牌把那些 wheel 推了上去,绕过了 LiteLLM 自己的发布 流水线。那么,这枚令牌是从哪来的?
整出戏
Trivy。Aqua Security 的开源漏洞扫描器,数以万计的流水线正是拿它来找这一类问题。
Aqua 的公告
GHSA-69fq-xp46-6x23
没有淡化根因。一个被追踪为 TeamPCP(Google 称其为 UNC6780)的威胁行为者,在 2 月
底通过 pull_request_target 工作流,从 Trivy 的 GitHub Actions 环境里取走了一枚个
人访问令牌。Aqua 在 3 月 1 日披露此事并轮换了凭据。公告接着写道,那次轮换“并非原
子的(并非所有凭据都被同时吊销)”。攻击者就坐在那次轮换的内部,Aqua 铸出多少新的
密钥,他就收走多少。
十九天后,他把这些花了出去。3 月 19 日,他把 aquasecurity/trivy-action 77 个版
本标签中的 76 个,以及 aquasecurity/setup-trivy 全部七个标签,force-push 到
恶意提交上,于是一个钉在 @v0.28.0 的工作流会拉到新代码,而仓库上看起来什么都没
变。他用被入侵的服务账号 aqua-bot,把 Trivy v0.69.4 发布到 GHCR、ECR
Public、Docker Hub、deb 和 rpm。Docker Hub 上的镜像 v0.69.5 和 v0.69.6 在 3 月 22
至 23 日跟上,用的是另一枚单独被入侵的凭据。
StepSecurity 的拆解 描述了流水线一旦执行它,被植入的窃取程序会做些什么:
- 通过
/proc/*/environ读取运行器上每一个进程的环境变量。 - 用 base64 编码的 Python,经由
/proc/<pid>/mem转储 GitHub Actions 运行器 worker 进程的内存。这会把运行器在日志里掩码掉的密钥捞回来,因为掩码只是一道日 志过滤器,而进程内存里放的是未掩码的值。 - 扫遍文件系统,寻找 SSH 私钥、git 凭据,AWS、GCP 和 Azure 的令牌,Kubernetes 的 secret、Docker 配置、数据库凭据、Terraform 状态,以及加密货币钱包。
- 把这一切用一把硬编码在程序里的 RSA-4096 公钥做混合加密封起来,送往
scan.aquasecurtiy.org。
再读一遍那个主机名。那是厂商自己的域名,两个字母调换了位置。对域名信誉情报来说,
或者对凌晨两点扫一眼出站流量的分析师来说,一台扫描器在跟看起来像
aquasecurity.org 的东西讲话,是整页里最不值得留意的一行。
如果上传失败,窃取程序会在受害者自己的 GitHub 账号上建一个名为 tpcp-docs 的公开
仓库,把战利品当作 release 附件挂上去。
SOCRadar 测到的第一笔收集,发生在恶意构建上线后的十八分钟。
从流水线里出去的东西
Hudson Rock 拿到了那份归档,Help Net Security 在 8 月 13 日 报道了它的大小: 153 GB、433,909 个文件、118,829 份 CI 运行器转储,对应到 2,488 个企业域名。CloudSEK 独立清点出约 434,000 个文件,把暴露规模放在接近 2,500 个组织、超过 430,000 条流水 线。Hudson Rock 的 CTO Alon Gal 称之为一场“全球性的道德披露行动”,并说这个量级 “把我们推进了一个在响应方式上完全崭新的世界”。Kevin Beaumont 的判读是:“这是一 场因为 AI 安全做得差而造成的大规模供应链入侵。”
SOCRadar 逐组织的拆解走得更远。超过一千个组织丢了 JWT 和认证令牌。数百个丢了私 钥、AWS 访问密钥、GitLab 令牌、OpenAI API 密钥、Slack webhook、GitHub Actions 令牌 和 Google API 密钥。有一个组织丢了大约 3,477 项单独的密钥。一千一百个组织暴露了提 交者的邮箱地址。运行器涵盖 GitHub Actions、GitLab CI、Jenkins、Bitbucket、CircleCI 和 Buildkite。德国、巴西和法国受创最重,而这些收集成果现在正在 Telegram 上被转手。
那条链上的每一道控制都需要一个名字
把 3 月时存在的防御一字排开,然后记下每一道在能够动作之前需要什么。
公告需要 Aqua 先识别出入侵。下架需要 PyPI 先识别出包。IoC 清单需要有人把
scan.aquasecurtiy.org 读成敌意的东西,而不是读成一个大家都信任的厂商名字的错
字。“你有没有在 10:39 到 16:00 UTC 之间安装”这张检查清单,需要挑对那五个小时。域
名信誉需要那个域名先有信誉。那份告诉 2,085 个组织他们一直在看错的一周的重新归因,
需要一份泄露归档浮出水面,还需要一支研究团队在五个月后坐下来把它翻完。
上述每一项都是身份型的控制:它要等到有人给某样东西命名之后,才对那样东西动作。 命名是生态系统分享知识的方式,而且行得通,所以这不是在抱怨那些写公告的人。这是在 谈操作的先后顺序。身份型控制在命名之后才抵达,而在这场行动里,每个制品都活了三到 十二小时。从发布到第一笔收集之间的那十八分钟,回答了这个顺序够不够好的问题。
在这里能改变结局的控制,是那些不需要名字的。
一只钟,不是一位神谕
Bromure Agentic Coding 让每一次包拉取都先过主机侧的 MITM 代理,才让任何一个字节抵 达 VM,而年龄门是唯一一个默认就 开着的供应链层。它拒绝任何比截止值更年轻的包版本。出厂默认是两天。
它不做任何查询。它对维护者、发布者、签名或名字都没有意见。它读发布时间戳,套上一 个下限,前提是:一个刚出炉的版本,最有可能就是一小时前被劫走的那个;而等这个窗口 过去,对一个正在干活的开发者来说几乎不花什么成本。
面对这场行动,那就是整场仗。LiteLLM 1.82.7 和 1.82.8 存在了四十分钟。两天的下限意
味着,从 VM 内部看,它们根本从未存在过。代理会把太新的版本从注册表元数据里剔掉,
并把 latest 和其他 dist-tag 重新指向仍然存活的最新版本,于是
pip install litellm 和所有 semver 范围都会解析到 1.82.6,不报错,也没有任何东西
让智能体绕过去。如果你用精确钉选去要一个被投毒的版本,制品拉取的兜底关卡会返回一
个 451,正文会把该包的实际年龄跟所要求的最小值摆在一起。pip 会原样打印出来:
Bromure Supply-Chain Security blocked this request: pypi package
[email protected] published 34 minutes ago — policy requires 2 days minimum
这道门覆盖 npm、PyPI、Cargo、RubyGems 和 Packagist,都带完整的发布时间数据。至于 pip,由于默认的 PEP 503 索引不带时间戳,它会对 PyPI 的 JSON API 做按需查询。每一个 决定都会在发生的当下落入安全日志(窗口 → 供应链日志…)。
内存转储捞得到什么
窃取程序最高明的一手,是去读 CI 运行器 worker 的内存,把日志掩码藏起来的密钥捞回 来。它之所以有效,是因为掩码只是化妆,而值本身握在进程手里。
把同一招拿去对 Bromure 的工作区跑,捞回来的是占位符,因为进程手里握的就是占位符。 线上边界是这个产品的核心机制:你 配置的每一份凭据,在 VM 内部都以保留结构的假货形式出现,而真值加密留在你的 Mac 上。主机侧的代理会在请求离开 VM 之后才把它换到线上,而且只有在请求要送往这份凭据 当初铸给的那台主机时才会换。
拿窃取程序自己的清单,逐条对着一台 Bromure VM 走一遍:
/proc/*/environ
ANTHROPIC_API_KEY 是 sk-ant-api03-brm-…。OPENAI_API_KEY 是
sk-brm-…。GH_TOKEN 是 ghp_ 加上 36 个字符。结构都保留着,所以 claude
和 gh 收下它们毫无怨言,而对其他任何人来说一文不值。
/proc/<pid>/mem
转储进程捞到的,是跟环境变量里一模一样的占位符。内存更深处也没有藏着什么有特权 的副本:主机侧的代理是在 VM 外面、在字节已经离开之后才做替换的。
SSH 私钥
一把也没有。SSH_AUTH_SOCK 指向的是一座架在 vsock 端口 8444 上的 ssh-agent 桥
接。私钥的字节从未进入客户机,也无法从客户机里读出来。Bromure 在设计上就让你
macOS 的登录代理不被暴露。
云、k8s、Docker、数据库
~/.aws/config、~/.kube/config、~/.docker/config.json、
~/.git-credentials、~/.config/doctl/config.yaml。全都在,全都有内容,也全都
是假的:brm-k8s-…、brm-docker-…、brm-db-…、glpat- 加 20 个字符、
dop_v1_ 加十六进制。
Bromure 用 HKDF-SHA256,从真值加上一份每次安装各自的 32 字节盐,派生出每一个假货, 因此一个会对自己的密钥做指纹的工具(Claude Code 会缓存一份密钥哈希)永远不会看到 凭据在不同会话之间换过。也没有什么开关可以忘记打开。代理是 VM 通往网络的唯一路径, 所以绕过它的请求身上带的是占位符,会在上游认证那一关失败。这道边界在结构上就是失 效即关闭。
从没有人见过的那个域名仿冒
现在来看外传。3 月 19 日,scan.aquasecurtiy.org 没有信誉、没有历史,也没有任何理
由出现在谁的封禁清单上。TeamPCP 挑上它,正是因为这一点。
Bromure 的入侵检测一样也不读。它盯的是你的凭据,而不是目的地的信誉。一具用工作 区自己铸出的那些假货建成的 Aho-Corasick 自动机,会扫过每一个出站请求的头部和正文。 当其中一个假货出现在一个送往它铸造范围之外的主机的请求里,代理就把这个请求当成外传 企图:
- 代理用 HTTP 451 拒绝该请求,且一个字节也不转发给目的地。
- Bromure 当场暂停 VM。
- 一条告警会指明发生了什么:一次把会话凭据外传到非其铸造对象主机的尝试。
接着 Bromure 会把该工作区标记为已遭入侵,而下一次启动必须先抹掉 VM 的磁盘镜像和持
久化家目录才会开机。你的令牌、SSH 密钥和工作区设置都能撑过这一关。追踪检查器
(⇧⌘I)和 bromure-cli trace leaks 会显示出事的主机以及那个确切的请求,让你在一分
钟之内就能判断罪魁祸首是某个依赖,还是一段被注入提示里的指令。
一个范围限定在 amazonaws.com 的 AKIA 形状铸造物,出现在送往
scan.aquasecurtiy.org 的 POST 里,第一次尝试就会触发那具自动机。检测器从没听说过
那个域名,也不需要听说过。它知道的是:这份凭据只有一个合法的目的地族群,而这一个是
另一个。没有东西需要启用;它在每一个请求上都在跑。
你早就拥有的那条时间线
这里的记录问题,活得比事件本身还久。
研究人员是从外面重建时间线的,材料是一份 153 GB 的泄露归档,而它得先浮出水面、被拿 到、被筛过,才有人说得出哪个窗口才是关键。受害者没办法从自己的记录里回答,因为那些 记录漏掉了决定一切的两个事实:他们的流水线在什么时候拉了哪些制品,以及由此产生的进 程把流量送去了哪里。
Bromure 的工作区把这份记录当成运作的副产物生出来。每一次拉取都穿过主机侧的代理,所
以安全日志会以实时流的形式保有每一笔年龄门、OSV、socket.dev 和 451 的判定。每一个请
求都穿过同一台代理,所以 bromure-cli trace ls 会按工作区给你主机、方法、状态、延
迟,以及 swap×N / LEAK×N 标记,trace hostnames 会列出一个会话接触过的每一个不
同主机,trace summary 则把整份聚合起来。这一切在你的 Mac 上静态存储时,都在保险库
主密钥之下保持加密。
于是 SOCRadar 在 8 月才回答出来的那个问题——我被收集了吗,什么时候?——变成一次你自 己在 3 月就能对着自己的数据跑的两分钟查询,不必等任何人先公布正确的名字。
这就是全部的论点。没有人能让生态系统给东西取名字取得更快:3 月让我们看到,连厂商自 己的事件响应都留下了一条十九天的尾巴和一个错的归因。你能改变的,是你的结局要不要取 决于那个名字。在注册表前面放一只钟,在跑代码的那台机器里放上占位符,并且为它拉了什 么、跟谁讲过话,留下你自己的日志。
资料来源:SecurityWeek,“Trivy, Not LiteLLM Behind the 2,500 Org Compromise”(2026 年 8 月 14 日) · Aqua Security 公告 GHSA-69fq-xp46-6x23 · StepSecurity,“Trivy Compromised a Second Time” · The Hacker News(2026 年 8 月 12 日) · Help Net Security(2026 年 8 月 13 日) · CrowdStrike,“From Scanner to Stealer”