理解Docker容器逃逸 - Trail of Bits博客
Trail of Bits近期完成了对Kubernetes的安全评估,包括其与Docker的交互。Felix Wilhelm发布的概念验证(PoC)“容器逃逸”推文引发了我们的兴趣,因为我们曾进行类似研究,并好奇此PoC对Kubernetes的影响。
Felix的推文展示了一个在带有
--privileged标志的Docker容器内,在主机上启动进程的漏洞利用。该PoC通过滥用Linux cgroup v1的“释放时通知”功能实现。
以下是PoC版本,用于在主机上启动ps命令:
|
|
--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功能在主机上执行。
|
|
分解概念验证
既然我们理解了使用此技术的条件并优化了概念验证漏洞利用,让我们逐行解析其工作原理。
要触发此漏洞利用,我们需要一个可以创建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的创建及其目录列表:
|
|
接下来,我们通过在notify_on_release文件中写入1来启用“x”cgroup释放时的cgroup通知。我们还通过将主机上的/cmd脚本路径写入release_agent文件来设置RDMA cgroup释放代理以执行/cmd脚本——我们稍后将在容器中创建此脚本。为此,我们将从/etc/mtab文件中获取容器在主机上的路径。
我们在容器中添加或修改的文件存在于主机上,并且可以从两个世界修改它们:容器中的路径及其在主机上的路径。
这些操作如下所示:
|
|
注意/cmd脚本的路径,我们将在主机上创建它:
|
|
现在,我们创建/cmd脚本,使其执行ps aux命令并将输出保存到容器中的/output,通过指定主机上输出文件的完整路径。最后,我们还打印/cmd脚本以查看其内容:
|
|
最后,我们可以通过在“x”子cgroup内生成一个立即结束的进程来执行攻击。通过创建/bin/sh进程并将其PID写入“x”子cgroup目录中的cgroup.procs文件,主机上的脚本将在/bin/sh退出后执行。然后在主机上执行的ps aux输出保存到容器内的/output文件:
|
|
安全使用容器
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主题生成。