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

代理的令牌是组织所有者的

10 月 23 日,LiteLLM 公布了遭投毒的 1.107.0 版本的根本原因。第一个被盗的凭据并非来自日志,而是来自 LiteLLM 自己的代理平台:它的 API 会返回云端代理用来开 pull request 的 GitHub 令牌,而那是组织所有者的令牌,权限没有收窄。这个 API 的密钥在一份公开配置文件里放了四个半月。在 Bromure Agentic Coding 中,开 pull request 的代理拿到的是替身,真正的令牌加密保存在 Mac 上,没有任何 API 会返回它。

LiteLLM 的代理需要一个 GitHub 令牌来开 pull request。它们拿到的,是属于组织所有者的令牌。一次使用泄露密钥的 API 调用,就把它取了回来。

10 月 23 日,LiteLLM 公布了其 1.107.0 版本背后的攻击链,也就是 10 月 21 日夹带凭据窃密木马发布的那个版本。这篇文章自称“权威的唯一事实来源”,并修正了 LiteLLM 最初的推断。被盗的 GitHub 令牌并非来自记录下来的模型流量。第一个令牌来自 LiteLLM 为自家编程代理运行的一项实验性服务。

我们在那个版本才发布两小时中报道过这次发布本身,当时沿用的是最初的推断。最终报告的起点不同:攻击者拿到的第一个凭据,属于那些代理。

握有所有者令牌的代理平台

用 LiteLLM 的话说,LiteLLM Agent Platform 是“一个让自托管云端代理向 GitHub 仓库提交 PR 的实验性项目”。为了开 pull request,平台持有一个 GitHub 令牌。这个令牌“没有关联服务账号,权限也没有收窄”。它是组织两位所有者之一的 GitHub CLI OAuth 令牌。

报告中的时间线(UTC):

  • 5 月 27 日 18:53。 一个 opencode.json 文件被提交到公开仓库 LiteLLM-Labs/lite-harness。那是一个代理运行框架的配置文件,里面有 Agent Platform 某个部署的主密钥。团队发现后,用 force-push 覆盖了那次提交,并把暴露的密钥标记为待吊销。“主密钥的吊销并未执行。”
  • 10 月 10 日 21:27。 攻击者从 69.48.229.143 用这把密钥调用平台的 API。GET /api/v1/managed_agents/agents?limit=100 返回了每个受管代理的配置,连同所有者的令牌。
  • 10 月 10 日 22:11。 攻击者用所有者的令牌,把一个一次性分支 ci-tmp-a351cc 推送到 BerriAI/litellm。它的工作流把六个仓库机密发到 webhook.site。其中的 GH_TOKEN,是第二位维护者的 GitHub CLI 令牌。二十四秒后,BerriAI/litellm-auto 里的第二个分支把第三位维护者的令牌发到同一个端点。两次运行都在三分钟内被删除。
  • 10 月 12 日。 私有仓库上又有其他工作流,把能接触到的每个仓库机密都发了出去。
  • 10 月 21 日 10:08。 用第二位维护者令牌推送到发布仓库的变更,被分支保护拦下。五秒后,所有者的令牌关闭保护并 force-push 这项变更,让发布工作流指向仿冒的 Berri-AI/litellm。
  • 10 月 21 日 11:11。 第二位维护者的令牌批准了生产环境关卡。原本有条规则要求由绑定在 LiteLLM 办公室某台设备上的账号批准,那一周被放宽了,于是任意两名工程师都能发布。
  • 10 月 21 日 11:39 至 11:46。 七个 wheel 上了 PyPI,接着是 Docker Hub 和 GHCR 的镜像。
  • 10 月 21 日 11:53。 第三位维护者的令牌删除了发布运行的记录。12:24,所有者的令牌用 force-push 把 main 恢复干净。

LiteLLM 自己的总结是:“这起事件有力地说明了,不轮换可能已暴露的密钥会带来什么风险。”该公司现在正在吊销与每个内部系统、每个供应商平台相关的所有令牌。

每支代理队伍都握有这样的令牌

LiteLLM 有一支能干的安全团队,它的配置也很常见。开 pull request 的代理需要一个能推送的令牌。最顺手的令牌就是你自己的 gh 登录,而 LiteLLM 的代理拿到的正是这个。

