provider 在密钥所在的地方运行
Aikido 发现了一个朝鲜的 RAT,以两个 Terraform provider 的形式分发在 HashiCorp 的注册中心上。terraform init 会下载 provider,terraform plan 会把它当成本地进程运行,而 provider 会读取你的云端凭证,因为 provider 本来就是干这个的。载荷只对某一位受害者的变量名解密,所以把包拉下来的扫描器看到的都是毫无作用的代码。被触及的十八台机器里有十台是 Mac。Bromure Agentic Coding 给那个可执行文件一台 VM、一把假的密钥,以及一份它可以调用的目的地清单。
业界为编码智能体建立的每一项控制,都位于命令的上游:批准计划、阅读 diff、把关
push。没有任何一项站在一个下载下来的 provider 可执行文件与 ~/.aws 之间。
你正要往一个 stack 里加一个容器。你向智能体要一个 Docker provider,它写了四行
HCL,你执行 terraform init。Terraform 从注册中心解析出这个 provider、下载它、
校验它的签名,然后放进缓存。接着你执行 terraform plan,Terraform 就把那个可执行
文件当成你机器上的一个进程启动,并通过 RPC 与它对话。过程中它会向你的云账号认证,
用的是你自己在用的那套环境变量和那份 ~/.aws 配置文件。
这每一步都是文档里写明的设计,而这个月它同时也是一条恶意软件的投递路径。
Aikido 发现了什么
9 月 22 日,Aikido 通报 了一个用 Go 写的远程访问木马,通过两个 Terraform provider 和两个 Go 模块分发。 The Hacker News 第二天跟进报道, 称这是攻击者首次利用 HashiCorp 运营的注册中心投递载荷。Checkmarx、JFrog 和 SafeDep 也补上了各自的分析。
这两个 provider 是 gocommunity-io/dockerd,下载数 222,以及
kreuzwenker/docker,下载数 1,449。后者与真正的 provider kreuzwerker/docker
只差一个字母,而后者宣称的下载量有 5,600 万。两个 Go 模块分别是 8 月 11 日发布的
gocommunity.io/orderedbtree 和 9 月 8 日发布的 gogets.dev/btreex。
Aikido 把这次行动关联到 Graphalgo,ReversingLabs 在二月记录过它,并归因于朝鲜的 操作者。同一把签名密钥自四月起就出现在 npm 的载荷里。人们是通过一份工作邀约走到 这些包面前的:一家假冒的 Web3 公司在 LinkedIn 或 Facebook 上接触开发者,发来一份 编程作业。作业的仓库是干净的,它拉进来的依赖才是恶意软件。
provider 是一个程序,而 Terraform 把密钥交给它
npm install 会执行包的安装脚本,然后退场。terraform init 取回的是一整个
程序,而运行那个程序才是重头戏。
Terraform 的 provider 是一个由 Terraform Core 通过 RPC 调用的可执行文件。 Terraform 运行它,因为运行它正是 provider 的定义。你不会找到可以摘掉的安装钩子, 也不会找到可以审计的生命周期脚本,因为在这里运行是功能本身,而不是解开 tarball 的副作用。
这个可执行文件还跑在你网络中通往生产路径最短的那个位置。Purple Shield 这样描述那台机器: “因此,一个恶意的 provider 会在部署你基础设施的那个人的机器上运行,带着那个人所 拥有的一切权限。”以及:“一台会执行 terraform apply 的笔记本,手边通常就有云厂商 密钥、Git 令牌、SSH 密钥和 Terraform 状态文件。”
npm 上的窃取程序得在你的家目录里四处翻找凭证。Terraform 直接把凭证递给 provider。
只寄给一个人
这些 provider 只攻击一个目标,对所有其他安装它的人视而不见。
两者都在 /internal/provider/resource_docker_container_funcs.go 里带着一个隐藏的
入口,只有当 Terraform 变量 containerName 和 networkID 拼接后的 SHA256 等于
某个硬编码的值时才会触发。接着代码用那个哈希作为 AES 密钥,解开包内的一个文件
路径,把找到的东西解压,逐个解密每个文件,再以分离的 go run . 当成 Go 包运行。
他们把压缩包藏在 examples 目录里,文件名叫 import-resource.sqlite3。
也就是说,攻击者必须知道你给那两个变量起了什么名字,而这个攻击者知道:工作邀约 这个诱饵,就是他学到名字的途径。每一位把包拉下来的研究者、每一个扫描器,拿到的 都是坐在那里什么也不做的代码。你无法检测永远不会解密的东西。
第二阶段是一个远程访问木马。它先回报系统信息,包括硬件属性、操作系统、主机名,
以及这台机器上有没有 node,然后在两个地方等待指令。一个是 Slack:一个叫
portfolio-devs 的工作区、一个叫 frontend-devs 的频道,之后还有第二个工作区,
里面有个叫 qa-announcements 的频道。Aikido 在那里数到 1,240 条加密消息,最早可
追溯到 7 月 16 日。另一个是 Arbitrum Sepolia 测试网上的一份智能合约,位于一个硬
编码的地址,含有 1,402 笔交易,最早追溯到 8 月 6 日。恶意软件每三秒轮询一次
区块链,每十秒轮询一次 Slack。
这两条通道都是通往信誉良好目的地的普通 HTTPS。你的开发机器整天都在跟 Slack 说 话,而其中一台跟公开的 Ethereum RPC 端点说话,在任何一周团队有东西要上链时看起来 都很正常。同一批人在 Checkmarx 上周描述的那个 npm 包里也用了 Sepolia 合约,他们 大大方方地重复使用,因为大大方方地用也行得通。
Aikido 从攻击者自己的 Slack 里捞出了受害者清单:725 条签到消息,涵盖 18 个不重复 的主机名。三台 Windows、五台 Linux、十台 macOS。
在 Bromure 工作区里有什么不同
这场行动要的是凭证。在 Bromure Agentic Coding 里,VM 的 ~/.aws/config 并不含
你的密钥。它指向一个 credential-process 辅助程序,通过 virtio socket 交出你真正的
访问密钥 ID,配上一把 40 个字符的假密钥。手册列出了完全不需要配置就会采用它
的工具:每一个 AWS SDK、aws CLI、boto3,以及 terraform。provider 按它预期的方式
读取凭证链,拿到一份格式完好的配置,然后用一把一文不值的密钥给请求签名。
主机会在出口把那个签名修好。它的 resigner 认得发往 *.amazonaws.com 的流量,剥掉
客户机那注定失败的 SigV4 签名,在请求离开你的 Mac 之前用真正的密钥重新签。你的
terraform plan 照常工作。AWS 则对被偷走的那份凭证副本回以
InvalidSignatureException。
接着那把假密钥想要出去。代理会扫描每一个外发请求的头部和正文,寻找工作区自己的 占位值,用的是便宜到可以挂在全部请求上的 Aho-Corasick 自动机。把一把假密钥发往它 并非为之铸造的主机,比如一个 Slack webhook,代理就回 HTTP 451,一个字节也不转发。 VM 暂停。一行红色的 Credential brokering 落在 Security Timeline 上,Bromure 把这个工作区标记为已被入侵,于是你下次启动时会有选项清掉它的磁盘和家目录,同时 保留你的令牌、SSH 密钥和设置。
C2 仍然得离开这台机器。出向流量是一组有序的允许与拒绝规则,自上而下匹配,最后以
default allow 或 default deny 收尾。一个部署基础设施的工作区需要
registry.terraform.io、releases.hashicorp.com 和你的云 API,这让
portfolio-devs.slack.com 和某个 Arbitrum RPC 端点都不在清单上。主机会强制执行
这些规则两次:虚拟交换机匹配目的 IP 和从 DNS 侧录到的主机名,TCP 和 UDP 都匹配,
代理再按 TLS 服务器名匹配一次。交换机把 80 端口和 443 端口导进代理,不牵涉任何
环境变量,客户机也没有任何东西可以关掉。每一次裁决都会变成 Security Timeline 上
的一行 Firewall。
删除由你批准
在 provider 与你的账号之间还有一道控制,而当攻击者要的是你的基础设施而不是你的 机密时,那才是最关键的一道。
AWS 凭证带有一份写入策略,新工作区默认是 Prompt before write。代理依据
X-Amz-Target 头或 Action= 参数里的动作名来分类每一次调用:Get*、List*
和 Describe* 是读取,一声不响地放行;而 Delete*、Terminate*、Revoke*
及其同类则算作破坏性操作。写入会停在一个标题为
Allow write on "<scope>" from workspace "<name>"? 的对话框,上面逐字显示该操作
而不是摘要,并列出 Allow for 15 minutes、Allow once、
Allow for the rest of the session 和 Don't allow。你拒绝,客户机就收到一个
AccessDeniedException,智能体会回报并继续往下做。
按下那个对话框的人是你,在你的 Mac 上。VM 里的进程够不着它,Slack 频道另一端的 那个人也够不着。
把工具链当成执行器看待
terraform init 和 terraform plan 就是下载并运行。要问一个 provider 在里面
会运行什么,因为它真的会运行。
不要让活的机密留在伸手可及之处
给 provider 一条能满足它的凭证链,以及一把一文不值的密钥。签名由主机在边界的 另一侧完成,并限定在那把密钥所属的目的地。
先给目的地点名
一个部署用的工作区需要的是一份简短稳定的清单:注册中心、发行主机、你的云 API。写下一次,把未命中的流量设成 Deny,死信箱合约就没有地方可以轮询。
让不可逆的操作开口问
让读取安静地跑。让 Terminate* 的代价,是一个显示在 VM 够不着的屏幕上的
对话框。
这里没有人找到漏洞
HashiCorp 的注册中心发出了别人向它索取的那个包。Terraform 校验了拿到的校验和, 并用它运行每一个 provider 的方式运行了这个 provider。没有沙箱被攻破,没有智能体 读到被污染的指令,也没有模型出错。
两个加起来 1,671 次下载的 provider 触及了至少 18 台机器,大多是 Mac,就坐在那里每 三秒轮询一次智能合约。撑起整条链的是文档里写明的行为。唯一新颖的部分——一段除非 你的变量恰好起了特定名字、否则一直保持加密的载荷——存在的理由,就是不让任何东西 找到它。
所以这些事要在会话开始之前就定下来,因为过程中你抓不到它们。你没办法扫描一个尚未 解开载荷的 provider,也没办法从发往 Slack 的流量里挑出外泄。把部署跑在密钥是诱 饵、目的地是你写下的清单、而被拿下的那台机器是你可以扔掉的地方。