一次会话,一百个仓库
Mandiant 的 AI Risk and Resilience 2026 报告描述了这样一个攻击者:他接管了某 SaaS 供应商一个正在运行的 AI 编码助手会话,借此让对方装上被投毒的 PyPI 包,窃走 GitHub OAuth 令牌,并把 Shai-Hulud 蠕虫扩散到约 100 个内部仓库。报告没有说会话是怎么被夺走的。在 Bromure Agentic Coding 的工作区里,它也不需要说:那个会话里的令牌是 `ghp_` 诱饵,想把它发出去就会让虚拟机暂停,而把一台机器变成一百个仓库的那些推送,会停在宿主机的代理上。
有人接管了一个正在运行、已通过认证、而且被信任的编码会话,就在某位开发者 自己的机器上。之后发生的一切,一直到一百个仓库,全都出自那台机器当时握着 的东西。
某家软件即服务公司的开发者问他的编码助手,一个再普通不过的问题该用哪个包。
助手说了一个。开发者说好,而这正是用助手的全部意义,剩下的交给
pip install。
那时攻击者早就握着这个会话了,敲下那条建议的人就是他。
Mandiant 在 AI Risk and Resilience Report 2026 中描述了这个案例,这份报告由 Google Cloud 的事件响应部门于本月发布; The Hacker News 在 9 月 16 日作了报道。 攻击者劫持了开发者工作站上一个正在运行的编码助手会话。再走五步,他就到 了大约一百个内部代码仓库,以及公司自己的包命名空间,第二位员工在那里拉下了 被投毒的版本。
一环一环看这条链
Mandiant 偏偏扣下了你最想知道的那一段。公开的案例研究没有说入侵发生在什么 时候,也没有说攻击者是怎么拿到一个运行中会话的控制权的。被窃的浏览器 cookie、 恶意扩展、共享机器上被入侵的 CLI,报告一个也没挑。想在里面找补丁的人,只能 空手而归。
第 2 到第 6 步完全没有任何漏洞利用。包安装就按包安装的方式运作,以开发者身份
运行的代码就去读开发者磁盘上摆着的令牌。带着有效令牌的 git push 就推得上去,
带着有效发布权限的发布就发得出去。只有一步是入侵,另外五步都是在花钱。
乘数是凭据,而且还在变大
Shai-Hulud 是那只自我复制的供应链蠕虫,从 2025 年起一路吞食维护者账号;它会
在这样的故事里作为载荷出现,是因为第 5 步正是作者当初造它的目的。GitGuardian
在八月拆解了一个近期变种,
发现它会扫过 469 个凭据所在位置,
比早期版本的 189 个更多:开发环境、CI/CD 工具、云端配置、AI 工具配置、包管理器
的配置文件、shell 历史、.env 文件、IDE 设置、CLI 缓存。这只蠕虫不是去猎捕
漏洞,而是去读一整个文件柜。
GitGuardian 的分析用一句话讲完了这个类型:
“攻击者已经不再试图打破信任关系,而是开始使用那些本来就让这些关系得以运作 的凭据。”
一台工作站之所以变成一百个仓库,是因为它握着价值一百个仓库的授权。那个数字 出自一份清单,而往清单里放什么,由你决定。
那个会话握着的是诱饵
Bromure Agentic Coding 让代理在你 Mac 上的 Linux 虚拟机里运行,而那台机器不 握有任何真正的机密。你不需要打开任何开关才有这个效果。工作区存放凭据的方式 本来就是如此。
你真正的 GitHub 令牌加密留在宿主机上。虚拟机收到的是一份保留结构的假货:
ghp_ 后面接 36 个字符,总共 40 个,所以 gh 自己的前缀与长度检查都会顺利
通过。Bromure 把它导出成 GH_TOKEN,并写进 ~/.git-credentials 和 gh 的
配置里;那是整台机器里唯一的 GitHub 凭据。宿主机上的代理会在请求离开虚拟机
之后才在链路上换上真正的值,而且只在目的地与该凭据所铸给的主机相符时才换。
每一份假货都是确定性的,由真值加上每次安装专属的 32 字节盐,经 HKDF-SHA256
推导而来,所以对自己密钥取指纹的客户端,在不同会话之间看不出任何轮换。
拿第 4 步来对付它看看。窃密程序抵达,扫过那 469 个位置,找到环境变量,找到
~/.git-credentials,找到 gh 的配置,然后把找到的东西寄出去。每一次读取都
成功,每一个文件都躺在蠕虫预期的地方。攻击者最后拿到的,是一串四十个字符、
在任何地方都通不过认证的字符串,除非它再穿回某一台特定的 Mac。
第 5 步和第 6 步都需要第 4 步交出一个能用的令牌,而第 4 步交出的是诱饵。
窃取本身就是警报
一份假令牌只有一个正当的去处。不会有任何无辜的请求,把铸给 github.com 的
ghp_ 诱饵送往别的主机,所以代理会扫描每一个外发请求的头部与正文,找出任何
离开自身范围的假货。它跑的是 Aho-Corasick 自动机,便宜到足以对准全部流量。
当代理找到一个,它做的不只是记录:
- 它以 HTTP 451 拒绝该请求,一个字节都到不了目的地;
- Bromure 当场把虚拟机暂停;
- 警示会给你 关机、保留以供调查(先把磁盘、家目录与共享文件夹导出以 便取证)或自担风险的 继续;
- Security Timeline 多出一行红色的 Credential brokering;
- Bromure 把该工作区标记为已遭入侵,于是下次启动会抹掉磁盘镜像与持久家目录。 你的令牌、SSH 密钥与工作区设置都能挺过这次抹除。
这些你都不用去开;探测器一直开着。第 4 步的外发尝试本身就是让会话停下来的 那件事,所以这起事件的结局是一台暂停的虚拟机和一条时间线记录,而不是一百个 仓库加一份案例研究。
一百个仓库就是一百次推送
假设你连保险都想多上一层。第 5 步是蠕虫往仓库里写,而宿主机会把写入分类。
工作区的护栏为每个服务带着一份写入策略。对 GitHub 而言,
这份策略不只读 REST API,也读 git over HTTPS:git push 以
git-receive-pack 抵达,算作写入;git fetch 以 git-upload-pack 抵达,算
作读取。只读会拒绝这些推送。写入前先询问是新工作区的默认值,它会把
每一次推送都拦下来,交给宿主机端一个写明操作名称的对话框。两种设置下 fetch
都能通过,所以代理照样能干活。
做这个判断的是宿主机上的代理,在虚拟机之外。一个会话已经属于别人的代理,既 关不掉这份策略,也绕不过去,因为策略根本不在代理那一侧运行。一只想要一百个 仓库的蠕虫,会收到一百次拒绝,或者问你一百次。
而且包终究得先送到
第 3 步正是 Mandiant 给出具体建议的地方:拿密码学校验和与允许清单去验证 AI 推荐的第三方依赖,并让依赖流量走你自己掌控的仓库。
在工作区里,每一次取包本来就会经过宿主机,因为代理是虚拟机通往网络的唯一
路径。Bromure 在 pypi.org 与 files.pythonhosted.org 拦下 PyPI,并在那里
套用宿主机的策略。虚拟机里的 pip.conf 可以把代理送出来的东西收得更紧,但
放不松。有三层会替 Python 包发言:
- 年龄门槛,默认开启,最短两天,会拒绝比门槛更新的版本;理由是刚发布的 版本,正是最可能刚被劫持的那一个。PEP 503 索引不带时间戳,所以 Bromure 会 按需去查发布时间;
- OSV 检查,对
api.osv.dev查询,免费且不需要密钥,会挡下任何带有你所选 严重程度以上安全公告的版本; - socket.dev 的受害包过滤,对恶意软件、已知恶意软件、抢注相似名称和流氓 安装脚本触发。
拦截会以 HTTP 451 返回,正文开头是
Bromure Supply-Chain Security blocked this request: 接着是原因,而 pip 会
一字不改地打印出来。你看得到安装为什么失败,代理也看得到,它往往能自己去钉
一个较旧的版本。那个较旧的版本,正是年龄门槛一直想把它导向的那一个。
Mandiant 的三项控制,以及它们住在哪里
报告以三项建议收尾这个案例研究。每一项都指向一个 Bromure 工作区早已把界线画 好的位置。
验证 AI 推荐的依赖
Mandiant 要求对助手建议的任何东西都做校验和与允许清单。你的工作区会在虚拟 机看到登记中心的响应之前,就先对它套用策略。代理会把太新的版本从版本列表 里拿掉,所以在代理眼中它们还不存在;若是直接指定了太新的版本,它会拒绝, 并在错误信息里引出该包的真实年龄。信誉服务的密钥留在宿主机上,不在那台 执行安装的机器里。
让原始密钥与长期令牌离得远远的
这就是把链路上的界线写成建议的样子。虚拟机里没有任何文件、环境变量或进程 握有真正的 API 密钥、OAuth 令牌、AWS 机密或 SSH 私钥。Bromure 把订阅的 OAuth 令牌加密存在宿主机上,并在到期前约五分钟在那里刷新,所以客户机不带 任何刷新令牌。两者之间的通道只往一个方向走:宿主机把假货写进去,而虚拟机 没有任何调用可以回头索取真的令牌。
让依赖流量走你自己掌控的东西
代理是虚拟机对外的唯一路径。每一个 HTTPS 请求都经由虚拟套接字送到宿主机, 由宿主机终结 TLS、检查请求,再用 macOS 自己的 TLS 栈重新发出。正是这条唯一 的路径,让另外两项控制从建议变成强制。没有任何取包能跳过检查,也没有任何 凭据能不经替换就离开。
还有,刻意把波及范围缩小
一百个仓库会倒,是因为一台机器带着一百个仓库的授权。把工作区切成一个项目 一个,或一道凭据界线一个。每一个对应唯一一台虚拟机,没有两个共用一台,而且 各自只带那个工作区的活儿所需要的凭据。任何一台机器上的清单,都在一个设置 面板里由你决定。
没办法打补丁的那一部分
Mandiant 用一句描述形态而非事件的话,框住了这个类型:“一个被投毒的数据源、 模型依赖或扩展挂钩,都可能把一个受信任的代理变成内部侦察、横向移动,或者从 沙箱自主逃逸的通道。”
三个不同的入口,一句话,而且三个都适用。入口决定攻击者怎么进来;某位开发者 机器里的东西,决定他能走多远。这次的攻击者是从被劫持的会话进来的。下一个会 从别的地方进来,而报告没有替你指出任何一扇该锁的门。
为入口之后的那一步做设计。假设这个会话已经属于别人——这正是案例研究借着始终 不解释第 1 步,强加给你的假设——然后再问,那个会话能够到什么。
这起事件的其中一个版本结束在第 3 步:终端里一个 451,Security Log 里一行记录。 另一个版本结束在第 4 步:一台暂停的虚拟机,加上时间线上一行红色记录,记着有 一份凭据试图离开。两个版本都到不了第 5 步,而那一百个仓库就在第 5 步。
把代理放在一台只握着诱饵的机器上。 安装 Bromure Agentic Coding,然后尽管让人把会话拿走。