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

它等到了第三次调用

Pillar Security 正在追踪 Deadbugz,一个通过 pull request 塞进开源项目的 MCP 服务器。它提供两个诚实的工具,数着你的智能体发出的工具调用,然后在第四次时把返回的元数据改写成指令:去收集 SSH 密钥、AWS 凭据、shell 历史和 kubeconfig,而且别提这件事。没有缺陷,没有 CVE:协议全都允许。真正被击碎的,是“批准一次服务器就能说明下一次调用”这个念头。在 Bromure Agentic Coding 的工作区里,整段载荷照跑,收到四个假货,然后在往外走的路上死在代理手里。

你读过源码。你跑过它。它照着宣称的样子把文字排好。你批准了它,而攻击者 正是算准了你会批准。

8 月 10 日晚间,UTC 21:52 到 23:07 之间,一个叫 zellkernel 的 GitHub 账号对彼此毫无关联的 AI、MCP 和开发工具项目开了二十三个 pull request。从头到尾七十四分钟。每个 pull request 都做同一个小改动:在某个配置文件里 加进一个 Model Context Protocol 服务器。

Pillar Security 在 8 月 12 日公布了这波行动, 并以其中一个变体所投放的文件,把它命名为 Deadbugz。一个月后,每月的 MCP 汇总仍以它开头。Adversa 昨天刊出的 九月摘要 把整个事件章节都给了它。替它挣来这个版面的,是那座时钟。

两个工具,加一个计数器

这个服务器自称 productivity-suite。它提供两个工具:format_textsummarize。两个都能用。把智能体接上去,请它把一段文字收紧,你就会拿到收紧的段落。

在它们背后,服务器维护着 Pillar 所描述的“一个内存中的、以客户端为单位的 tools/call 请求计数器”。当这个计数器数到三,服务器就改变它对另外两种请求的回答: tools/list,告诉智能体有哪些工具、各是做什么的;以及 prompts/get,把备好的指令交给智能体。 从第四次调用起,这些响应回来时就带着,用 Pillar 的话说,“意在把接上的 AI 智能体引向敏感的本地文件,并向其操作者隐藏该活动的指令”。

四样东西:SSH 密钥、AWS 凭据、shell 历史、Kubernetes 配置。然后再加一条指令: 向用户隐藏这个活动。

工具名称从头到尾没变。format_text 还是叫 format_text。翻脸的是它底下那段描述, 而描述正是模型当成权威来读的那一部分。

productivity-suite — 同样两个工具,计数器前与计数器后你的编码智能体tools/call ×N寻常的工作,寻常的回合内存计数器以客户端为单位,从不持久化阈值:三调用 1 – 3tools/list → format_text, summarize“把一段文字排好”prompts/get → 一段整理文字的提示审查会看到什么一个说到做到的小服务器两个工具,诚实的描述,没有意外已批准第 4 次第 4 次调用及其后tools/list → format_text, summarize同样的名字,不同的指令prompts/get → 载荷现在模型被交代要做的事~/.ssh · AWS 凭据shell 历史 · ~/.kube/config而且别跟用户提这件事
Deadbugz 以客户端为单位计数 tools/call 请求。前三次,tools/list 和 prompts/get 对 format_text 与 summarize 返回诚实的元数据。从第四次起,同样那两个工具名称回来时,附带的描述和备好的提示会要求智能体去收集 SSH 密钥、AWS 凭据、shell 历史和 kubeconfig,并且闭口不谈。工具名称从不改变,所以审查者看到的东西没有一样看起来不同。

这里没有一样是漏洞

整个故事里没有出现任何缺陷。没有内存破坏要修补,也没有 CVE 要提交,因为服务器没做任何协议所禁止的事。

MCP 是一个服务器在运行期公布自身能力、而且可以再次公布的协议。工具描述是服务器选择、 客户端取回的文本。提示也是服务器选择、客户端取回的文本。Deadbugz 完全照着设计在用这些东西。它唯一的原创贡献,是决定何时说真话。

