漏洞

OpenBao:恢复一份备份,为什么会运行目录外的程序

OpenBao 把插件路径检查放在注册时,恢复快照却能直接换掉插件记录。服务器随后照着新记录启动进程,直到加载时重新检查真实路径,才堵住这条路。CVE-2026-104090 需要高权限恢复能力;另一组缺陷能在特定配置下逐步取得它。

浅暖纸手绘:备份中的插件记录进入服务器,指向配置目录外的程序。
文章导航

负责恢复备份的人,往往有权把服务带回过去某个时刻。OpenBao 这次暴露的问题,又给这项权限添了一种后果:换进一份准备好的快照,服务器可能启动插件目录以外的程序。对于一个集中保管凭据、签发证书的服务,这已经越过了恢复数据的范围。

OpenBao 的公告将它标为 CVE-2026-104090,影响 2.6.3 之前的版本,9 月 23 日发布的 2.6.3 和 2.7.0 首次修复。到本文 10 月 2 日核对时,最新版本已是 2.7.1;暂时留在 2.6 分支的部署也已有 2.6.4 安全更新。仍使用 Raft 存储、允许加载外部插件的旧部署,应优先处理。

单看这条漏洞,需要先取得高权限快照恢复能力。9 月 28 日,ControlPlane 又公开了一条组合路径,说明另外三个错误怎样在特定身份认证配置下,把最初没有令牌的人送到恢复接口。先看最后一步更容易理解整件事:程序明明检查过插件目录,为什么恢复以后就不管用了?

1 备份里有一份“运行什么”的记录

OpenBao 可以把认证、数据库连接等功能交给外部插件。管理员在服务器上配置 plugin_directory,再登记插件的文件名、参数、环境变量和 SHA-256。登记成功后,这些信息保存在插件目录表,也就是 plugin catalog 中;需要插件时,OpenBao 从表里取出记录,创建子进程,再与它通信。这里的目录表是数据记录,和磁盘上的插件文件夹是两回事。

正常登记确实有检查。2.6.2 的 Set() 与 setInternal() 拒绝带有父目录引用的命令,拼接配置目录和文件名,解析符号链接,再检查真实文件的父目录。普通外部插件的父目录必须等于配置的 plugin_directory。这些步骤可以挡住登记时企图把命令指向外面的输入。

快照恢复走另一条路。Raft 是 OpenBao 可用的一种集群存储后端,它的快照保存整份存储状态,也包括插件目录表。handleStorageRaftSnapshotWrite() 接收快照;使用 snapshot-force 时,跳过快照是否能由当前 seal 机制验证的检查,因此可以导入来自另一套密钥的存储。接口仍要求权限,force 放宽的是快照兼容性检查。随后恢复流程清理内存状态、替换存储。密钥变了以后,还须用快照对应的密钥完成解封,必要时迁移 seal 配置,才能继续加载服务;恢复回调在无法取得可用密钥时会报错。

于是,一条插件记录可以随整份存储进入服务器,完全不经过刚才的登记函数。攻击者控制自己的快照内容及其解封材料,服务器加载新状态时,看到的是这份快照带来的记录。记录里原先在哪里登记、在那里允许运行什么,都不能替当前服务器作决定。

旧版 PluginCatalog.get() 却把这个决定省掉了。它从加密存储取出 JSON,解码成 PluginRunner,核对插件类型,然后只用 filepath.Join 拼接路径。路径拼接会整理路径片段,本身没有“必须留在这个目录”的保证。登记时成立的限制,就这样在恢复后消失了。

2 握手失败时,进程已经启动过了

还有两道看起来可能拦住它的检查:文件的哈希,以及插件通信协议。沿着调用顺序看,它们各自解决的问题就很清楚。

run_config.go 把记录中的命令和参数交给 exec.Command,把环境变量加入进程配置,同时把记录中的 SHA-256 交给 go-plugin。插件客户端随后调用 Client(),进入实际启动流程。程序不会因为 JSON 中写了一个路径就立即执行,中间仍要通过这些步骤。

OpenBao 2.6.2 使用的 go-plugin v1.8.0 在 Client.Start() 中先读取目标文件,检查它是否符合给定的哈希。校验值也来自刚恢复的目录记录,攻击者可以把路径与相应校验值一起放进去。因此,攻击者需要服务器上已有目标程序,并知道或猜中其 SHA-256;这个检查仍然执行,只是无法判断该程序是否获准充当插件。

