返回所有文章
发布于 · 作者 Renaud Deraison

他们换掉的是地址,不是包

2026 年 8 月 31 日的十四个小时里,一部分发往 registry.coder.com 的请求,是由攻击者加进 Coder 自家 Cloudflare 源站池的服务器应答的。主机名没错,证书通过校验,模块带着一直以来的名字和版本,而回传的代码开始四处翻找 SSH 密钥、OIDC 令牌和数据库密码。Coder 手里没有攻击者的日志,所以它的公告要你自己去检查出站流量。这个问题,大多数跑着编码智能体的机器都答不上来。

主机名是 Coder 的。证书通过校验。模块带着你的模板早就钉死的名字和版本。8 月 31 日的十四个小时里,其中一些应答的服务器属于别人。

一位平台工程师在周一早上更新工作区模板。terraform initregistry.coder.com 拉取模板指名的模块(Docker、Zed、aider),跟上个月同一个 地方。Terraform 打印出它惯常的一片绿色,构建成功。那天早上、那整个月,没有任何 工具报警。

9 月 1 日,Coder 发布了 GHSA-vx42-ghc9-gw65, 评为严重,CVSS 9.0。用它自己的话说:“一名身份不明的恶意行为者取得了 Coder 的 Cloudflare 基础设施访问权,并向 Coder 模块注册表所用的池中加入了未经授权的 IP 地址。”在 8 月 31 日(周一)07:35 至 21:45 UTC 之间,一部分对 Coder 模块的请求 是由攻击者控制的机器应答的。Coder 做的是自托管的云端开发环境; BleepingComputer 列出 Dropbox、Palantir、Square、Mercedes-Benz、KKR、EnBW、美国政府机构和国防承包商 都在它的用户之列。

没有人动过那个包

一支谨慎团队所有的供应链习惯,检查的都是产物。名字拼对了吗。发布者是不是他 自称的那个人。这个版本是昨天才发的吗。锁文件的哈希还对得上吗。升版之前先读 diff。

攻击者改的是:你的客户端去问地址时,由哪些机器来回答;产物本身他没碰。他没有 发布任何包,没有接管任何维护者账号,没有注册任何近似名称,也没有编辑 Coder 注册表里的任何东西。名字一如既往地解析成功。TLS 握手是对着一张为真实主机名签发 的有效证书完成的。模块就叫 dockerzedaider,因为那正是 Coder 模板会要 的模块,而且它带着模板早就钉死的版本号。

不一样的是回传的内容。被投毒的副本夹带了一个叫 dlp.sh 的 shell 脚本,为 docker、aider、rstudio-server、windows-rdp 和 zed 各模块各自构建了一份,公告里 也各自附了哈希。公告把这个载荷描述为“设计用来识别若干凭据,并把它们外传到一台 远程服务器,该服务器的域名与 Coder 经常使用的某个域名极为相似”的代码。那个域名 是 coder-infra[.]com,8 月 28 日注册,比任何人用到它早了三天,解析到 199.91.220.205,回调位于 http://www[.]coder-infra[.]com/cli/check

它下手的清单,读起来像一份开发平台信任的盘点:配置器的环境变量与密钥、用户的 OIDC 令牌、SSH 密钥、外部认证凭据,以及 coderd 的配置变量,含数据库密码和认证 提供方凭据。

你不需要做任何反常的事就会落在那个时间窗里。按公告所说:“使用 Coder 主模块 注册表(registry.coder.com)作为模板来源,且在这段代码可获取的时间段内更新过 组件的用户,可能已经安装了恶意包。”创建模板、升一版模板、跑一次 dry run,或在 关掉模块缓存的情况下构建工作区,都是再普通不过的周一工作。

8 月 31 日 07:35–21:45 UTC — 十四小时的时间窗构建开口要terraform init新模板、升一版,或一次 dry run名字解析registry.coder.comCloudflare,真正的大门池里的一个 IP攻击者加进去的,供应改过的dlp.sh回调地址coder-infra[.]com8/28 注册,8/31 启用客户端检查了什么,又发现了什么主机名Coder 自己的,不是近似域名证书有效,签给真名,在真的边缘节点模块名docker, zed, aider —正是你要的那几个版本模板里早就写着的那一版。没有新东西四项检查全部通过。是哪台服务器应答的,正是客户端所有检查都不看的那一件事。
攻击者把自己的 IP 地址加进 registry.coder.com 背后的源站池,于是主机名、证书、模块名和钉死的版本全都是真的,只有字节来自别的地方。一次被投毒的构建和一次干净的构建,在可观测层面只差一件事:一条通往三天前才注册的域名的出站连接。

