写点什么

RunC TOCTOU 逃逸 CVE-2021-30465 分析

发布于: 2 小时前
RunC TOCTOU逃逸CVE-2021-30465分析

背景

国外安全研究员 champtar[1]在日常使用中发现 Kubernetes tmpfs 挂载存在逃逸现象,研究后发现 runC 存在条件竞争漏洞,可以导致挂载逃逸[2]


关于条件竞争 TOCTOU 和一些 linux 文件基础知识可见这篇文章《初探文件路径条件竞争 - TOCTOU&CVE-2019-18276》[3]


CVE-2021-30465 在 Redteam 的研究者视角中比较鸡肋,因为需要 K8S 批量创建 POD 的权限。但在产品安全的视角恰恰相反,针对 Caas(Container as a service)类产品,用户/租户拥有批量创建 POD 权限,利用挂载逃逸可打破租户间隔离,同时读取 Host 层面某些敏感数据,危害性极大。

 RunC 简介

为了让容器生态更加开放,Linux 基金会发起 OCI(Open Container Initiative),目标是标准化容器格式和运行时[4],其中一个重要产物就是 CRI(Container Runtime Interface),抽象了容器运行时接口,使得上层调控容器更加便捷。containerd 和 runC 都是其中代表产物,从 dockerd 中再剥离出 containerd[5],向上提供 rpc 接口,再通过 containerd 去管理 runC。containerd 在初期也是直接对 runC 进行管理,但为了解决 containerd 进行升级等操作时会造成不可用的问题,containerd 再拆出 containerd-shim,独立对接 runC。containerd 从 Runtime、Distribution、Bundle 维度提供容器全生命周期的管理能力[6],runC 专注于 Runtime。



容器设备挂载相关基础知识

Namespace

Namespace 是 linux 控制系统资源的抽象层,将不同的进程放置入不同的 Namespace 将获得不同的资源视角,该项技术是容器实现的基础[7]

linux 提供 8 种不同的 Namespace 以提供不同维度的隔离能力,分别是:


1. Cgroup

2. IPC

3. Network

4. Mount

5. PID

6. Time

7. User

8. UTS


其中,Cgroup 和 Mount Namespace 是最常接触的,在容器挂载相关能力均通过 Mount Namespace 进行实现。Namespace 的使用主要通过 clone 和 unshare 两个方法实现,其中 clone 创建新进程时,标志位为 CLONE_NEW*将会创建新的 Namespace 并将子进程放入该 Namespace[8],unshare 方法将调用进程放入不同的 Namespace 中[9]

Mount Namespace

Linux 中有一个很核心的思想,那就是一切皆文件。在该思想下,Linux 通过挂载对不同设备中的文件进行管理。在 Linux 中,每一个空目录/文件都可以成为挂载点并设置相应的属性。在 Mount Namespace 下,处在当前 Namespace 中的进程只对当前 Namespace 中的挂载点可见,通过

/proc/[pid]/mounts 、/proc/[pid]/mountinfo 和/proc/[pid]/mountstats

提供不同维度的数据。每个 task(在 Linux 中,不论是进程还是线程,在内核的视角都是一个 task)都会指向一个 Namesapce(存放在 task→nsproxy 中)[10]


struct nsproxy {     atomic_t count;     struct uts_namespace *uts_ns;     struct ipc_namespace *ipc_ns;     struct mnt_namespace *mnt_ns;     struct pid_namespace *pid_ns;     struct net        *net_ns;   };
复制代码


但 Mount Namespace 的引入也带来了新的问题,由于 Mount Namespace 中的隔离性,当用户需要挂载一个新的磁盘使所有 Namespace 可见时,就需要在所有的 Namespace 中都进行一次挂载,很麻烦,于是 2.6.15 中引入了共享子树(Shared Subrees)。通过在不同挂载点设置不同的属性,使挂载事件在不同的维度(peer group[11])进行传播。目前支持以下四种传播类型,其中 MS_SHARED 和 MS_SLAVE 比较常见。


