云😶🌫️
OCI(Open Container Initiative)规范是事实上的容器标准,已经被大部分容器实现以及容器编排系统所采用,包括 Docker 和 Kubernetes。
从 OCI 规范开始了解容器镜像,可以让我们对容器技术建立更全面清晰的认知,而不是囿于实现细节。OCI 规范分为 Image spec
和 Runtime spec
两部分,它们分别覆盖了容器生命周期的不同阶段
Docker核心原理
Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从 Apache2.0 协议开源。
Docker的实现依赖于Linux中众多的基础机制,包括用于资源限制的cgroup,用于隔离的Namespace,以及用于实现docker文件系统的Union FS等。
namespace
资源隔离
Linux的 namespace 可以实现资源能够在不同的命名空间里有相同的名称,譬如在 A命名空间
有个pid为1的进程,而在 B命名空间
中也可以有一个pid为1的进程。
nsproxy.h,7种namespace,对于每个任务 task_struct
都存在一个nsproxy 成员
1 | /* |
namespace也是linux内核的一个特性,它将内核资源分隔开,一组进程能看到一些资源,而其他组的进程看到的是不同的资源,组与组之间互不干扰,不知道对方的存在。简单来说,namespace就是内核提供的一种进程间资源隔离技术。
1 | ubuntu@ubuntu:~$ ls -al /proc/self/ns |
与其有关的系统调用
1 | # 有个unshare的命令,需要区分一下 |
unshare创建命名空间
1 | :/ # readlink /proc/$$/ns/uts |
cgroup
资源限制
cgroup(control group)是linux内核的一个特性,它可以用于限制、计算、隔离进程组对计算机资源的使用(如CPU、memory、disk I/O、network等)。
cgroup有如下四个功能:
- 资源限制(Resource limits):限制进程组对某一特定资源(CPU,disk,或network)的使用量
- 优先级(Prioritization):通过给某个cgroup中的进程分配多一些资源(相比于其他cgroup),从而提高优先级
- 审计(Accounting):记录进程/进程组使用的资源量
- 控制(Control):进程组控制,如可以使用freezer将进程组挂起或恢复
cgroup是容器(containers)的一个重要组成部分,因为容器中通常会运行多个进程,这些进程通常需要一并控制。
1 | $ cat /proc/self/cgroup |
Union FS
Union File System ,简称 UnionFS,把其他文件系统联合到一个联合挂载点的文件系统服务,目的是将多个文件联合在一起成为一个统一的视图
它的思想是,如果一个资源是重复的,但没有任何修改,这时候并不需要立即创建一个新的资源,这个资源可以被新旧实例共享。
创建新资源发生在第一次写操作,也就是对资源进行修改的时候。通过这种资源共享的方式,可以显著地减少未修改资源复制带来的消耗,但是也会在进行资源修改的时候增减小部分的开销。
OverlayFS:Overlayfs 是一种堆叠文件系统,它依赖并建立在其它的文件系统之上(例如 ext4fs 和 xfs 等等),并不直接参与磁盘空间结构的划分,仅仅将原来底层文件系统中不同的目录进行“合并”,然后向用户呈现。OverlayFS
MORE
如果觉得不明白,可以自己写一个简单的Docker
Docker逃逸
检测Docker环境
- 检查根目录下是否存在
.dockerenv
文件 - 检查
/proc/1/cgroup
是否存在含有docker字符串!
1 | root@06fcbcd12128:/# ls -al / |
Docker 启动容器的危险配置
如果设定了以下配置就会导致相应的隔离机制失效:
- –privileged:使容器内的 root 权限和宿主机上的 root 权限一致,权限隔离被打破
- –net=host:使容器与宿主机处于同一网络命名空间,网络隔离被打破
- –pid=host:使容器与宿主机处于同一进程命令空间,进程隔离被打破
- –volume /:/host:宿主机根目录被挂载到容器内部,文件系统隔离被打破
当操作者执行docker run --privileged
时,Docker将允许容器访问宿主机上的所有设备,使容器拥有与那些直接运行在宿主机上的进程几乎相同的访问权限。可以通过写ssh密钥、计划任务等方式达到逃逸。
判断是否为特权模式:CapEff 主要是检查线程的执行权限。如果是以特权模式启动的话,CapEff 对应的掩码值应该为0000003fffffffff 或者是 0000001fffffffff (1)
1 | # 特权级下 |
挂载文件系统进行逃逸
1 | # 查看磁盘文件 |
也可以添加新的用户进行登录
1 | mount /dev/sda1 /mnt |
Docker危险挂载
docker.sock
docker.sock是**Docker守护进程(Docker daemon)默认监听的Unix域套接字(Unix domain socket)**,容器中的进程可以通过它与Docker守护进程进行通信。
docker.sock挂载
1 | docker run -itd -v /var/run/docker.sock:/var/run/docker.sock id |
检测
1 | ls -lah /var/run/docker.sock |
根目录
相当于直接写主机的根目录了
1 | chroot /mount_dir |
procfs
查看是否挂载
1 | find / -name core_pattern |
逃逸,(2)
1 | # 寻找在主机下的绝对路径 |
Docker remote API
通过将宿主机的docker服务通过socket的方式暴露给外部连接,使得其他主机也可以访问docker服务。
1 | dockerd -H unix:///var/run/docker.sock -H 0.0.0.0:2375 |
可以利用 remote API 来操作docker进行逃逸
1 | # 查看容器 |
程序漏洞导致Docker 逃逸
使用 docker version
命令可以看到 runc && containerd
组件
runc
runc是一个底层服务工具,runC 管理容器的创建,运行,销毁等,docker部分版本服务运行时底层其实在运行着runc服务,攻击者可以通过特定的容器镜像或者exec操作重写宿主机上的runc 二进制文件,并在宿主机上以root身份执行命令。
一个容器开启时,可以分为以下三步
- fork 创建子进程
- 初始化容器化环境
- 将执行流重定向到用户提供的入口点
docker run
等命令的时候实际上在底层调用的是runC程序,我们在容器中运行 /bin/bash
也会调用到runC
procfs:
/proc/[PID]/exe
: 一种特殊的软连接,是该进程自身对应的本地文件/proc/[PID]/fd/
: 这个目录下存放了该进程打开的所有文件描述符
/proc/[PID]/exe
的特殊之处在于当权限通过的情况下打开这个文件,内核将会之间返回一个指向该文件的文件描述符,并非按照传统的打开方式做路径分析和文件查找,这就会导致绕过了mnt命名空间和chroot的限制。
CVE-2019-5736:该漏洞允许攻击者重写宿主机上的runc 二进制文件,导致攻击者可以在宿主机上以root身份执行命令。
- 修改容器内的
/bin/sh
文件,改为#!/proc/self/exe
,这样的话,当容器内的/bin/sh
被执行的时候,实际上被执行的文件路径是/proc/self/exe
/proc/self/exe
是内核为每个进程创建的符号链接,指向为该进程而执行的二进制文件。当容器中的/bin/sh
被执行时,/proc/self/exe
指向的宿主机上的runc
就会被执行
漏洞的存在原理在于/proc/pid/exe这个绑定的方式,/proc是比较熟知的一个概念,为一个虚拟文件系统,其中的文件能够显示当前的进程运行信息。/proc/pid/exe是一个程序链接,指向这个pid运行的程序。
而这个漏洞的利用方式就在于,在docker里查找到runc的exe,获取对应于该位置的一个文件句柄,然后向这个位置写入东西的话,就能够将宿主机的程序覆盖掉,然后用户下一次再要运行runc的时候,就会触发反弹shell。
PoC
1 | package main |
CVE-2024-21626:由于 runc
内部不正确处理文件描述符,导致泄漏关键的宿主机文件描述符到容器中。
近日大火的CVE,在翻看这位师傅的文章时看到的
看起来很NB,虽然我现在看不懂😭
containerd
containerd 是一个工业级标准的容器运行时,它强调简单性、健壮性和可移植性,containerd 可以负责干下面这些事情:
- 管理容器的生命周期(从创建容器到销毁容器)
- 拉取/推送容器镜像
- 存储管理(管理镜像及容器数据的存储)
- 调用 runc 运行容器(与 runc 等容器运行时交互)
- 管理容器网络接口及网络
内核漏洞
ROP
commit_creds(prepare_kernel_cred(0))不会突破namespace对于进程的限制,也就是说即使在完成提权之后,task_struct中的fs_struct或者是ns_proxy都不受到影响还处于原本的命名空间中。
可以看CVE-2021-22555的漏洞利用,在得到root权限后,需要在内核中将进程的命名空间切换为初始的全局命名空间 init_nsproxy
即可完成容器逃逸,执行switch_task_namespaces(find_task_by_vpid(1), init_nsproxy)
即可替换掉当前进程的命名空间
1 | // switch_task_namespaces(find_task_by_vpid(1), init_nsproxy) |
dirty pipe
通过利用 CAP_DAC_READ_SEARCH
与脏管道可以实现覆盖主机文件,实际上主要是CAP_DAC_READ_SEARCH
可以调用open_by_handle_at
, 可以获得主机文件的文件描述符,配合脏管道于是就可以修改主机文件。但是需要添加cap权限 (3)
1 | docker run --rm -it --cap-add=CAP_DAC_READ_SEARCH ubuntu |