CopyFail: 从Pod到主机
摘要
Copy Fail 是一种新的 Linux 本地提权漏洞,它利用内核内存损坏缺陷重写页缓存,从而实现跨容器攻击和容器逃逸。
暂无内容
查看缓存全文
缓存时间: 2026/05/20 08:29
# 复制失败:从 Pod 到主机 - Xint
来源:https://xint.io/blog/copy-fail-pod-to-host
两周前,我们披露了 **Copy Fail**(https://copy.fail/),一种新型且极其危险的 Linux 本地权限提升漏洞。Copy Fail 利用内核内存损坏漏洞,无需向运行中的内核注入代码,因此体积小且异常可移植。Copy Fail 为攻击者提供了对 Linux 页缓存(page cache)中任意可读文件的可重复、受控的 4 字节写入能力;换句话说,它允许攻击者重写 Linux 文件系统上文件的缓存内容。为帮助运维人员判断其环境是否受 Copy Fail 影响,我们发布了一个概念验证(PoC)漏洞利用和一个模型攻击路径。我们的模型攻击针对大多数 Linux 系统上都存在的 `su` 二进制文件。由于 `su` 是 setuid root,攻击者若能重写该文件并执行之,即可提权至 root。重写后的 `su` 不再询问和验证 root 密码,而是直接跳过验证过程,将调用者送入 root shell。
我们的概念验证使一些人认为攻击仅限于重写 `su` 这类 setuid 二进制文件。并非如此!Copy Fail 及相关页缓存写入漏洞赋予攻击者的能力强大且多样。举例来说,让我们一起来解读如何利用它从一个命名空间隔离的容器中逃逸。
要理解这个新的漏洞利用模式,你必须先了解一点 Copy Fail 在底层的工作原理。Copy Fail 通过混淆处理 IPSec ESP 扩展序列号(`authencesn`)的内核代码实现漏洞利用。该代码通过 `AF_ALG` socket 暴露给非特权用户——这是用户空间与 Linux 内核加密子系统的接口。具体来说,Copy Fail 让 `authencesn` 代码以为它在处理一次性临时内存,而实际上它操作的是指向页缓存的可变引用。它告诉内核加密代码去解密一个密文块,解密所用的字节来自一个使用 `splice(2)` 从管道进行的零长度复制。由于 IPSec ESN 的线路格式与加密代码操作的隐式格式不同,`authencesn` 代码会重新排列序列号。但代码处理的并非来自数据包的一次性缓冲区;Copy Fail 已诱使其操作一个指向已缓存文件的引用。
跨容器内核攻击通常通过破坏内核内存实现:竞争窗口(race windows)、释放后使用(UAF)、版本相关的有效载荷。这些原语功能强大,因为它们能实现内核级别的代码执行。但它们也很脆弱。Copy Fail 是确定性的。它是一种更可靠的原语,用于实现跨 Pod 入侵或运行时投毒,且无需依赖内核代码执行。主要有两种攻击场景:
- **场景 1:跨容器投毒。** 从已被攻陷的 Pod 中,或从新启动的攻击者 Pod 中(仅需 `create pods` 权限),可能对同节点上通过相同底层 address_space 访问相同脆弱下层文件的容器进行后门植入。镜像引用可以不同;只需层哈希匹配即可。入侵仅存在于内核页缓存中,因此磁盘上的字节保持不变,且对基于无 Agent 的磁盘扫描器不可见。
- **场景 2:容器逃逸。** 从一个无特权容器内部,或从一个具有主机文件系统挂载的已攻陷 DaemonSet 中,获取主机上的 root shell。
## **为什么页缓存跨越容器边界**
页缓存在容器之间是共享的。无论你在哪个命名空间,内核处理的每一个 `struct file` 都带有一个 `f_mapping` 指针,该指针通常来自底层 inode 的 `i_mapping`。这意味着任何两个共享同一个 `f_mapping` 的文件描述符都共享相同的缓存数据。内核用来表示连续内存页的数据结构称为“folio”。对于普通文件上的标准缓冲 I/O,通过一个文件描述符的写入会更新缓存的 folio。后续的读取操作,在所有相关的文件描述符上,都会看到更新后的数据(受常规并发和顺序规则制约)。Copy Fail 通过第 1 部分描述的 `AF_ALG/splice()` 路径改变相同的 folio,绕过了常规的写入审计。其可见性属性不变:任何 `f_mapping` 指向受影响的 `address_space` 的文件描述符,在下次访问页缓存时都会读取到被修改的字节。这一切都与容器无关。容器隔离存在于挂载、网络、PID、用户和 IPC 命名空间中。它们中没有一个会创建每个容器独立的 `address_space` 或页缓存。当容器的文件访问到达相同的底层 `address_space` 时,它们共享缓存的 folio。
Kubernetes 容器的根文件系统通常是一个 **overlayfs** 挂载,由一个可写上层(通常是每个容器的临时存储)和一个或多个只读下层(镜像层)拼接而成。容器运行时(containerd、CRI-O 等)通过**内容哈希**对层进行去重:如果同一节点上的两个容器使用相同的解压层/快照,那么相应的下层文件可能由同一个主机 inode/address_space 支持,无论镜像名称是什么。这种复用通过共享公共层降低了镜像的存储需求。`python:3.12-slim` 和 `xint-flask-app:v1`(构建自 `FROM python:3.12-slim`)共享 Python 层。两者共享底层的 `debian:bookworm-slim`。一个 `redis:7-bookworm` Pod 在同一节点上与两者共享 Debian 层。
在 overlayfs 挂载的正常操作中,以写权限打开或截断下层文件会触发 overlayfs 的 copy-up 操作,然后才会进行写入,从而在 Pod 的上层分配一个新的 inode,使更改成为私有。通过仅存储这少量差异,容器可以高效复用其下层,同时仍向应用呈现一个可写副本。然而,Copy Fail 完全绕过了标准的写入路径。它改变的 folio 属于下层 `address_space` 本身,是主机范围共享的,而不是那些本应存储写入差异的上层。Pod 的 overlayfs 挂载各自呈现一个看似私有的 `/usr/local/lib/python3.12/site-packages/foo.py`(或 `/lib/x86_64-linux-gnu/libc.so.6`),但 overlayfs 会将文件 I/O 委托给真正的下层文件。如果这些下层文件是同一个下层 inode/address_space,那么缓存的 folio 就是共享的:Copy Fail 的 4 字节写入会进入那个单一底层条目。随后任何通过相同底层 address_space 读取同一个下层文件的内容,都能读取到被投毒的字节,直到该页被驱逐或该层被丢弃。磁盘上的 inode 未改变,因此镜像仓库扫描器、检查磁盘哈希的文件完整性监视器,以及绕过受影响运行内核页缓存的离线、快照或块级扫描器,都只能看到原始内容。
## **场景 1:跨容器投毒**
**威胁模型。** 非特权攻击者,没有特权能力,没有节点访问权限,没有修改其他工作负载的准入权限。两种起始方式:在攻击者已控制的 Pod 中执行代码(1-1),或者仅拥有 `create pods` 权限(1-2)。
**目标。** 选择节点上广泛共享的层中的一个文件:如果节点运行基于 Python 的工作负载,则选择 Python 的 `site-packages/` 模块;若想获得更广泛的覆盖范围,则选择共享对象(如 `glibc`),但需满足可执行映射、补丁对齐和崩溃安全性的约束;Debian/Ubuntu/Alpine 基础层中的任何文件均可。本演示中我们将使用一个 Python 源文件。选择解释器启动时或常见框架初始化时导入的模块,以便目标 Pod 尽早加载它。
**写入操作。** Python 文件是演示的理想目标,因为它们更容易阅读,且比 shellcode 更具可移植性。对 Python 文件的任何更改当然可以通过链式使用我们的 4 字节写入原语来完成。具体选择哪个文件和用什么内容替换它是武器化的重要部分。此处我们仅提供一个简单的概念验证。
**触发条件。** 下一次任何包含目标 overlayfs 层哈希的 Pod 导入该目标模块时。CPython 打开 `.py` 文件,从页缓存中读取源文件字节,并编译被修改后的字节。重定向后的调度会解析到攻击者已控制且镜像中已存在的代码,或者通过在该层其他位置使用 Copy Fail 多次调用来注入的 payload。
### **1-1:共享基础层的已攻陷 Pod**
“将每个微服务沙箱化在其自己的容器中”这一模型假设,任何一个容器内的代码执行都被限制在该容器的镜像及其供应链范围内。共享的下层页缓存可能打破这一假设。这不仅允许攻陷 Pod 自身,还能影响同节点上从共享下层读取相同目标文件的相邻 Pod。目标可以是集群中最合法的工作负载:一个指标导出器、一个日志传送器、一个 CI 运行器、一个未受审计的调试 sidecar。关键在于它与一个加固的后端共享基础层(`debian:bookworm-slim`、`python:3.12-slim`)。
**演示。** Pod A 是已被攻陷的 Pod,镜像为 `python:3.12-slim`。Pod B 是同一节点上一个无关的、安全加固的后端,镜像为 `payments-api:v1`,构建自 `FROM python:3.12-slim`。镜像引用不同;Pod A 投毒的 Python 层也存在于 Pod B 的镜像栈中。Pod B 的部署导入了一个库,该库在初始化时引入了目标模块。
被攻陷的 Python 站点包 演示
一个在 Pod A 和 Pod B 外部运行的节点级磁盘扫描器,通过主机文件系统对文件进行哈希,看不到任何异常。针对镜像摘要的仓库扫描也看不到任何异常。一个运行时 EDR(端点检测与响应系统)若能在导入后对运行中的 `python3` 常驻页进行哈希,或监视 Pod B 内的 `execve` 和子进程,则是检测该入侵的最佳手段。
### **1-2:Pod 创建权限**
在这种场景下,攻击者对集群中的任何 Pod 都没有现有访问权限。但他们有能力在某些命名空间中运行 `create pods`。这在多租户集群、CI 运行器、构建代理和共享集群租户模式中很常见,因为许多 CI、构建和多租户服务账户被有意授予了此权限。攻击不依赖于层重叠的运气。在这里,如果攻击者能读取受害者 Pod 的规格,或以其他方式推断出受害者镜像和节点部署位置,就可以将相关的基础镜像拉取到攻击者 Pod 中,并通过 `nodeAffinity` 或 `nodeName` 请求将其调度在受害者所在的节点上。容器运行时的层哈希去重使得攻击者的 overlayfs 下层与受害者的下层是同一个主机 inode;页缓存写入随之生效。一个有趣的后果是,这种攻击能够跨越 RBAC 本应强制隔离的命名空间和租户边界。一个在自己命名空间中拥有 `pods/create` 权限的服务账户,对受害者工作负载没有直接权限,但可以通过继承受害者的基础镜像并落在同一节点上,来投毒另一个命名空间中的后端。
**子案例:DaemonSet 内的入侵。** 一个已被攻陷的 DaemonSet 是更高杠杆的利用路径。大多数生产环境的 DaemonSet 出于正当原因(CNI 代理、CSI 驱动、日志转发器、监控代理、安全代理)会挂载 `hostPath`。因此,与主机文件系统共享的页缓存直接位于攻击者的触手可及范围内。这意味着,利用相同的漏洞原语,侧向移动的目标集合现在包括了主机侧的二进制文件(`/usr/sbin/ipset`、`iptables`、`kubelet` 生成的助手程序),投毒这些文件可以导致主机 root 执行——如果主机稍后执行了受影响文件,并且补丁被正确武器化,那么下一次主机调用它们时就会触发。在 DaemonSet 中的代码执行实际上等同于 Pod 到主机的跳转,而无需经过场景 2 的机制。
## **场景 2:容器逃逸**
**威胁模型。** 相同的起始位置:无特权容器,代码执行,无特权能力。目标是获得主机上的 shell。
**共享的 inode。** 当 `runc` 修复CVE-2019-5736(https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/)时,最初的修复方法是在每次 `execve` 之前将 `runc` 二进制文件复制到 memfd,以防止从容器内部被覆盖。一个后续提交(https://github.com/opencontainers/runc/commit/16612d74de5f84977e50a9c8ead7f0e9e13b8628) 用主机 `runc` 的只读绑定挂载替换了复制操作,将其挂载到每个容器中,以利用跨派生子进程扇出的内核页缓存共享。当然,内核页缓存正是我们要覆盖的东西,这意味着该设计再次使主机 runc 映射暴露于页缓存写入原语之下。
**攻击链。** 与 Datadog 的Dirty Pipe 容器逃逸 PoC(https://www.datadoghq.com/blog/engineering/dirty-pipe-container-escape-poc/)形状相同,只是将写入原语换成了 Copy Fail。
**步骤 1:强制** `runc` **运行。** 当用户通过 exec 进入容器时,kubelet 通过 runc exec 进入已经在运行的容器实现,这正是我们需要的窗口。容器启动、重启和初始化步骤都不起作用:它们运行在全新的文件系统上,此时入口点尚未设置陷阱。Datadog 的 PoC 通过将 `/bin/sh` 覆盖为 `#!/proc/self/exe` 来设置陷阱。当 runc exec 执行 shell 时,内核解析 shebang 并重新执行 `/proc/self/exe`,而后者在 exec 过程中仍然指向 runc。这会在容器的 PID 命名空间中留下一个 runc 进程,存活足够长的时间以进行步骤 2。任何触发容器内进程的操作都足够,特别是当你运行 `kubectl exec` 进入容器、容器重启或初始化步骤时。为了使等待确定性,Datadog 的 PoC 将容器内的 `/bin/sh` 覆盖为 `#!/proc/self/exe`,这样下次任何人(或任何东西)在容器内执行 shell 时,就会调用 runc。
**步骤 2:定位 runc 的 PID。** 一旦 `runc` 出现在容器的 PID 命名空间中,扫描 `/proc`,寻找其 `/proc/<pid>/exe` 符号链接通过绑定挂载解析到主机 runc inode 的进程。
**步骤 3:通过** `/proc/<pid>/exe` **投毒。** 打开该文件描述符。对其运行 Copy Fail;页缓存写入会落在 runc 的第一页中,将它的 ELF 头部以及后续二进制内容替换为一个小的恶意 ELF。缓存的页面现在已被投毒,为下一次调用做好准备。
**步骤 4:等待下一次** `runc` **调用。** 主机上任何后续的 `runc` 调用都会映射这些缓存页,并以 root 身份执行修改后的代码。这包括管理员的 `kubectl exec`、下一个 Pod 启动、下一个探针等等。攻击者通常可以通过终止 Pod 来强制发生这种情况,从而触发重启。这个利用路径与 Dirty Pipe 非常相似。然而,Copy Fail 涵盖了从 2017 年引入该漏洞的提交(72548b093ee3)(https://github.com/torvalds/linux/commit/72548b093ee3)到 2026 年的修复(a664bf3d603d)(https://github.com/torvalds/linux/commit/a664bf3d603dc3bdcf9ae47cc21e0daec706d7a5)之间的每一个内核版本。
**PoC。** 从主机上下文获取的反向 shell,捕获在监听器上:
```
ubuntu@ip-172-26-6-67:~$ nc -l 1234 -v
Listening on 0.0.0.0 1234
Connection received on ec2-43-202-13-255.ap-northeast-2.compute.amazonaws.com 52450
[cwd] /run/containerd/io.containerd.runtime.v2.task/k8s.io/880c5f77aa39359e231f5ea709148f7914584c9986f8636b1201430f842e94c2
[listdir: cwd]
.
..
init.pid
log.json
runtime
options.json
bootstrap.json
shim-binary-path
log
config.json
work
rootfs
[listdir: /]
.bottlerocket
bin
boot
dev
etc
...
x86_64-bottlerocket-linux-gnu
```
从这个输出中可以读出两件事情。连接的对方是 ec2-43-202-13-255.ap-northeast-2.compute.amazonaws.com,一个 AWS 公共 DNS 名称,位于 `ap-northeast-2`,是一个 EKS 工作节点 EC2 实例。shell 的工作目录是 `/run/containerd/io.containerd.runtime.v2.task/k8s.io/880c5f77a
相似文章
Podman无根容器与Copy Fail漏洞
本文讨论了Copy Fail漏洞,这是一个影响Podman无根容器的安全漏洞。
CVE-2026-31431: Copy Fail
CVE-2026-31431(Copy Fail)是Linux内核中的一个本地提权漏洞,影响自2017年以来的所有主流发行版,允许非特权用户通过AF_ALG加密子系统对任何可读文件的页缓存进行确定性的4字节写入,从而获得root shell访问权限。
Copy Fail 2: Electric Boogaloo
Copy Fail 2 是一个针对 Linux 内核 xfrm 子系统中非特权本地权限提升(LPE)漏洞的概念验证利用程序,攻击者可利用该漏洞在现代发行版上获取 root 权限。
ipv6_frag_escape: Linux 本地提权 - 可靠的容器/沙盒逃逸
针对 Linux 本地提权和容器/沙盒逃逸的概念验证,利用内核中的 IPv6 分片漏洞,目标是 CentOS/RHEL 10。
Linux 内核 __ptrace_may_access() 函数的逻辑漏洞 (CVE-2026-46333)
Qualys 披露了 Linux 内核 __ptrace_may_access() 函数中的一个逻辑漏洞 (CVE-2026-46333),可导致本地权限提升和信息泄露。该漏洞自 2016 年起存在,影响多个发行版,Qualys 已开发出四种概念验证利用代码。