1. MS_SHARED

2. MS_PRIVATE

3. MS_SLAVE

4. MS_UNBINDABLE


在 MS_SLAVE 传播属性的挂载点下,父挂载点(Master)的传播事件可以接收,但子挂载点下(Slave)的挂载事件不再传播,容器的 Rootfs 挂载即为该种类型,也就是说在容器中挂载的挂载动作是不影响宿主机的,保证了容器隔离。


漏洞分析

RunC 漏洞挂载逻辑分析


checkout 到修复 commit(0ca91f44f1664da834bc61115a849b56d22f595f)[12]的上一个版本 commitc(01a56034f5ab0c1aa314377a499fe60a9c26b36)。

整体流程如下


RunC 通过命令 runc create + runc start 或 runc run 启动一个容器,runc create 主要分为两部分,一部分是准备容器进程的启动参数,与真正实施容器 runc init 进程进行交互,保证容器初始化顺利进行;另外一部分是执行克隆出的 runc init 进程,加入各种 namespace 并初始化容器进程的执行环境。本文以第二部分为切入点进行分析,从

libcontainer/standard_init_linux.go 的 linuxStandardInit.Init()开始,在其中调用 prepareRootfs,准备初始化 rootfs 并进行挂载。



在 prepareRootfs 中,调用 prepareRoot 设置初始挂载点,并设置挂载标志位为 unix.MS_SLAVE | unix.MS_REC ,其后使用 mountToRootfs 对 container.json 中配置的挂载进行操作。



prepareRoot 中设置容器根目录挂载标志位为 unix.MS_SLAVE | unix.MS_REC ,容器在初始的时候会通过镜像中的容器标准包(bundle)挂载根文件系统(BaseFS),在这里 runC 默认将挂载点(Propagation Type)设置为 slave。由于当前已经处于容器的 mount namespace 中,所以当前\  为容器根路径。rootfsParentMountPrivate 函数确保上一层的挂载点是 PRIVATE ,应该是出于防止逃逸的考虑。



在 mountToRootfs 中,针对不同的设备类型存在不同的处理逻辑。


 

在 tmpfs 的处理逻辑中,configs.EXT_COPYUP 默认为 1。


 

首先准备/tmp 目录,在 prepareTmp 函数中将这个挂载点设置为 MS_PRIVATE ,再创建 runctmpdir 路径,将目标路径复制到 tmpDir 中,最后将 dest 路径挂载到 tmpDir 中,且 Propagation Type 设置为 MS_MOVE 。对于 MS_MOVE ,官方说明[13]如下:


If mountflags contains the flag MS_MOVE (available since Linux 2.4.18), then move a subtree: source specifies an existing mount point and target specifies the new location to which that mount point is to be relocated. The move is atomic: at no point is the subtree unmounted.


当此时的 dest 为一个 symlink 时,subtree 将覆盖已存在挂载点。所以此处存在 TOCTOU(Time-of-check to time-of-use),在 SecureJoin 函数执行时,dest 为正常路径,当挂在发生时,dest 为 symlink,导致逃逸发生。

结论

RunC 为了防止在路径组合中的路径穿越漏洞,引入了 filepath-securejoin[14]作为符号链接过滤函数,但 r 在挂载时并未校验挂载的实际目的路径,从而导致存在 TOCTOU 条件竞争漏洞。


从 securejoin 的 Readme 中也可看出这一点。


 

之所以能够成功逃逸的另一原因在于 tmpfs[15]中为了实现 copy-up 功能[16]使用 MS_MOVE[17]作为挂载标志,根据 runC 作者的描述只有在 tmpfs 情况才能够逃逸[18]

补丁分析

在补丁中,可以看出在 tmpfs 的挂载逻辑中,增加了 doTmpfsCopyUp 函数。


 

在其中使用 WithProcfd 函数防止 TOCTOU 漏洞的发生,所有的 securejoin.SecureJoin 移入 WithProcfd 进行统一处理。


 

