版权声明:本文为「星云实验室 绿盟科技研究通讯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://mp.weixin.qq.com/s/q4xJtlO6iFpHQginGvVBDQ
CVE-2020-2025
该漏洞也属于权限控制问题——在存在漏洞的环境中,虚拟机镜像并未以只读模式挂载。因此,虚拟机能够对硬盘进行修改,并将修改持久化到虚拟机镜像中。这样一来,后续所有新虚拟机都将从修改后的镜像创建了。
我们来验证一下。思路是,在之前CVE-2020-2023的基础上,先启动一个容器,使用debugfs向虚拟机硬盘中写入一个flag.txt文件,内容为hello, kata,然后销毁该容器,再次创建一个新容器,在其中使用debugfs查看文件系统是否存在上述文件,以判断虚拟机镜像是否被改写。具体的过程如下:
1 | docker run --rm -it ubuntu /bin/bash |
可以看到,虚拟机镜像确实被改写了。
CVE-2020-2026
CVE-2020-2026属于非常典型的一类漏洞——符号链接处理不当引起的安全问题[25]。我们来抽丝剥茧,一步步分析这个漏洞。
在「背景知识」部分,我们已经介绍了Kata Containers的基本组件,下面是Kata Containers执行OCI命令create时组件间的交互时序图 [26]:
其中,virtcontainers曾经是一个独立的项目,现在已经成为kata-runtime的一部分,它为构建硬件虚拟化的容器运行时提供了一套Go语言库。除此以外,上图涉及到的其他组件我们都介绍过了。
可以看到,Docker引擎向kata-runtime下发create指令,然后,kata-runtime通过调用virtcontainers的CreateSandbox来启动具体的容器创建过程。接着,virtcontainers承担起主要职责,调用Hypervisor提供的服务去创建网络、启动虚拟机。
我们重点关注virtcontainers向agent发起的CreateSandbox调用,从这里开始,virtcontainers与agent连续两次请求响应,是容器创建过程中最核心的部分,也是CVE-2020-2026漏洞存在的地方:
1 | virtcontainers --- CreateSandbox ---> agent |
这里的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调用:
1 | storages := setupStorages(sandbox) |
可以看到,其中带有SandboxId和Storages参数。其中,Storages的值来自setupStorages函数,这个函数用于配置共享目录的存储驱动、文件系统类型和挂载点等。Storages内的元素定义如下(setupStorages函数):
1 | sharedVolume := &grpc.Storage{ |
其中,kataGuestSharedDir函数会返回共享目录在虚拟机内部的路径,也就是MountPoint的值:/run/kata-containers/shared/containers/。
OK,切换到kata-agent侧。当它收到gRPC调用请求后,内部的CreateSandbox函数开始执行(位于agent/grpc.go)。具体如下(我们省略了内核模块加载、命名空间创建等代码逻辑):
1 | func (a *agentGRPC) CreateSandbox(ctx context.Context, req *pb.CreateSandboxRequest) (*gpb.Empty, error) { |
可以看到,在收到请求后,kata-agent会调用addStorages函数去根据kata-runtime的指令挂载共享目录,经过深入,该函数最终会调用mountStorage函数执行挂载操作:
1 | // mountStorage performs the mount described by the storage structure. |
这里的MountPoint即是来自kata-runtime的/run/kata-containers/shared/containers/。至此,宿主机与虚拟机的共享目录已经挂载到了虚拟机内。
最后,CreateSandbox执行完成,kata-runtime收到回复。
那么,kata-runtime什么时候会向共享目录中挂载呢?如下图所示,发送完CreateSandobx请求后,kata-runtme在bindMountContainerRootfs中开始挂载容器根文件系统:
代码如下:
1 | func bindMountContainerRootfs(ctx context.Context, sharedDir, sandboxID, cID, cRootFs string, readonly bool) error { |
其中,rootfsDest是宿主机上共享目录中容器根文件系统的位置。它的形式是/run/kata-containers/shared/sandboxes/sandbox_id/container_id/rootfs,其中sandbox_id与container_id分别是Sandbox和容器的ID。如前所述,对于只运行一个容器的情况来说,这两个ID是一致的;cRootFs是根文件系统在虚拟机内部共享目录中的挂载位置,形式为/run/kata-containers/shared/containers/sandbox_id/rootfs。
在函数的末尾,bindMount函数执行实际的绑定挂载任务:
1 | func bindMount(ctx context.Context, source, destination string, readonly bool) error { |
重点来了!该函数会对虚拟机内部的挂载路径做符号链接解析。
符号链接解析是在宿主机上进行的,但是实际的路径位于虚拟机内。如果虚拟机由于某种原因被攻击者控制,那么攻击者就能够在挂载路径上创建一个符号链接,kata-runtime将把容器根文件系统挂载到该符号链接指向的宿主机上的其他位置!
举例来说,假如虚拟机内部的kata-agent被攻击者替换为恶意程序,该恶意agent在收到CreateSandbox请求后,根据拿到的Sandbox ID在/run/kata-containers/shared/containers/sandbox_id/创建一个名为rootfs的符号链接,指向/tmp/xxx目录,那么之后kata-runtime在进行绑定挂载时,就会将容器根文件系统挂载到宿主机上的/tmp/xxx目录下。在许多云场景下,容器镜像是攻击者可控的, 因此——他够将特定文件放在宿主机上的特定位置,从而实现虚拟机逃逸。
第一眼看到CVE-2020-2026,也许有的朋友会觉得不太好利用,攻击者不是在容器里么?如何跑到虚拟机里?
是的,一般情况下的确比较困难,但是一旦与CVE-2020-2023、CVE-2020-2025结合起来,就有可能了。