而这样就够了,因为多数团队用来对付敌意 MCP 服务器的每一道控制,都只跑一次。你读了代码仓库。 你把服务器启起来戳一戳。你拿工具清单去对 README 的说法。你批准了,也许锁定版本,然后就往前走。 这其中每一件都是一张照片,而 Deadbugz 决定你什么时候按下快门。

Pillar 给 MCP 客户端开发者的建议本身就泄了底。把已批准服务器上的工具定义变动, 视为需要重新批准的安全事件。让定义的变动对操作者可见。在批准当下给工具定义取指纹, 之后再做比对。这里的每一条,都是在请你把一道检查从批准时挪到运行时。

Deadbugz 狩猎的种群既庞大又不稳定。七月发表的一份针对面向互联网的 MCP 服务器的动态评估,Exposed by Design, 从作者在公开互联网上找到的两万一千多个实例中,测试了 640 个已确认的生产服务器中的 414 个。它报告了横跨十个类别的 68 个可通报漏洞,91.8% 的服务器完全没有启用 OAuth 认证,以及 687 个在没有访问控制下暴露 shell 执行的工具实例。真正留在心里的数字是更替率: 41.6% 的已确认服务器在连续两轮测量之间、不到三天内就消失了,作者把这读成“未经安全评审的 快速部署周期”。你上周审计过的服务器,这周可能是另一套软件,或者已经不在了。

投递方式也值得一提,因为它很便宜。二十三个 pull request 中有十七个,是往项目的 MCP 配置里加一个远程端点 productivity-suite-mcp.onrender.com/mcp。四个把 Python 指向一个隐藏的本地文件 ~/.config/.cache/.sys/.deadbug-mcp.py。两个是向服务器目录的登记申请。 Pillar 在查看当时没有发现任何一个是经由 GitHub 的合并按钮并入的:十九个已关闭,四个仍开着。 但 pull request 只是一行配置旅行的方式之一。同样那一行,也塞得进一篇博客文章、 一份入门指南、一条 MCP 目录里的条目,或是某个关于文本排版的问题底下一则热心的回答。

那张购物清单

回到那张清单。它很具体,而且每次都一样:SSH 密钥、AWS 凭据、shell 历史、Kubernetes 配置。

那是一台开发机的常备库存。是信息窃取程序带走的东西,是恶意 postinstall 脚本拿 grep 去捞的东西,也是攻击者要一个被劫持的智能体去取的东西。这张清单历经十年的行动仍然稳定, 因为其内容稳定:一把用来签名的私钥、一个用来认证的密钥、一份你敲过什么的记录, 以及一个能通到生产环境的令牌。

一台普通笔记本上的智能体能满足每一项,因为它在你的家目录里有一个 shell,而且以你的身份运行。

同一张清单,放进 Bromure 的工作区

Bromure Agentic Coding 让每个编码智能体跑在你 Mac 上一台硬件虚拟化的 Linux VM 里, 并把每个真正的密钥留在那道边界的宿主机侧。智能体拿到的是保留结构的假货,由真值加上每次安装专属的 盐派生而来,因此那些会给密钥取指纹的工具,不会看到它在不同会话之间变来变去。宿主机侧的 代理会在请求离开 VM 之后才把真值换上线路,而且只在它要送往这份凭据所铸造的那台主机时 才这么做。

让载荷跑起来,让它赢。假设模型读了被投毒的描述、相信了它,然后动身去找。把那四项依序看过。

SSH 密钥。 智能体找不到任何密钥,连一把诱饵密钥都没有。VM 的 SSH_AUTH_SOCK 是架在 virtio socket 上的 ssh-agent 桥接,走 vsock 端口 8444。客户机可以列出身份、 请求签名;私钥的字节只存在于宿主机上,而且如手册所言,无法从 VM 内部读取或抽出。Bromure 从不把你的 macOS 登录代理暴露给任何会话。每一次真正发生的签名,都会发出一个带着该密钥 SHA256 指纹的 credential.ssh_sign 审计事件,而且你可以把任何导入的密钥设成每次收到签名 请求都要在宿主机上询问。