接着,令牌得放在代理读得到的地方:环境变量、配置文件、某个用来配置代理的 API 背后的数据库行。谁能读那个地方,谁就拿到令牌。这次,一把泄露过一次、从未轮换的 API 密钥就够了。

GitHub CLI 令牌让情况更糟。LiteLLM 指出,这些令牌“是在通过 2FA 之后签发的,不受 GitHub 针对组织或仓库敏感操作的‘超级用户’2FA 重新验证流程约束”。有了一个,攻击者就能关掉分支保护、force-push、批准发布,再抹掉证据。

在你自己的配置里找找同样的部件:代理环境里的 GH_TOKEN、含有 bearer 令牌的 MCP 配置、某天会有人提交出去的运行框架配置。LiteLLM 的 opencode.json 里不只有平台密钥,还有它的 GitHub、Linear、Slack MCP 服务器的 bearer 令牌,以及模型网关的密钥,全都是明文。

同一支队伍放在 Bromure 机器上

Bromure Agentic Coding 在你的 Mac 上以 Linux 虚拟机运行 Claude Code、Codex、Grok、Kimi 和 omp。它的自动化做的事和 LiteLLM 的代理平台一样:代理按计划或在出现新的 GitHub issue 时启动,在全新的 worktree 里工作,然后推送分支或开 pull request。拿 LiteLLM 的攻击链对一台运行这些自动化的机器走一遍。

代理拿到的是替身

在机器的 Credentials 中添加 GitHub 令牌。启动时,Bromure 会在虚拟机里写入一个假的:长度正确的 ghp_ 值,放在 GH_TOKEN、gh 的配置和 ~/.git-credentials 里。代理的 gh pr create 和 git push 照常工作。当请求离开虚拟机前往 github.com 时,Mac 上的代理服务器会把假的换成真正的令牌,而且只针对那个主机。

真正的令牌加密保存在 Mac 上,加密密钥保存在 macOS 钥匙串里。它从不写入虚拟机,不在任何文件,也不在任何变量。手册这样描述主机与虚拟机之间的通道:“虚拟机没有任何请求可以把真正的令牌要回来。”

所以,打开 LiteLLM 组织的那一步,在这里没有东西可返回。把代理的环境转储出来、读它的配置、通过 API 列出每个自动化:里面的 GitHub 值全是替身。把它粘贴进你自己的 gh,GitHub 会拒绝,因为只有那台机器的代理服务器知道它代表什么。

提交出去的配置也只有替身

这次泄露始于公开仓库中的一份运行框架配置。在 Bromure 机器上,代理读取的配置文件都是用替身写成的。在机器的 MCP 面板设置的 MCP 服务器 bearer 令牌,以 brm-mcp_… 值送到代理手上,在发往该服务器主机的请求中于传输途中替换。在 Preferences › Models 设置的模型密钥从不进入虚拟机;代理看到的是符合供应商格式的假值。其他任何 API 密钥放在 Other API keys,加上主机过滤,虚拟机会在你指定的变量里拿到一个 brm_… 值。

当代理不小心提交了自己的配置,或开发者把它复制到演示仓库,文件里带的都是替身。里面没有任何东西能在那台 Mac 以外使用,所以你会不会被入侵,不取决于一次 force-push,也不取决于一份没人做完的吊销清单。

没有平台 API 可调用

LiteLLM 的攻击者是从互联网访问到平台 API 的。Bromure 的自动化在你的 Mac 上运行。用手册的话说:“没有云服务,也没有入站网络攻击面。”GitHub 和 Linear 的触发器由 Mac 每三分钟用机器存储的令牌轮询一次,那个令牌留在主机上。没有 webhook 端点,没有主密钥,也没有泄露的密钥能打开的 /managed_agents 路由。

被盗的替身会触发警报

10 月 10 日的工作流把机密发到 webhook.site。设想同一招用在 Bromure 机器上:一个被投毒的依赖,或一段经由提示注入塞进来的指令,读取 GH_TOKEN 并把它发到 webhook.site。

代理服务器会检查每个出站请求的请求头和正文,找出被发往非其签发主机的替身。一个要发往 webhook.site 的 ghp_ 替身,会在任何字节发出前被 HTTP 451 拦下。虚拟机随即暂停,Mac 上弹出警告 This environment may have been compromised,列出令牌预览、凭据名称、它所属的主机,以及它被发往的主机。你可以关闭机器,或保存它的磁盘和主目录以供调查。

