细说Containerd CVE-2020–15257
周日有空,zouyee带各位看看11月末CVE-2020–15257的安全漏洞。Containerd是基于OCI规范实现的一款工业级标准的容器运行时。Containerd在宿主机中管理容器生命周期,如容器镜像的传输和存储、容器的执行和管理、存储和网络等。containerd-shim是用作容器运行的载体,实现容器生命周期管理, 其API以抽象命名空间Unix域套接字方式暴露,该套接字可通过根网络名称空间访问。因此,一旦普通用户获得主机网络访问权限(通过启动主机网络模式的容器),则可以访问任一容器的API,并以此提权。例如生命周期管理,高级网络,资源绑定,状态抽象以及这些抽象概念多年来的变化。
在主机网络名称空间中运行容器是不安全的:
1)请勿使用docker run --net = host运行Docker容器
2)请勿使用.spec.hostNetwork:true配置运行Kubernetes Pods
Containerd CVE-2020–15257
漏洞级别
该漏洞社区评分为5.2 分/10分(中等)的安全级别,需要具备一定的触发条件、攻击路径较长。
提权条件
如果不受信任的用户在平台上无法创建主机网络模式(hostnetwork)的容器,或者容器内的进程是以非root用户(UID 0)运行,则不会触发该漏洞,具体满足以下多个条件:
容器使用主机网络
hostnetwork
部署,此时容器和主机共享网络命名空间;容器使用root用户(即UID 0);
containerd
版本在 <=1.3.7
漏洞确认
对于在易受攻击的系统上运行容器的用户,可以通过禁止主机网络模式,或者通过确保此类容器以非零UID/GID运行来缓解此问题。用户可将containerd
版本更新到最新版本。此外,更新前创建并运行的容器仍会受到攻击,因此用户需要确保所有容器完全停止,然后在更新后重新启动。
对于不确定CVE-2020-15257
是否会影响的用户,可以使用以下命令快速确定受影响的containerd
版本创建的容器是否仍在运行。如果有返回结果,则说明存在。
$ cat /proc/net/unix | grep 'containerd-shim' | grep '@'
特别说明
即使替换了补丁版本的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架构如下图所示。
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
,则受到影响。
该CVE修复了containerd
的v1.4.3/v1.3.9版本,其将抽象套接字修改为/run/containerd
下基于文件的普通UNIX套接字。
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
通信时执行以下命令:
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
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 run --net=host --user 12345 --security-opt no-new-privileges时,
确保选择与主机上现有用户帐户没有冲突的UID号。无需指定no-new-privileges
,但是建议禁止使用sudo之类的特权。
对于Kubernetes,指定Pod相关字段.spec.[]containers.securityContext
:
1 2 3 4 5 6 | hostNetwork: true containers: - name: foo securityContext: runAsUser: 12345 allowPrivilegeEscalation: false |
对于普通用户使用1024以内端口,需要如下配置:
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配置文件可用于禁止容器使用抽象套接字:
#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容器:
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策略,用于保护主机上的抽象套接字:
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
:
1 2 3 4 | # cat <<EOF > /etc/docker/daemon.json {"selinux-enabled": true} EOF # systemctl restart docker |
四、参考资料
technical advisory
dont-use-host-network-namespace