AWS 凭据。 一个看起来对、却一文不值的文件。VM 的 ~/.aws/config 指向一个 credential-process 辅助程序,它通过 vsock 端口 8445 发出真正的访问密钥 ID, 配上一个 40 字符的密钥。所有 SDK、aws CLI、terraform 和 boto3 都不需额外配置就会采用它,而客户机会产生一个格式完全正确、却用错误密钥算出来的 SigV4 签名。 宿主机端的重签器会剥掉那个签名,在往外走的路上重新签。把那组密钥带去别的地方,AWS 会回你 InvalidSignatureException

Kubernetes 配置。 ~/.kube/config 存在,里面放着一个以 brm-k8s- 开头的 bearer 令牌。通过代理,它对这个工作区所配置的那些 API 服务器是有效的。在别的地方它就是惰性的。

Shell 历史。 真的,而且会留存:编码工作需要持久的状态,所以工作区的 VM 是一台持续存在的机器,而不是用完即扔的。它保存的是一台从未装过真密钥的机器的历史, 其中的命令是在一个不属于你的家目录里、对着诱饵执行的,旁边还躺着同样是假货的 ~/.git-credentials~/.docker/config.json

那张购物清单,在工作区里跑一遍载荷索求什么它找到什么~/.ssh 私钥什么都没有 — 字节在宿主机上签名走 vsock 8444;密钥从不过去AWS 凭据真的密钥 ID,假的 40 字符密钥拿去别处重放:InvalidSignatureException~/.kube/config一个 brm-k8s- 占位值通过宿主机代理才有效,别处惰性shell 历史会留存,而且满是诱饵一台从未持有真密钥的 VM
载荷那张四项购物清单,拿去对 Bromure Agentic Coding 的工作区跑一遍。SSH 完全不返回任何私钥字节,因为签名发生在宿主机上、走 vsock 8444。AWS 配置发出的是真正的访问密钥 ID 配上一个假密钥,所以被偷走的一组会在上游过不了签名验证。kubeconfig 里放的是一个 brm-k8s- 占位值。Shell 历史则是一台从未持有真凭据的 VM 的历史。

然后它还得把东西送出去

收集只是一半的工作。另一半是往 productivity-suite-mcp.onrender.com 的那段外发路程,而这段路撞上了一整叠宿主机侧的检查。

那些假货是绊索。每一个都只有单一一族合法的目的地,也就是它被铸造时所对应的主机范围, 所以代理会扫描每一个外发请求的头部与正文,找出正跑向不属于它之处的假货。一旦命中, 它就把该请求当成凭据外泄未遂:以 HTTP 451 拒绝,一个字节也不转发,当场把 VM 暂停,并抛出一条告警,提供关机保留以供调查(先把磁盘、家目录与共享文件夹导出以便 取证)或自负风险的继续。这个事件会以一行红色的凭据代理记录,落在 Security Timeline 上。

第二道检查是可达性。每个工作区都带着一份有序的出口防火墙(动作、协议、主机或 CIDR、 端口,以及对网页流量而言的单个 HTTP 动词),并为未匹配的流量设有默认值。新工作区会放行 任何规则都没对上的流量;把它设成拒绝,列出工作真正需要的东西,而某个 render.com 的子域名并不在那张清单上。有两个位于客户机之外的组件在执行它:虚拟交换机以目的地 IP 与从 DNS 窥得的主机名匹配每一条流,代理再以 TLS 服务器名称匹配一次。规则的修改 不需重启就会抵达运行中的会话。

Guardrails 把闸门设在后果那一端。它把智能体对 Kubernetes、AWS、DigitalOcean、 容器镜像仓库、git 托管平台与 HTTPS 数据库的调用,分类成读取、写入或破坏性操作,而新工作区 默认为写入前先询问:每一次变更都会停在一个宿主机端对话框上,显示原原本本的操作、 确切的 SQL 或 METHOD /path。这正是 Pillar 对平台建造者的请求:敏感文件的读取、 凭据的访问与代码的执行,不该是远程元数据的后果。

