捕风集

Back

0x00 序言#

在xint团队公开Copy Fail漏洞本地利用的技术细节的两周之后,5月19日,该团队进一步公开了Copy Fail漏洞在容器逃逸方面的应用,主要目标为Kubernetes容器。本文在上篇介绍了Copy Fail本地利用原理的基础上,进一步介绍这一漏洞在容器环境下的利用。

0x01 前置知识#

  1. inodei_mappinginode是Linux中文件系统用于管理文件的数据结构,记录对应文件的属性和存储的磁盘块等信息。i_mappingi_node结构体中的一个指针,指向结构体address_space,该结构体用于管理页面缓存。
  2. filef_mapping:进程通过file结构体维护打开的文件。f_mappingfile结构体中的一个指针,一般与inode->i_mapping一致。因此,如果两个文件描述符具有相同的f_mapping,则它们具有相同的页面缓存。
  3. overlay文件系统:一种叠加式的文件系统,通过多个不同层级的文件系统叠加形成合并的文件系统,目前广泛用于云原生技术中。由于云原生场景下Copy Fail的利用使用了较多Overlayfs的特性。因此在0x02节中加以介绍。
  4. workload:工作负载,指云原生系统为了完成业务目标而正在运行的“实际任务”或“应用程序本身”。对于K8s而言,主要分为以下几类:
    • 无状态workload:以最常见的Deployment为代表,每个实例之间没有区别,无需记录上下文信息。如微服务接口、前端界面等。
    • 有状态workload:以StatefulSet为代表,实例独一无二,具备独立的存储和网络,如数据库实例。
    • pod守护workload:以DaemonSet为代表,不负责具体的业务实现,负责针对节点物理基础设施本身的任务,如日志收集、节点状态监控、网络服务和策略配置等,每个节点只会运行一个pod。需要注意的是,由于DaemonSet的任务与物理设备关联较大,因此常通过hostPath挂载敏感目录。
    • 定时任务workload:以JobCronJob为代表,定时任务,执行完自动退出。

0x02 Overlay Filesystem简介#

Overlay Filesystem主要分为四个目录:

  • lowerdir:底层目录,提供整个容器的基础数据,权限为只读。可以由多个目录构成,上级目录中的同名文件会屏蔽下级目录中的文件,在容器中作为镜像层。
  • upperdir:上层目录,提供数据修改功能,权限为读写。在容器中作为容器层。
  • workdir:工作目录,对用户和进程透明,可读写,初始为空,用于保证合并视图的一致性和完整性。
  • merged:合并视图,由内核通过lowerdirupperdir合并而成,为用户直接所见的容器根目录。

为节省和复用存储空间,基础镜像所在的lowerdir是由多个只读的目录组成的,多个不同的容器可以共享相同的lowerdir。对于读取操作,overlayfs会从上到下依次尝试读取,直到读到匹配文件名的文件为止。因此,上层会屏蔽下层的同名文件。对于对lowerdir中文件的修改操作,overlayfs采用写时复制(CoW, Copy on Write)的方式进行处理。即系统首先调用copy-up,复制一个新的文件到pod的upperdir并在新文件中修改。对于删除操作,overlayfs会在upperdir中创建whiteout文件,设备号为0/0,在Merged视图中会隐藏下层同名文件,实现删除的效果。

需要注意的是,overlayfs借助CoW实现在镜像层只读前提下的修改操作,从而保持下层文件的共享。但一旦攻击者通过某种手法在不触发CoW的情况下修改了文件,就会造成对共享文件的投毒,影响同节点上的其它pod甚至宿主机自身。

0x03 攻击场景分析#

当Copy Fail在容器中触发时,由于其修改了目标页面缓存而非目标页面本身,因此不会触发overlayfs的copy-up操作。与镜像层在各不同pod之间共享类似,不同pod间相同的共享文件所使用的也是一样的页面缓存。因此,通过Copy Fail写入的恶意shellcode会进入目标共享文件的页面缓存中,当其它pod后续读取这些文件时,所读取到的就是被投毒的文件。