通过校验后,go-plugin 在第 735 行启动子进程,再等它从标准输出给出插件握手信息。普通系统程序可能根本不会说这个协议,加载最后也可能报错;它的入口代码却已经获得执行机会。日志里的“插件连接失败”,可以发生在命令执行之后。

这也解释了只读容器镜像为什么仍需修复。目标可以是镜像里原有的可执行文件,整个过程无需往插件目录新增文件。实际后果受 OpenBao 进程身份、容器约束、文件权限以及传入参数影响;公开材料支持的是服务环境内的代码执行,没有据此确认宿主机逃逸。

3 补丁在读记录时,再看一次真实路径

修复提交很小。原来登记时使用的路径检查被提取成 checkCommandDirectory(),登记和读取都调用它。2.6.3 的 get() 在交出插件记录之前,先用当前服务器的配置检查路径,失败就返回错误。来自快照的记录,也必须经过这里。

快照替换插件记录,加载时读取记录。修复前拼路径后进入启动流程;修复后解析真实路径,目录不符就拒绝加载。
图 1:新增检查发生在读取插件记录时。图中省略了两版都保留的文件哈希校验;启动仍以文件存在、哈希匹配等条件成立为前提。

检查的是解析符号链接后的真实位置。新函数先调用 EvalSymlinks,再取父目录的绝对路径。普通插件要求父目录与配置目录完全相同,放在任意更深子目录中也不会自动获准。通过 OCI 分发的插件则走自己的缓存规则,要求父目录精确匹配由插件类型、名称和哈希前八个十六进制字符组成的缓存位置。

这条修复顺序可以用下面两行节选对照。两行分别来自上述旧版与修复版 get(),后者还会在 err != nil 时立即返回。

// 2.6.2
entry.Command = filepath.Join(c.directory, entry.Command)

// 2.6.3
entry.Command, err = c.checkCommandDirectory(entry.Name, pluginType, entry.Command, entry.Sha256, entry.Oci)

对于恢复演练,这意味着验收必须看到插件真正重新加载。只确认快照导入成功、健康接口可用,会漏掉之后才读取的插件记录。可以在隔离副本中检查正常插件能启动、哈希不符的文件被拒绝、指向目录外的路径或符号链接被拒绝,并观察每个节点的实际加载结果。本文核对了调用链与补丁,未执行整套产品利用;这些检查是按修复位置提出的回归建议。

4 有限权限怎样一步步碰到恢复接口

恢复权限本来就很重,所以管理员还会问:为什么要特别担心这次漏洞?ControlPlane 给出的场景把权限来源接了起来。部署用证书认证工作负载,把应用放在一个受限命名空间里;负责创建应用的 provisioner 可以管理部分证书角色,但不能修改管理员角色。环境中另有可修改角色策略的管理员,以及根命名空间里负责快照恢复的策略。

第一处错误出在证书签发。ACME 通常通过挑战确认申请者控制某个域名,随后签发证书。OpenBao 的旧实现核对 DNS 和 IP,却可能把证书请求中未经验证的 URI 等其他名称一起签进去。SPIFFE 恰好用 URI 表达工作负载身份;当证书认证信任这些名称时,额外带入的 URI 就有了权限含义。补丁检查 SAN 扩展,只接受订单能够验证的 DNS/IP 类型,拒绝其余类型。

要从这里开始那条链,还要允许 ACME 签发带 ClientAuth 用途的证书;默认配置没有打开它。申请者也要能完成域名挑战、知道 provisioner 的身份标识,并满足部署的 ACME 访问要求。满足这些条件后,证书先换来受限角色的令牌。

下一处错误让角色名字出现了两种理解。访问规则可以广泛允许管理角色,再单独拒绝 admin;处理证书角色的代码却把名字转成小写。于是,规则看到的不同拼写,到了后端又落在同一个角色上。受限角色可以借此修改管理员证书角色要求的 URI,让手中的证书满足它,再次认证便取得管理员角色。修复把路径规范化移到权限判断之前。原有策略里的路径仍由管理员检查并改为规范形式,补丁不会自动重写所有策略文本。