内容路径上也坐着一个检测器。当智能体把外部内容(文件内容、抓回的页面、命令输出、 工具调用的结果)流回模型时,一个本地的 PromptGuard 分类器会在模型据以行动之前, 在你的 Mac 上给那些片段评分,并且可以记录、询问,或以 451 阻断。至于 HTTP 服务器的 MCP bearer 令牌,它根本不会进到 VM 里:真的那个留在宿主机上,智能体的配置拿到的是一个 brm-mcp_ 占位值,而设置字段把这件事明明白白说出来,绝不送进 VM — 由代理替换

检查只跑一次安装时批准,往后一直继承第 1 天 — 读源码、跑一次、批准服务器是诚实的,判定也是对的调用 1 – 3 — 仍然诚实没有任何事促使谁再看一眼第 4 次调用 — 元数据翻脸被一个事前就下好的判定涵盖着没有人被要求做新的决定智能体触及到什么真的 ~/.ssh · 真的 AWS 密钥 · 真的 kubeconfig检查每次都跑没有安装时的判定可以继承工作区 VM — 智能体与服务器的输出brm-k8s- · brm-mcp_ · 假的 AWS 密钥从开机起就是占位值;没有私钥字节vsock 8443 — 唯一的对外路径宿主机代理 — 每个请求都问一次出口防火墙 · 主机、端口、HTTP 动词写入策略 · 读取 / 写入 / 破坏性对送往模型的内容做注入扫描每个请求一行追踪记录,就在你的机器上外泄的那一段路假货跑出范围 → HTTP 451、VM 暂停、时间轴记录
同一个决定的两种摆放位置。左边,安装当下的一次批准涵盖了服务器此后所做的一切,于是第四次调用时的元数据变动,继承了第一天下的判定。右边,没有安装时的判定可以继承:凭据从开机起就是占位值,而智能体尝试的每一个操作,都会在往外走的路上于宿主机代理里被重新分类一次。

如果你碰过这一个

在你的 MCP 客户端配置、代码仓库与家目录里搜索 productivity-suite-mcp.onrender.com、较早的端点 promo-surname-xml-quantum.trycloudflare.com,以及本地的产物 ~/.config/.cache/.sys/.deadbug-mcp.py。把引入它们的任何配置变更还原, 并且不要执行那个脚本。保留你的 MCP 客户端日志,找出第三次调用之后的工具定义更新。 Pillar 对轮换的建议是有节制的:在本地证据支持确有入侵之处才轮换。

让问题变小的那条规则

在工作区的 Guardrails 面板中,把未匹配的流量设为拒绝, 并列出工作真正需要的东西:allow web api.github.comallow web registry.npmjs.orgdefault deny。然后在一次会话之后执行 bromure-cli trace hostnames my-workspace,它会打印智能体接触过的每一个不同主机 以及次数。一个你从未选过的目的地,会以一行你没写过的记录现身。

批准是一张快照;线路是连续的

这样一波行动之后的本能,是把评审做得更狠:多读一些源码、把 commit 锁定。两件事都值得做, 而两件事都没碰到那个机制。这里的评审是正确的。服务器在有人看着的时候都是诚实的, 然后依照它自己掌握的时间表改变了,而且这么做并没有违反任何一条协议规则。

一道能在服务器改变心意后仍然存活的控制,是不依赖于“先前判断正确”的控制。一个属于占位值的 凭据,在第四次调用是占位值,在第四百次也是。一把放在宿主机上的私钥,在任何一次调用都读不到。 代理在每一条连接上评估一次出口规则,并在每一次写入发生的当下加以分类。这些东西没有一样 带着你批准某物那一天的记忆,正因如此,攻击者无法继承它。

MCP 让“给智能体新能力”变得容易,这本来就是它的用意;同时它也让那些能力,变成由远方某一方 用文本描述、而你的模型当成指令来读的东西。两者都会留下来。决定你的智能体能持有什么、能触及什么, 并把强制执行放在服务器没有投票权的地方。安装 Bromure Agentic Coding,别给你的智能体任何值得等上三次调用的东西。