WithProcfd 中使用/proc/self/fd/ ,确保打开的文件是 securejoin.SecureJoin 后的文件。


 POC 分析

漏洞作者给出的 POC 中给出了一个很精妙的构造,利用了这个看似很难利用的条件竞争漏洞。


首先,创建两个公共 tmpfs 的挂载,名称为 test1、test2,在容器 A 中,将 test1 挂载到/test1 路径,test2 挂载到/test2 路径,同时将/test2/test2 指向/ 。在容器 B 中,将 test1 挂载到/test1 路径,test2 挂载到/test1/mntx 路径和/test1/zzz 路径。


在容器 A 启动后,将/test1/mnt-tmpx 指向 rootfs 路径,且交换 mnt 和 mnt-tmpx,且 rootfs/test2/test2 指向/(K8S 中,同一个 pod 下的 rootfs 在一个路径,形如/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir )。


所以当条件竞争挂载的时候,即容器 B 启动时,挂载 test2,

mount('/','rootfs/test1/zzz')  ,同时 MS_MOVE 标志位将原有该挂载点的 subtree 移至新挂载点下,造成逃逸发生。

总结

Linux 在引入 symlink 的时候并不存在安全风险,但随着时代的变迁(容器的引入),symlink 确实在一定程度上确实容易造成容器逃逸的发生。[19]Linux 在尝试在不同的角度去解决这个问题,但目前还没有很完全的能够解决此风险。这里不禁让人想引用 tk 的一句话:


安全意识要有时代背景。


作者认为伴随容器场景愈发复杂,安全研究的逐渐深入,非 Linux 内核漏洞导致的容器逃逸长期来看还会有一个增长的趋势。

参考资料

[1]  champtar

https://github.com/champtar

[2]runc mount destinations can be swapped via symlink-exchange to cause mounts outside the rootfs (CVE-2021-30465) 

https://blog.champtar.fr/runc-symlink-CVE-2021-30465/

[3] 《初探文件路径条件竞争 - TOCTOU&CVE-2019-18276》

http://whip1ash.cn/2021/06/16/toctou/

[4] About the Open Container Initiative

https://opencontainers.org/about/overview/

[5] What is the relationship between containerd, OCI and runc?

http://www.caict.ac.cn/kxyj/qwfb/ztbg/202010/t20201021_360375.htm

[6] Containerd Architecture

https://github.com/docker-archive/containerd/blob/master/design/architecture.md

[7] namespaces(7) — Linux manual page

https://man7.org/linux/man-pages/man7/namespaces.7.html

[8] clone(2) — Linux manual page

https://man7.org/linux/man-pages/man2/clone.2.html

[9] unshare(2) — Linux manual page

https://man7.org/linux/man-pages/man2/unshare.2.html

[10] Linux Namespace 分析——mnt namespace 的实现与应用

https://hustcat.github.io/namespace-implement-1/

[11]Mount namespaces and shared subtrees

https://lwn.net/Articles/689856/

[12] 0ca91f44f1664da834bc61115a849b56d22f595f

https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f

[13] mount(2) — Linux manual page

https://man7.org/linux/man-pages/man2/mount.2.html

[14] filepath-securejoin

https://github.com/cyphar/filepath-securejoin

[15]  tmpfs

https://en.wikipedia.org/wiki/Tmpfs

[16] Overlay Filesystem

https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html#non-directories

[17] mount(2) — Linux manual page

https://man7.org/linux/man-pages/man2/mount.2.html

[18]  rootfs: add mount destination validation

https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f

[19] Re: [PATCH 2/3] namei: implement AT_THIS_ROOT chroot-like path resolution

https://lwn.net/ml/linux-kernel/CAG48ez30WJhbsro2HOc_DR7V91M+hNFzBP5ogRMZaxbAORvqzg@mail.gmail.com/

用户头像

还未添加个人签名 2020.07.20 加入

还未添加个人简介

评论

发布
暂无评论
RunC TOCTOU逃逸CVE-2021-30465分析