“检查防火墙、代理、DNS 和 VPC 流日志”

这是 Coder 补救步骤的第五条,也是整份公告里最有启发的一行。第一到第四条是 Coder 帮得上忙的事:找出时间窗内被缓存模块的 SQL、清掉它们的 SQL、打过补丁的版本 (2.37.0、2.36.4、2.35.7 和 2.34.9,全都在 9 月 1 日放出),以及一份该轮换的凭据 清单。第五条伸到这一切之外,因为能说出谁中招的那些服务器属于攻击者。Coder 看不 到它们,而且直说了:它无法确凿地认定每一个被入侵的部署。

于是问题回到你身上,而这是一个关于证据的问题。跑过那个模块的机器上,有任何东西 连过 coder-infra[.]com 吗?对一个生产环境的 VPC,多数团队一小时内就能回答, 因为平台团队里已经有人拥有流日志。

现在,换成你的编码智能体干活的那台机器再问一遍。智能体会跑 terraform initnpm installmake、一次容器构建、一轮测试、一个它在仓库里找到的脚本、一个 某模块下载回来的二进制文件。这每一件事都会打开连接,而没有人把它们写在你一周后 还能检索到的地方。智能体自己的会话记录留着模型发出的工具调用,却没有任何一行 记录 Terraform 模块里的 shell 脚本对网络做了什么,因为模型压根没看见那件事发生。

第五步的答案没法事后再买。要么在请求离开机器的那一刻就有东西把它记了下来,要么 根本没有东西可查。

那个脚本本来会找到什么

就照载荷的说法,让它跑。它扫过环境找凭据,读走 SSH 密钥,抓走 OIDC 和外部认证 令牌,捡起里面带着数据库密码的配置变量,然后把整包发往 coder-infra[.]com

Bromure Agentic Coding 给每个工作区一台跑在 Apple hypervisor 上的专属 Ubuntu 虚拟机,并以一个宿主机端代理作为客户机通往网络的唯一出口。在那里 跑同一轮扫描,它会带回完整一套凭据,而且全是假的。

ANTHROPIC_API_KEY 里放的是 sk-ant-api03-brm-… 诱饵,GH_TOKEN 是长度正确的 ghp_ 字符串,~/.git-credentials~/.docker/config.json~/.kube/config 里放的是相应的占位值。格式都保留着,所以 ghdockerkubectl 都会照收 不误。代理把你真正的值加密存在你的 Mac 上,等请求离开虚拟机之后才在线路上换进 去,而且只在请求要去的正是这份凭据所签发的那台主机时才换。没有开关能关掉这件事; 在这个 app 里,凭据本来就是这么工作的。

SSH 密钥是最干净的例子:虚拟机里根本没有密钥可读。SSH_AUTH_SOCK 指向一座跨越 虚拟 socket、通往宿主机上代理程序的桥,而那个协议里有一种请求叫“帮我签这个 挑战”,没有任何一种请求叫“把密钥交给我”。ls ~/.ssh/id_* 回来是空的,而 git push 照常工作。

AWS 值得单独一段,因为 Terraform 面对的正是它。虚拟机的 ~/.aws/config 指向一个 辅助程序,它吐出的是你真实的 access key ID,配上一把假的 secret key。 Terraform、AWS 的各种 SDK、boto3 和 aws CLI 都不用额外配置就会读这个辅助程序, 接着用假密钥签请求,产出一个格式完整、但 AWS 会拒绝的签名。宿主机在出去的路上 剥掉那个签名,用真正的密钥重新签一次。一个在环境里翻找 AWS 素材的脚本,带走的是 一个 key ID 和四十个什么都认证不了的字符。

接着它得把战利品发到某个地方,而工作区的出站防火墙就坐在那里。把 Unmatched traffic 设成 Deny,虚拟机就只能连到你列出的主机,别的都不行。执行发生两次,在 客户机够不到的两个位置:虚拟网络交换机按目的 IP 和从 DNS 里嗅到的主机名比对每一 条流量,跨所有协议;代理再按 TLS 服务器名比对一次。没有人会把一个三天大的域名 放进那份清单。规则修改不用重启就能送达运行中的会话,所以你可以在构建还在跑的 时候补上 deny any coder-infra[.]com

