容器逃逸技术概览
近年来,容器技术持续升温,全球范围内各行各业都在这一轻量级虚拟化方案上进行着积极而富有成效的探索,使其能够迅速落地并赋能产业,大大提高了资源利用效率和生产力。随着容器化的重要甚至核心业务越来越多,容器安全的重要性也在不断提高。作为一项依然处于发展阶段的新技术,容器的安全性在不断地提高,也在不断地受到挑战。与其他虚拟化技术类似,在其面临的所有安全问题当中,「逃逸问题」最为严重——它直接影响到了承载容器的底层基础设施的保密性、完整性和可用性。
从攻防的角度来看,「容器逃逸」是一个很大的话题,它至少涉及了攻方视角下的成因、过程和结果,以及守方视角下的检测与防御。本系列文章将对这一话题作研究和讨论,尝试把日益复杂的攻防技术与态势条理化展开,希望能够带来有益思考。
本文将主要梳理并介绍已有的容器逃逸技术,帮助大家对这一主题建立基本了解。
前言
善守者,藏于九地之下;善攻者,动于九天之上。
我们谈「容器逃逸」,搜索引擎中输入这四个字也能找到为数不多的解读和研究。那么什么是「容器逃逸」?我们如何定义「容器逃逸」?对这个问题的深入理解有助于研究的展开。开宗明义,本文将「容器逃逸」限定在一个较为狭窄的范围,并围绕此展开讨论:
「容器逃逸」指这样的一种过程和结果:首先,攻击者通过劫持容器化业务逻辑,或直接控制(CaaS等合法获得容器控制权的场景)等方式,已经获得了容器内某种权限下的命令执行能力;攻击者利用这种命令执行能力,借助一些手段进一步获得该容器所在直接宿主机(经常见到“物理机运行虚拟机,虚拟机再运行容器”的场景,该场景下的直接宿主机指容器外层的虚拟机)上某种权限下的命令执行能力。
注意以下几点:
基于计算机科学领域层式思想及分类讨论的原则,我们定义「直接宿主机」概念,避免在容器逃逸问题内引入虚拟机逃逸问题;
基于上述定义,从渗透测试的角度来看,本文理解的容器逃逸或许更趋向于归入后渗透阶段;
同样基于分类讨论的原则,我们仅仅讨论某种技术的可行性,不刻意涉及隐藏与反隐藏,检测与反检测等问题;
将最终结果确定为获得直接宿主机上的命令执行能力,而不包括宿主机文件或内存读写能力(或者说,我们认为这些是通往最终命令执行能力的手段);
一些特殊的漏洞利用方式,如在软件供应链阶段能够触发漏洞的恶意镜像、在容器内构造的恶意符号链接、在容器内劫持动态链接库等,其本质上还是攻击者获得了容器内某种权限下的命令执行能力,即使这种能力可能是间接的。
这些「注意」的每一点延伸开来,都能够获得很有意思的见解。例如,结合第4点我们可以想到,在权限持久化攻防博弈的进程中,人们逐渐积累了许许多多Linux场景下建立后门的方法。其中一大经典模式是向特定文件中写入绑定shell或反弹shell语句,五花八门,不胜枚举。那么如果容器挂载了宿主机的某些文件或目录,将挂载列表和前述用于建立后门写入shell的文件、目录列表取交集,是不是就可以得到容器逃逸的可能途径呢(例如后文4.2节介绍的情况)?进一步说,用于防御和检测后门的思路和技术,经过改进和移植是否也能覆盖掉某种类型的容器逃逸问题呢?
带着这些问题和理解,我们开始探索之旅。后文的组织结构如下:
介绍容器环境检测技术
介绍危险配置导致的容器逃逸
介绍危险挂载导致的容器逃逸
介绍相关程序漏洞导致的容器逃逸
介绍内核漏洞导致的容器逃逸
容器环境探测检查
为了使逻辑链条更加完整,我们首先介绍一些确定运行环境是容器环境的检查方法。
为什么要探测容器环境?
未知攻焉知防。从攻击者的视角来看,好的攻击不是使用Armitage之类的工具把ExP乱投一遍(当然,Armitage还有别的精细化功能),而是因地制宜,对症下药。放到本文的语境下,我们要清楚目标环境是不是容器,然后才谈得上容器逃逸。除此之外,笔者认为探查容器环境至少还有以下收益:
建立对目标环境的感性认识:如果判断目标环境是一个容器环境,那么攻击者就能够为后续工作做更多准备。例如:
容器环境下许多工具通常是不存在的,例如ping、ipconfig等网络相关工具及gcc等构建工具。
成熟业务往往不会直接在容器这一层次进行部署和控制,而是采用诸如Kubernetes之类的编排系统进行统一编排和调度,Kubernetes中每个Pod是一台逻辑主机,目标容器所在的Pod内很可能存在其他容器。
评估、发现目标环境潜在的脆弱点:如果判断目标环境是一个容器环境,那么攻击者就能够进一步利用容器相关背景知识去有针对性地查找漏洞。例如:
容器使用了什么镜像?镜像是否包含漏洞?
既然有容器,那么Docker甚至Kubernetes的服务端口会不会暴露在外面?
如果能够通过此容器发现宿主机内核的问题,那么这一问题是否会同时影响该宿主机上部署的其他容器?
进一步讲,如果发现当前宿主机内核的问题,又发现目标是类似Kubernetes的集群环境,那么是否会有更多的宿主机节点存在相同问题?如何在这样的环境中横向移动?
如何探测检查容器环境?
James Otten为Metasploit编写了checkcontainer模块(在Metasploit中,与之类似的还有checkvm模块,感兴趣的读者可以自行了解)。该模块的检查是简单而直观的,仅仅进行了三处检查:
检查/.dockerenv文件是否存在;
检查/proc/1/cgroup内是否包含"docker"等字符串;
检查是否存在container环境变量。
关于容器环境检查,网络上已经存在一些讨论的文章[2][3]。另外,攻防的本质是对抗,因此在几乎所有「检测」的领域,人们都会提出「反检测」和「反-反检测」的话题。如前所述,我们暂不深入。至少,在笔者部署的构建于2019年11月13日,版本为19.03.5的Docker测试环境中,该检测还是有效的:
Cgroup自不必提,.dockerenv从Docker发布开始,经过不断的迭代后依然存在着,这倒蛮有意思。有人提出了相关的问题,其中还提到.dockerinit文件,只不过该文件在较新的Docker版本下已经不存在了。
危险配置导致的容器逃逸
安全往往在痛定思痛时得到发展。在这些年的迭代中,容器社区一直在努力将「纵深防御」、「最小权限」等理念和原则落地。例如,Docker已经将容器运行时的Capabilities黑名单机制改为如今的默认禁止所有Capabilities,再以白名单方式赋予容器运行所需的最小权限。截止本文成稿时,Docker默认赋予容器近40项权限中的14项:
func DefaultCapabilities() []string {
return []string{
“CAP_CHOWN”,
“CAP_DAC_OVERRIDE”,
“CAP_FSETID”,
“CAP_FOWNER”,
“CAP_MKNOD”,
“CAP_NET_RAW”,
“CAP_SETGID”,
“CAP_SETUID”,
“CAP_SETFCAP”,
“CAP_SETPCAP”,
“CAP_NET_BIND_SERVICE”,
“CAP_SYS_CHROOT”,
“CAP_KILL”,
“CAP_AUDIT_WRITE”,
}
}
然而,无论是细粒度权限控制还是其他安全机制,用户都可以通过修改容器环境配置或在运行容器时指定参数来缩小或扩大约束。如果用户为不完全受控的容器提供了某些危险的配置参数,就为攻击者提供了一定程度的逃逸可能性。
Privileged特权模式运行容器
最初,容器特权模式的出现是为了帮助开发者实现Docker-in-Docker特性。然而,在特权模式下运行不完全受控容器将给宿主机带来极大安全威胁。这里笔者将官方文档对特权模式的描述摘录出来供参考:
当操作者执行docker run --privileged时,Docker将允许容器访问宿主机上的所有设备,同时修改AppArmor或SELinux的配置,使容器拥有与那些直接运行在宿主机上的进程几乎相同的访问权限。
如下图所示,我们以特权模式和非特权模式创建了两个容器,其中特权容器内部可以看到宿主机上的设备
在这样的场景下,从容器中逃逸出去易如反掌,手段也是多样的。例如,攻击者可以直接在容器内部挂载宿主机磁盘,然后将根目录切换过
至此,攻击者已经基本从容器内逃逸出来了。我们说“基本”,是因为仅仅挂载了宿主机的根目录,如果用ps查看进程,看到的还是容器内的进程,因为没有挂载宿主机的procfs。当然,这些已经不是难题。
危险挂载导致的容器逃逸
为了方便宿主机与虚拟机进行数据交换,几乎所有主流虚拟机解决方案都会提供挂载宿主机目录到虚拟机的功能。容器同样如此。然而,将宿主机上的敏感文件或目录挂载到容器内部——尤其是那些不完全受控的容器内部往往会带来安全问题。
尽管如此,在某些特定场景下,为了实现特定功能或方便操作(例如为了在容器内对容器进行管理将Docker Socket挂载到容器内),人们还是选择将外部敏感卷挂载入容器。随着容器技术应用的逐渐深化,挂载操作变得愈加广泛,由此而来的安全问题也呈现上升趋势。
挂载Docker Socket的情况
Docker Socket是Docker守护进程监听的Unix域套接字,用来与守护进程通信——查询信息或下发命令。如果在攻击者可控的容器内挂载了该套接字文件(/var/run/docker.sock),容器逃逸就相当容易了,除非有进一步的权限限制。
5.jpg
我们通过一个小实验来展示这种逃逸可能性:
首先创建一个容器并挂载/var/run/docker.sock;
在该容器内安装Docker命令行客户端;
接着使用该客户端通过Docker Socket与Docker守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部;
在新容器内执行chroot将根目录切换到挂载的宿主机根目录。
具体交互如下图所示:
至此,攻击者已经基本从容器内逃逸出来了。与上面章节类似,我们说“基本”,是因为仅仅挂载了宿主机的根目录,如果用ps查看进程,看到的还是容器内的进程,因为没有挂载宿主机的procfs。同样,这些已经不是难题。
挂载宿主机procfs的情况
对于熟悉Linux和云计算的朋友来说,procfs绝对不是一个陌生的概念,不熟悉的朋友可以参考网络上相关文章或直接在Linux命令行下执行man proc查看文档。
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时(截止到本文成稿时,Docker默认情况下不会为容器开启User Namespace)。