从脏管道(CVE-2022-0847)到 Docker 逃逸
本文作者:happi0
# 一、利用条件与限制
# 利用条件
- 有可读权限或者可以传回文件的文件描述符。
- 有漏洞的内核。
# 利用的限制
- 第一个字节不可修改,并且单次写入不能大于 4k。
- 只能单纯覆盖,不能调整文件大小。
- 由于漏洞基于内存页,所以不会对磁盘有影响。
# 二、与 Docker 的关系
由于 Docker 和宿主机是共享内核,尽管与其他进程资源是隔离开的,内核漏洞也很可能会对 Docker 容器造成安全问题。
# 对于容器的影响
由于 Docker 本质上是由一组互相重叠的层所组层的,然后容器引擎将其合并到一起,原本这些层都是只读的,但由于脏管道漏洞的影响,我们可以在 u1
容器里修改 /etc/passwd
使得 u2
容器的 /etc/passwd
被修改。
# 利用 CAP_DAC_READ_SEARCH 实现容器逃逸
通过利用 CAP_DAC_READ_SEARCH
与脏管道可以实现覆盖主机文件, 该攻击手段可以在 Github 看到详细过程,地址: github.com/greenhandatsjtu/CVE-2022-0847-Container-Escape (opens new window)
这里实际上主要是 CAP_DAC_READ_SEARCH
可以调用 open_by_handle_at
, 从而获得主机文件的文件描述符,再配合脏管道于是就可以修改主机文件了。
这种攻击方式非常简单,核心就是获得文件的文件描述符即可。
# 通过 runC 实现容器逃逸
一个容器开启时,可以分为以下三步
- fork 创建子进程
- 初始化容器化环境
- 将执行流重定向到用户提供的入口点
对于第三步,以大名鼎鼎的 CVE-2019-5736
为例,当重定向入口点时,容器内的/proc/self/exec
与主记的 runc
二进制文件相关联。
因此可以通过在容器内写入该文件描述符实现容器逃逸。
对于 CVE-2019-5736
的修复可以参见:github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b (opens new window)
由于篇幅原因这里不跟进 CVE-2019-5736
的修复的具体代码,直接看 git commit
了解修复逻辑。
可以看到修复逻辑是克隆 /proc/self/exec
避免容器内部直接获取 runC
,然而很快开发者修改了修复逻辑,参见:github.com/opencontainers/runc/commit/16612d74de5f84977e50a9c8ead7f0e9e13b8628 (opens new window)
可以看到开发者认为克隆导致的内存开销太大了,可能造成 OOM
或者其他问题,把修复逻辑改成了只读挂载。
这里联想到上文总结的 脏管道
的利用条件和利用效果,发现刚好契合。
这里的利用主要参考了这里的内容:securitylabs.datadoghq.com/articles/dirty-pipe-container-escape-poc/ (opens new window)
主机执行 docker exec -it u1 /bin/sh
后 /usr/sbin/runc
的哈希值变化了,且头部被注入标识。
利用思路也很简单,修改 CVE-2022-0847
的 exp,将需要注入的字节改为 shellcode,这里我随便改个标识,然后在容器内找到主机的 runc
的 pid 即可,可以参考以下的 shell
脚本。
#!/bin/bash
echo '#!/proc/self/exe' > /bin/sh
echo "Waiting for runC to be executed in the container"
while true ; do
runC_pid=""
while [ -z "$runC_pid" ] ; do
runC_pid=$(ps axf | grep /proc/self/exe | grep -v grep | awk '{print $1}')
done
/exp /proc/${runC_pid}/exe
done
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 三、总结
由于 Docker 容器和主机是共享内核的,且目前的 runc
是通过挂为只读权限防止逃逸的,对于提权类内核洞来说,这两个限制很容易被绕过。所以虽然容器本身逃逸类的漏洞不多,但提权类的内核漏洞会很可能导致容器逃逸。