如果机器的沙箱策略把 Unmatched traffic 设为 Deny,webhook.site 根本连不上,这次尝试会在 Security Timeline 中显示为红色的一行。

推送会等你

新的 Bromure 机器启动时,GitHub 的写入策略是 Prompt before write。读取直接放行。git push 或任何对 github.com 的写入,都会停在你 Mac 上的对话框:

Allow write on "GitHub" from workspace "release-bot"?
POST /repos/<owner>/<repo>/git/refs

攻击者的每一步都是写入:ci-tmp- 分支、修改发布工作流、对 main 的 force-push、删除运行记录。从 Bromure 机器发出的话,每一步都会停在这个对话框。Allow once 只放行那一次写入,下一次推送会再问一次。force-push 在传输上仍是一次推送,Prompt before write 一样会拦。

无人值守的自动化没法等人点击,所以要给它刚好够用的最窄令牌,并套用为那台机器设置的策略。你添加到 release-bot 机器上的令牌,可以是只限单个仓库的细粒度令牌。你自己那个对一切都有所有者权限的 gh 登录,没有理由出现在那台机器上。

记录在你的 Mac 上

LiteLLM 在 GitHub 的审计日志里比对令牌哈希,才查出哪个被盗令牌做了什么。在 Bromure 机器上,每次在传输途中替换凭据,都会在 Security Timeline 留下一行 Credential brokering,附上打码后的令牌和主机。你拒绝的每次写入,都是一行红色的 Guardrails,附上方法和路径。在已注册到 bromure.io 的 Mac 上,同样的事件也会发送到你的组织,所以从意料之外的机器使用的令牌会自己浮现,不必有人翻遍审计日志。

从一份配置文件到签名的发布版本LiteLLM,5 月 27 日至 10 月 21 日1 · 密钥在配置里公开仓库的opencode.json,未吊销2 · API 返回令牌代理配置里有组织所有者的 gh 令牌3 · 机密被发出ci-tmp 分支再把两个令牌发到 webhook.site4 · 推送与发布关闭保护、force-push、批准关卡、删除记录在 Bromure 机器上配置里只有替身虚拟机里的 MCP、模型和 API 密钥都是假的,提交出去也不会泄露。没有令牌可返回没有入站 API;代理只有 ghp_ 替身,拿到别处 GitHub 拒收。已拦下,虚拟机暂停发往 webhook.site的替身得到 451,并弹出入侵警告。Prompt before write每次推送、force-push和 API 写入都要等Mac 上的对话框。
LiteLLM 的攻击链,以及运行相同代理的 Bromure 机器在哪里拦下每一步。所有检查都在 Mac 上、虚拟机之外运行。

代理拿到的是替身

GH_TOKEN、gh 的配置和 ~/.git-credentials 里放的是 ghp_ 假值。真正的令牌在 Mac 上换入,而且只针对 GitHub 的主机。

没有 API 会发放令牌

自动化在你的 Mac 上运行,没有入站攻击面。虚拟机能调用的任何东西,都不会返回真正的凭据。

泄露会让机器暂停

替身只要被发往自己主机以外的地方,就会被 451 拦下,虚拟机也会暂停,直到你做出决定。

写入前先询问

新机器在每次推送、每次写入 GitHub 之前都会询问,包括 force-push,并把确切的请求显示在屏幕上。

根本不要把令牌交给代理

LiteLLM 正在吊销所有令牌,并将在下周公布流程上的改变。轮换、收窄权限、服务账号都有帮助。但每一种做法仍会把一个能用的令牌留在代理读得到的某处,而那正是攻击者在 10 月 10 日来找的东西。

在 Bromure 机器上,代理用来工作的令牌,只能通过它自己的代理服务器、对它被签发的那一个主机生效。代理读得到的东西,接触到代理的攻击者也读得到;而在 Bromure 机器上,这些东西里永远没有真正的令牌。

想知道同样的替身如何对付盯上编程代理文件的窃密木马,请读窃密木马打包带走的是替身。关于代理留在自己对话记录里的副本,请读令牌留在了对话记录里。