取得命名空间内的管理员权限后,还差根命名空间的恢复策略。旧策略缓存用 path.Join(ns.UUID, name) 合成键,路径清理会把名字中的上级目录片段也解释进去。补丁改用分别保存命名空间和名称的结构体,两个字段不再经过路径运算。官方公告还限定了成功条件:目标策略在令牌创建和使用时都须留在内存缓存;引用另一命名空间的 root 策略,也不会直接得到全局 root 权限。

组合场景利用的是那份已有的快照恢复策略。攻击者用管理员角色修改 token_policies,加入指向该跨命名空间策略的引用,再次认证后取得它授予的恢复权限,最后才进入本文分析的恢复与插件启动路径。这些部署条件决定了链条能否接通;单独的快照漏洞仍要求高权限,非 Raft 后端不受这条快照恢复路径影响。

5 升级后,要让恢复出来的服务真的跑一遍

目前的升级选择是 2.7.1,留在 2.6 分支则更新到 2.6.4。两者包含首修,且补上了 10 月 1 日发布的另一批安全问题;2.6.3 和 2.7.0 用来定位这次修复的历史边界。跨版本前应阅读官方升级说明并保存可信的数据备份。若升级失败,保持受影响入口受限,用与数据兼容的已修复版本恢复服务;OpenBao 不保证存储向后兼容,仅换回旧程序可能无法恢复,退回漏洞版本也会重新打开这条执行路径。

来不及升级、又不依赖外部插件时,可以移除 plugin_directory 配置并按部署方式重启验证。源码在目录未配置时只查内置插件,这会停止正常外部插件的使用,相关认证或凭据服务可能随之不可用。限制快照恢复权限有助于减少可触达的人;强制 ACME 使用 EAB 则收紧前段证书申请。这些措施分别作用在不同位置,已有高权令牌仍须按其权限处理。

恢复测试环境还需要隔离外网。OpenBao 的升级指南特别提醒,副本中的动态凭据可能到期,测试实例可能尝试撤销真实云资源或数据库凭据。使用可信快照建立副本后,除前述插件检查,还应实际完成一次正常登录和凭据读写。若某个插件现在因路径检查报错,应核对它的实际位置和符号链接,调整为允许的布局,再重试业务操作。

如果曾发生未经授权的恢复,调查对象还包括恢复后的插件记录、子进程和该进程能够访问的秘密。保留恢复请求、解封与重启记录,和进程遥测对应起来;插件握手错误本身不能排除进程已经运行。本文在 10 月 2 日读取的 CISA KEV 快照中未找到该编号,公开公告也未列出已确认受害者。

我认为这次补丁最有用的提醒,在于恢复功能的权限应该怎样理解。快照可以把“将运行哪个程序”这样的决定一起带回来。只在第一次登记时检查,等于让过去那台机器替现在这台机器批准执行。OpenBao 现在把决定放回加载现场,用当前配置再核一次路径;评审备份、迁移和导入功能时,也该沿着恢复后的记录,继续追到真正使用它的地方。

6证据与来源

6.1来源与材料

  1. OpenBao:CVE-2026-104090 快照恢复与插件执行公告https://github.com/openbao/openbao/security/advisories/GHSA-j6wc-jpvg-xfxq
  2. ControlPlane:四缺陷组合场景与披露过程https://control-plane.io/posts/unauthed-to-rce-in-vault-and-openbao/
  3. OpenBao:加载时校验插件路径的固定补丁https://github.com/openbao/openbao/commit/6477e66e7d568cb2b8dccbd86700e670a4f3b033
  4. go-plugin v1.8.0:校验、启动与握手顺序https://github.com/hashicorp/go-plugin/blob/155dcddc94873a285e14b7fa24b2f6ab6139668e/client.go
  5. OpenBao 2.7.1 发布说明https://github.com/openbao/openbao/releases/tag/v2.7.1
  6. OpenBao 2.6.4 发布说明https://github.com/openbao/openbao/releases/tag/v2.6.4
  7. OpenBao 升级与数据回退说明https://openbao.org/docs/guides/upgrade/
  8. CISA 已知被利用漏洞目录https://www.cisa.gov/known-exploited-vulnerabilities-catalog