前言
虽然漏洞的利用难度比较高,但是个人感觉还是很有意思的一个洞,值得记录一下。不过分析起来可能会像是一篇译文23333,欢迎直接阅读原文。
漏洞利用
1、首先需要创建多个容器,一个正常启动的容器c1,以及多个无法正常启动(即image为donotexists.com/do/not:exist
)的容器c2-c20
以及两个volume数据卷teset1和test2,分别挂载在各个容器中
1 | apiVersion: v1 |
2、然后准备 一个race.c,并编译成race二进制文件gcc race.c -O3 -o race
1 |
|
3、等待c1容器正常启动之后,将race
copy至c1中
1 | kubectl cp race -c c1 attack:/test1/ |
4、并且在c1中生成 /test2/test2
链接文件,指向根目录/
(这里软链接的文件名务必和数据卷名字相同)
1 | kubectl exec -ti pod/attack -c c1 -- bash |
5、然后在c1容器中启动race
二进制
1 | cd test1 |
这里的作用就是起了四个进程,创建mnt-tmpX
软链接,指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
,然后通过系统调用renameat2
不断交换mntX
和mnt-tmpX
两个文件。
6、然后将原先c2-c20的容器镜像设置回一个正常的容器镜像即可(即让c2-c20容器正常启动)
1 | for c in {2..20}; do |
此时能看到attack这个pod中的容器全部正常启动
7、然后会有一部分容器的/test1/zzz
会被指向宿主机的根目录,至此逃逸成功
1 | for c in {2..20}; do |
漏洞分析
可以看出这应该是一个和条件竞争相关的漏洞,因此我们可以简化一下这个流程,来讨论一下逃逸成功的情况。
首先要了解一点,runc在挂载 volumes 时是不允许将软链接挂载至容器中的,因为runc会跟随软链接指向的地址,将宿主机上的目录挂载至容器中。
因此runc会经过一个 securejoin.SecureJoinVFS()
的函数,先对要挂载的目录进行check,然后再进行mount操作。在这期间就会形成一个先后时间差,从而产生条件竞争。从而可能会发生跟随软链接的行为,将宿主机上的目录挂载至容器中,从而产生逃逸。
知道这一点之后,再来看下POC是如何利用这一点的。
首先要确保攻击container(c1)和恶意创建的container都挂载了相同的volumes(test1和test2)
1、c1在test1数据卷下生成一个mntX
以及一个指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
的mnt-tmpX
软链接,然后不断交换mntX
和mnt-tmpX
。并且创建/test2/test2
指向根目录。
这里的$MY_POD_UID
虽然是在启动时注入的,但实际上可以通过/proc/self/mountinfo
来获取
以及这个/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
实际上就是宿主机上当前pod的数据卷目录
2、然后当其余容器正常启动时,就会先去挂载test1数据卷到/test1
目录下,然后挂载test2至/test1/mntX
中
由于共享一个数据卷的原因,c1就会不断更换当前容器的/test/mntX
和/test1/mnt-tmpX
。
因而在test2挂载至/test1/mntX
时,一开始securejoin.SecureJoinVFS
检查时是一个正常的文件,于是通过了检测,进行mount操作。
但是在mount操作时/test1/mntX
被更换成了一个指向/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
的软链接。
于是在宿主机上本来是将进行如下操作
1 | mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mntX") |
跟随软链接之后就变成了
1 | mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/") |
于是相当于test2数据卷覆盖了整个/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
目录,因此再挂载test2数据卷到/test1/zzz
目录时,就会进行如下操作
1 | mount("/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/zzz") |
由于之前c1容器创建了/test2/test2
指向根目录,因此这里的/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2
其实就是一个指向当前宿主机根目录的软链接,于是以上操作就变成了。(这也是为什么要创建和数据卷同名的软链接文件)
1 | mount("/", "/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/zzz") |
从而将宿主机根目录挂载到了容器的/test1/zzz
中,实现了容器逃逸。
最后
感觉还是很有意思的一个洞,很佩服这个黑客的脑洞,以及直接将Google的bounty捐了XD(respect)。
个人认为该漏洞最重要的一步应该莫过于将test2
这个数据卷挂载到了一个软链接指定的任意宿主机目录,当然这里选择挂载到了pod数据卷目录。因此别的container挂载数据卷时就会去这个恶意的数据卷目录里去寻找对应目录,然后再利用软链接将宿主机根目录挂载到了容器中,从而实现容器逃逸。
至于利用的话,难点在于需要有容器的发布、挂载和exec权限,以及和恶意容器拥有两个共享的数据卷。
References
http://blog.champtar.fr/runc-symlink-CVE-2021-30465/
https://github.com/opencontainers/runc/security/advisories/GHSA-c3xm-pvg7-gh7r
https://man7.org/linux/man-pages/man2/rename.2.html