Containerd CVE-2020–15257细节说明

Containerd是基于OCI规范实现的一款工业级标准的容器运行时。 Containerd在宿主机中管理容器生命周期,如容器镜像的传输和存储、容器的执行和管理、存储和网络等。 containerd-shim是用作容器运行的载体,实现容器生命周期管理, 其API以抽象命名空间Unix域套接字方式暴露,该套接字可通过根网络名称空间访问。 因此,一旦普通用户获得主机网络访问权限(通过启动主机网络模式的容器),则可以访问任一容器的API,并以此提权。

在主机网络名称空间中运行容器是不安全的:

  • 请勿使用docker run --net = host运行Docker容器
  • 请勿使用.spec.hostNetwork:true配置运行Kubernetes Pods

一、Containerd CVE-2020–15257

  1. 漏洞级别

该漏洞社区评分为5.2 分/10分(中等)的安全级别,需要具备一定的触发条件、攻击路径较长。

  1. 提权条件

如果不受信任的用户在平台上无法创建主机网络模式(hostnetwork)的容器,或者容器内的进程是以非root用户(UID 0)运行,则不会触发该漏洞,具体满足以下多个条件:

  • 容器使用主机网络hostnetwork部署,此时容器和主机共享网络命名空间;
  • 容器使用root用户(即UID 0);
  • containerd版本在 <=1.3.7
  1. 漏洞确认

对于在易受攻击的系统上运行容器的用户,可以通过禁止主机网络模式,或者通过确保此类容器以非零UID/GID运行来缓解此问题。用户可将containerd版本更新到最新版本。 此外,更新前创建并运行的容器仍会受到攻击,因此用户需要确保所有容器完全停止,然后在更新后重新启动。

对于不确定CVE-2020-15257是否会影响的用户,可以使用以下命令快速确定受影响的containerd版本创建的容器是否仍在运行。 如果有返回结果,则说明存在。

$ cat /proc/net/unix | grep 'containerd-shim' | grep '@'

  1. 特别说明

即使替换了补丁版本的containerd,使用主机网络也是不安全的。


二、 安全分析

2.1 代码定位
  • containerd/containerd
    • runtime/v1/shim/client/client.go: WithStart(), newCommand()
    • cmd/containerd-shim/main_unix.go: serve()
    • cmd/containerd-shim/shim_linux.go: newServer()
  • containerd/ttrpc (via vendor/github.com/containerd/ttrpc/unixcreds_linux.go)
    • unixcreds_linux.go: UnixSocketRequireSameUser()
2.2 漏洞细节

containerd是一个容器运行时的核心组件,其管理基于runc的容器,在Kubernetes中可通过Docker(dockershim)方式或CRI方式使用。Docker架构如下图所示。
containerd架构
Docker架构包含docker、containerd、 containerd-shim、runC等组件。

  • containerd是容器运行时,作为守护进程,containerd通过containerd-shim调用runc管理容器。
  • containerd作为守护进程,其对外暴露用于容器生命周期管理(如容器运行管理、镜像管理等)的gRPC接口。
  • containerd生成containerd-shim进程对容器的生命周期进行一对一的管理。

为了提供自己的gRPC(实际上是ttrpc,一种裁剪版gRPC协议)API,containered-shim监听Unix域套接字。 这些是Linux独有的Unix域套接字,其使用以空字节开头的长度前缀键,并且可以包含任意二进制序列。 它们在抽象Unix域套接字sun_path中嵌入了结尾的空字节,其可阻止常见的Unix工具(例如socat)与其连接。

  • @/containerd-shim///shim.sock\0
  • @/containerd-shim/.sock\0

containered-shim不仅具有绑定和侦听此类套接字的能力,它还支持从其父进程接收任意套接字文件描述符。 containerd通过此方法,先创建抽象的Unix套接字并对其进行监听,在containerd-shim进程启动后,可以使用该句柄进行初始化,接下来containerd-shim启动ttrpc服务。 containerd-shim使用标准的Unix域套接字功能来验证传入的连接是否具有与其相同的UID和EUID(通常为UID:0和EUID:0)。

containerd-shim所使用的抽象的Unix域套接字,是绑定在主机的网络命名空间上的。当一个恶意容器同样处于主机的网络命名空间中,该容器内的root用户,可以通过譬如netstat -xl或者/proc/net/unix来扫描,找到containerd-shim的套接字,然后链接containerd-shim的API以执行命令。

containerd-shim暴露了许多危险的API,可用于逃避容器和执行特权命令。在使用的containerd(-shim)的两个主要版本1.2.x和1.3.x中,暴露以下能力:

  • 任意文件读取
  • 任意文件追加
  • 任意文件写入
  • containerd-shim中的任意命令执行
  • 从runc config.json文件创建容器
  • 启动创建的容器

大多数用户实际上不受此CVE的影响。如果在未指定–user的情况下运行docker run --net = host,则会受到影响。如果Kubernetes用户使用containerd作为CRI运行时并使用.spec.hostNetwork:true配置运行pod且未设置.spec.securityContext.runAsUser,则受到影响。

prefix

该CVE修复了containerd的v1.4.3/v1.3.9版本,其将抽象套接字修改为/run/containerd下基于文件的普通UNIX套接字。

fix

2.3 问题容器

Docker执行以下命令:

$ docker ps -a --filter 'network=host'

Kubernetes执行以下命令:

$ kubectl get pods -A -o json | jq -c '.items[] | select(.spec.hostNetwork==true) |[.metadata.namespace, .metadata.name]'

2.4 是否不使用network就一劳永逸