如果流量还是出去了,可能是工作区跑在默认的全部放行姿态下,也可能是目的地看着 太普通,那么诱饵就会做它的第二份工作。代理会扫描每一个出站请求的头部和正文, 寻找一份正发往“当初并非为它签发之处”的假凭据。一个寄往 coder-infra[.]comsk-ant-api03-brm-… 字符串毫不含糊。代理会在任何一个字节抵达目的地之前用 HTTP 451 挡下,暂停虚拟机,并发出一条点名该凭据和两端主机的告警。Bromure 把这个 工作区标记为已入侵,在你抹掉它的磁盘和家目录之前,它不会再启动。

第五步的答案在事情发生的当下就到手,而不是一周后从日志里拼回来。

在跑过那个模块的机器上terraform apply → dlp.sh以构建自身的权限运行它收集什么配置器的环境变量与密钥SSH 密钥、外部认证凭据用户的 OIDC 令牌coderd 配置 — 数据库密码出站:一个 HTTPS POST,来自整天都在发 HTTPS 请求的构建主机“我们中招了吗?”→ 检索你可能没有的日志真凭据出去了,宿主机上没有任何东西记下那个请求。在 Bromure Agentic Coding 工作区里terraform apply → dlp.sh照样运行 — 以 ubuntu 身份,在客户机里一次性 VM · 宿主机代理是唯一出口每个环境变量与配置令牌都是诱饵没有私钥字节 — 由宿主机通过 vsock 签名AWS:真 key ID、假 secret,宿主机重签出站防火墙默认拒绝 — 回调被挡诱饵被发出自身范围 → HTTP 451,VM 暂停,工作区标记为已入侵不论哪个进程发出,每个请求都有一行它收到的是诱饵,代理挡下回调并留下了那一行。
同一个模块、同一个 dlp.sh,在两台机器上。左边它找到真的环境变量、真的 SSH 密钥和真的令牌,而它的回调只是一个没有任何人记下来的普通 HTTPS 请求。右边它找到的是诱饵,没有私钥字节,签名密钥什么也认证不了,回调撞上一道默认拒绝的出站防火墙,而一份发往范围之外主机的诱饵会让虚拟机停下来。

一条命令回答那份公告

宿主机代理会为每一个离开虚拟机的请求写下一条元数据记录,不论是哪个进程发出 的:时间、主机、端口、方法、路径、状态、延迟,以及在任何凭据替换之前量到的 字节数。把工作区的 Session trace 设成 Activity only,你会拿到这条 记录,而且完全不存储任何请求正文。接着 bromure-cli trace hostnames my-workspace 就会把这个工作区接触过的每一个不同 主机连同次数一起打印出来。Coder 的第五步,一行搞定,针对那台真正跑过代码的 机器。

各个引擎做了什么判断

Security Timeline(Window → Security Timeline…)是另一个视图,看的是 Bromure 的引擎判断了什么:每一次出站防火墙的裁决、每一份代理居中处理的凭据、 每一次供应链拦截,全部标上颜色、可以过滤。一条通往三天前才注册域名的被拒连接, 会在构建还在跑的时候就以一行红色的 Firewall 出现在那里,而不是等别人的公告告诉 你该往哪看之后,再去 grep 一个月的流日志。

客户机能碰到你 Mac 上的只有一样东西:共享文件夹,一扇通往你真实文件系统的实时 读写窗口,而入侵后的擦除按设计不会动它。共享项目目录而不是你的家目录,那么一个 猎捕 id_rsa.env 的脚本,走遍的会是一个塞满检出和包缓存的 Linux 家目录。

当你无法检查来源,就盯住出口

所有来源验证最终都归结为同一个动作:把送到的东西,跟你已经信任的某样东西比对。 一个名字、一个发布者、一份签名、锁文件里的一个哈希。只要攻击者还必须往通道里放 点什么给你比对,这个动作就有效。握住通道一段的攻击者什么都不放进去,而 Coder 的 源站池就是通道的一段。8 月的 BdThemes 入侵是同一个形状, 一个被劫持的存储桶改掉了 35 万个 WordPress 站点会去拉取的 JSON 源,没有任何发行,也没有任何文件被 改动

十四个小时就是一个工作日。Coder 抓到了,第二天早上就送出补丁,公布了哈希和指标, 也告诉读者该检查什么。但这份公告最后仍然要求每一位读者,自己去看自己的出站流量, 因为另一端的记录已经没了。

轮换公告列出的密钥、清掉缓存的模块、打上补丁。然后决定下一次要让它碰到什么, 因为下一次会从一条你从来没有理由怀疑的通道进来。 安装 Bromure Agentic Coding,给构建一台自己的机器,等公告出现 的时候,答案已经备好了。