对于检测而言,由于没有文件落地和修改,磁盘上的本地文件没有发生变化,计算文件散列值等操作无法检测到攻击。基于上述分析,我们进一步分析Copy Fail在两个威胁模型下的利用。

1. 跨容器投毒#

这一威胁模型下,攻击者无需具有特权,唯一的前置条件为已控制某个pod或具备create pods权限。这些前提在云原生架构中非常常见,如在多租户场景下,租户常具有在给定的命名空间下操作和创建pod的权限,在CI/CD中,自动化托管工具也常具有构建pod的能力。因此,前置条件的达成对于攻击者而言具有较强的可行性。此外,如果攻击者具有create pods权限,那么即可通过nodeAffinitynodeName属性精确指定目标pod,使其所创建的pod与目标pod位于同一个物理节点上。

达成前置条件后,攻击者可以任意选择一个共享范围较广的文件进行投毒,如在Python的site-packages/下寻找常用库,或选择glibc等动态库。选定目标后,攻击者可在pod内通过Copy Fail对目标文件的页面缓存投毒,而不会触发CoW。当其它共享该文件的pod引入或运行该文件时,控制流就会执行shellcode。

由于DaemonSet相对于普通pod具有更高权限,大多数情况下还挂载了主机的敏感目录。在这种情况下,通过跨容器投毒直接能够实现容器逃逸,而无需场景2中的方法。

2. 容器逃逸#

该场景下攻击者的能力与前述场景一致。逃逸的出发点来源于对CVE-2019-5736的修复方式。CVE-2019-5736的核心insight在于,在某些场景下,需要runC在容器内运行,执行用户定义的二进制文件。runC的实现方式是创建一个runC init的子进程,调用execve,用用户提供的二进制文件覆盖自身。而攻击者能够使用/proc/self/exe使其用宿主机的runC覆盖自身,从而在容器内获得对宿主机runC的控制,写入恶意代码后重新等待runC执行即可实现逃逸。

有经验的读者可能会发现上述利用过程中的问题。首先,当runC在容器内运行的时候,runC文件处于运行时,是无法对其写入的,Linux会报错ETXTBSY。PoC中首先获得只读的文件描述符,然后循环尝试以写入方式打开。当runC进程结束时即可打开文件并写入。其次,为什么不选择直接覆盖/proc/self/exe而还要让runC运行一次自身?其原因在于runC init进程有non-dumpable标志,其它进程无法解引用,而execve会去除这个标志。以上本属于CVE-2019-5736自身的tricks,但为方便理解还是稍提一嘴。

具体的漏洞利用过程分为以下四个步骤:

  1. 强制runC循环运行。首先,将/bin/sh覆盖为#!/proc/self/exe,使runC在执行shell时重新执行runC,保证runC进程在容器内的命名空间中存活足够长时间。
  2. 找到runC的pid。通过遍历/proc/<pid>/exe找到runC的pid。
  3. 投毒。打开/proc/<runc_pid>/exe,利用Copy Fail对runC的页面缓存进行投毒,将其修改为恶意ELF文件。
  4. 等待下一次runC的执行。在页面缓存失效前,宿主机上的任何对runC的调用都会引用被投毒的页面缓存,从而实现shellcode的执行。

0x04 小结#

在Copy Fail本地利用的基础上,衍生出上述两种场景的利用方式是比较显然的。利用Copy Fail针对的是页面缓存的特性,容器级虚拟化的内核共享特性使其失去了隔离攻击的能力,而拥有独立内核的虚拟机则能够抵抗Copy Fail的逃逸,这又一次体现出安全与性能之间的权衡取舍。

0x05 参考文档#

  1. Copy Fail: From Pod to Host.
  2. Overlay Filesystem - The Linux Kernel documentation
  3. DaemonSet | Kubernetes(K8s) 容器编排系统
  4. Breaking out of Docker via runC – Explaining CVE-2019-5736
CVE-2026-31431 Copy Fail浅析(下):容器逃逸
https://centrix.top/blog/cve-2026-31431-copy-fail2
Author Centrix
Published at May 24, 2026
Comment seems to stuck. Try to refresh?✨