并不是的。 因为除了容器外,还有很多程序使用了抽象套接字。 这些程序包括:

  • dbus
  • ibus
  • irqbalance
  • iscsid
  • iscsiuio
  • LXD
  • multipathd
  • X Window System
  • [historical] systemd before v212
  • [historical] Unity (desktop environment)
  • [historical] upstart

等等

要查看主机上是否使用了抽象套接字,可运行grep -ao '@.*' /proc/net/unix

1
2
3
4
5
6
$ grep -ao '@.*' /proc/net/unix ⏎
@/org/kernel/linux/storage/multipathd
@/tmp/dbus-ihrEYFlKyT
@/containerd-shim/moby/d0f4f5dd326d505f79e20ca891ad35516656353bc7974378237826b3456bff86/shim.sock
@ISCSIADM_ABSTRACT_NAMESPACE
@/containerd-shim/moby/d0f4f5dd326d505f79e20ca891ad35516656353bc7974378237826b3456bff86/shim.sock

实际上,其实关于containerd的CVE-2020-15257漏洞,一些开发人员和用户早已知晓,但其一直未被视作安全漏洞,因为使用主机网络名称空间并不安全,无论是否存在containerd套接字。 虽然containerd项目考虑到攻击的影响范围而更改了漏洞策略,但上述的软件应该不会将抽象套接字视作漏洞。


三、安全建议

在需要使用主机网络时,需要考虑以下安全策略

  • 以非root用户运行容器
  • AppArmor
  • SELinux等

Docker

可以使用端口映射方式: docker run -p
通信时执行以下命令:

script
1
2
3
4
5
$ docker inspect -f '{{.NetworkSettings.IPAddress}}' nginx ⏎
172.17.0.2
$ curl http://172.17.0.2
...
<title>Welcome to nginx!</title>

或者修改docker proxy

script
1
2
3
4
# cat <<EOF > /etc/docker/daemon.json ⏎
{"userland-proxy": false}
EOF
# systemctl restart docker

以及其他方案,如

  • AppArmor
  • SELinux等

Kubernetes

对于使用Kubernetes的用户,可以使用以下方式或特性

  • kubectl get pods -o wide获取IP进行访问
  • 内部DNS(CoreDNS)
  • kubectl port-forward
  • AppArmor
  • SELinux等
3.1 以非root用户运行容器

对于Docker,运行docker run --net=host --user 12345 --security-opt no-new-privileges。 确保选择与主机上现有用户帐户没有冲突的UID号。
无需指定no-new-privileges,但是建议禁止使用sudo之类的特权。

对于Kubernetes,指定Pod相关字段.spec.[]containers.securityContext:

script
1
2
3
4
5
6
hostNetwork: true
containers:
- name: foo
securityContext:
runAsUser: 12345
allowPrivilegeEscalation: false

对于普通用户使用1024以内端口,需要如下配置:

script
1
2
# echo 'net.ipv4.ip_unprivileged_port_start=0' > /etc/sysctl.d/99-user.conf ⏎
# sysctl --system
3.2 使用AppArmor

AppArmor是Linux安全模块,供多个发行版使用,包括Ubuntu,Debian,SUSE和Google COS。
以下AppArmor配置文件可用于禁止容器使用抽象套接字:

script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <tunables/global>
profile docker-no-abstract-socket flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
network,
capability,
file,
umount,
signal (receive) peer=unconfined,
signal (send,receive) peer=docker-no-abstract-socket,
deny @{PROC}/* w,
deny @{PROC}/{[^1-9],[^1-9][^0-9],[^1-9s][^0-9y][^0-9s],[^1-9][^0-9][^0-9][^0-9]*}/** w,
deny @{PROC}/sys/[^k]** w,
deny @{PROC}/sys/kernel/{?,??,[^s][^h][^m]**} w,
deny @{PROC}/sysrq-trigger rwklx,
deny @{PROC}/kcore rwklx,
deny mount,
deny /sys/[^f]*/** wklx,
deny /sys/f[^s]*/** wklx,
deny /sys/fs/[^c]*/** wklx,
deny /sys/fs/c[^g]*/** wklx,
deny /sys/fs/cg[^r]*/** wklx,
deny /sys/firmware/** rwklx,
deny /sys/kernel/security/** rwklx,
ptrace (trace,read,tracedby,readby) peer=docker-no-abstract-socket,

# Only the following line is related to abstract sockets.
# Other lines are from "docker-default" profile
# (https://github.com/moby/moby/pull/39923)
deny unix addr=@**,
}
# To load the profile, run `sudo apparmor_parser -r docker-no-abstract-socket`

可以按如下所示将此配置文件应用于Docker容器:

script
1
2
$ sudo apparmor_parser -r docker-no-abstract-socket
$ docker run --net=host --security-opt apparmor=docker-no-abstract-socket ...

关于在Kubernetes中如何使用AppArmor特性

3.3 使用SELinux

RHEL/CentOS和Fedora的SELinux策略,用于保护主机上的抽象套接字:

script
1
2
3
4
5
6
7
8
9
$ getenforce
Enforcing
$ socat abstract-listen:foo,fork stdio &
$ sudo podman run -it --net=host alpine
/ # cat /proc/self/attr/current
system_u:system_r:container_t:s0:c83,c1019
/ # apk add -q socat
/ # echo test | socat stdio abstract-connect:foo
2020/11/27 15:42:08 socat[7] E connect(5, AF=1 "\0foo", 6): Permission denied

默认情况下,SELinux已启用Podman和OpenShift。 要为Docker启用SELinux,请按以下方式配置/etc/docker/daemon.json

script
1
2
3
4
# cat <<EOF > /etc/docker/daemon.json
{"selinux-enabled": true}
EOF
# systemctl restart docker

四、参考资料