3.2.2 CVE-2018-15664:符号链接替换漏洞

在18.06.1-ce-rc2版本之前的Docker中,docker cp命令对应的后端API存在基于竞争条件的符号链接替换漏洞,能够导致目录穿越。攻击者可利用此漏洞以root权限实现宿主机文件系统的任意读写,CVSS 3.x评分为7.5分。

CVE-2018-15664实际上是一个TOCTOU(time-of-check to time-of-use)问题,属于竞态条件漏洞。简单来说,这个问题指的是在程序对某对象进行安全检查和使用该对象的步骤之间存在间隙,攻击者可以先构造并放置一个能够通过安全检查的合法对象,顺利通过目标程序的安全检查流程,然后立即使用恶意对象替换之前的合法对象。这样一来,目标程序真正使用的实际上是被替换后的恶意对象。

下面用流程图来表示这一过程,假设某程序需要使用“/xxx”文件,为了避免安全风险,需要先对该文件进行合法性检查,如果检查不通过,程序将报错或执行其他操作,只有在检查通过后才会继续使用该文件。在图3-2中,左侧流程展示了正常的处理情况,右侧流程展示了攻击者利用TOCTOU问题将“/xxx”文件替换为恶意文件的情况。

图3-2 正常流程(左)与TOCTOU恶意利用流程(右)

以上就是TOCTOU问题的原理。对于CVE-2018-15664来说,当用户执行docker cp命令后,Docker守护进程收到这个请求,就会对用户给出的复制路径进行检查。如果路径中有容器内部的符号链接,则先在容器内部将其解析成路径字符串,留待后用。

一眼看上去,该流程似乎正常,但要考虑到容器内部环境是不可控的。如果在Docker守护进程检查复制路径时,攻击者先在这里放置一个非符号链接的常规文件或目录,检查结束后,攻击者赶在Docker守护进程使用这个路径前将其替换为一个符号链接,那么这个符号链接就会于被打开时在宿主机上解析,从而导致目录穿越。

理解了漏洞原理之后,我们使用漏洞发现者Aleksa Sarai提供的PoC[1]来实践一下[2]

大家可以使用我们开源的metarget靶机项目[3],在Ubuntu服务器上一键部署漏洞环境,在参照项目主页安装metarget后,直接执行以下命令:


./metarget cnv install cve-2018-15664

即可安装好存在CVE-2018-15664漏洞的Docker。

下载并解压PoC后,PoC目录结构如下:


.
├── build
│   ├── Dockerfile
│   └── symlink_swap.c
├── run_read.sh
└── run_write.sh

其中,build目录包含了用来制作恶意镜像的Dockerfile和容器内漏洞利用源代码symlink_swap.c。

Dockerfile的主要内容是构建漏洞利用程序symlink_swap并将其放在容器根目录下,并在根目录下创建一个w00t_w00t_im_a_flag文件,内容为“FAILED -- INSIDE CONTAINER PATH”。容器启动后执行的程序(Entrypoint)即为/symlink_swap。

symlink_swap.c的任务是在容器内创建指向根目录“/”的符号链接,并不断地交换符号链接(由命令行参数传入,如“/totally_safe_path”)与一个正常目录(例如“/totally_safe_path-stashed”)的名字。这样一来,在宿主机上执行docker cp时,如果首先检查到“/totally_safe_path”是一个正常目录,但在后面执行复制操作时“/totally_safe_path”却变成了一个符号链接,那么Docker将在宿主机上解析这个符号链接。

CVE-2018-15664属于竞态条件漏洞,不是每次都能复现。为了增大漏洞被触发的几率,我们需要在宿主机上不断执行docker cp命令(高频使用docker cp命令在现实中十分不常见,这里主要是为了验证可行性,证明“至少现实中这种漏洞是有机会被利用的”)。run_read.sh和run_write.sh脚本正是用于模拟受害者在宿主机上不断执行docker cp命令。那么,为什么会有两个脚本呢?事实上,这两个脚本模拟的是不同的场景:

·run_read.sh模拟受害者不断使用docker cp将容器内文件复制到宿主机上的场景,一旦漏洞触发,容器内恶意符号链接在宿主机文件系统解析后指向的文件将被复制到受害者设定的宿主机目录下。

·run_write.sh模拟受害者不断使用docker cp将宿主机上文件复制到容器内的场景,一旦漏洞触发,受害者指定的宿主机文件将覆盖容器内恶意符号链接在宿主机文件系统解析后指向的文件。

我们以run_write.sh为例进行讲解,内容如下:


SYMSWAP_PATH=/totally_safe_path
SYMSWAP_TARGET=/w00t_w00t_im_a_flag
# 创建flag
echo "FAILED -- HOST FILE UNCHANGED" | sudo tee "$SYMSWAP_TARGET"
sudo chmod 0444 "$SYMSWAP_TARGET"
# 构建镜像并运行容器
docker build -t cyphar/symlink_swap \
    --build-arg "SYMSWAP_PATH=$SYMSWAP_PATH" \
    --build-arg "SYMSWAP_TARGET=$SYMSWAP_TARGET" build/ &> /dev/null
ctr_id=$(docker run --rm -d cyphar/symlink_swap "$SYMSWAP_PATH")
echo "SUCCESS -- HOST FILE CHANGED" | tee /src_file
# 不断执行docker cp命令
while true
do
    docker cp /src_file "${ctr_id}:$SYMSWAP_PATH/$SYMSWAP_TARGET"
done

run_write.sh启动后,恶意容器运行,然后不断执行docker cp命令,漏洞未触发时,宿主机上的/w00t_w00t_im_a_flag文件内容为:


FAILED -- HOST FILE UNCHANGED

如果漏洞成功触发,容器内的符号链接“/totally_safe_path”将在宿主机文件系统上解析,因此docker cp实际上是将/src_file文件复制到了宿主机上的/w00t_w00t_im_a_flag文件位置。也就是说,此时宿主机上/w00t_w00t_im_a_flag文件内容将被改写为:


SUCCESS -- HOST FILE CHANGED

为了更直观地展示漏洞利用效果,我们手动执行run_write.sh内的命令,docker cp执行一段时间后按Ctrl+C取消,这个过程如图3-3所示。

图3-3 CVE-2018-15664漏洞利用截图

可以看到,漏洞触发后,/w00t_w00t_im_a_flag文件内容被成功改写了。Aleksa Sarai提到,宿主机上的攻击者可以借助这个漏洞来实现提权(如改写/etc/shadow文件),也可以实现对宿主机上任意文件的读取。这个说法没有问题,但如果宿主机上的攻击者并非root用户,却能够与Docker交互(执行docker cp命令),更简单的提权方式也许是直接利用Docker运行一个特权容器。

[1] https://seclists.org/oss-sec/2019/q2/131。

[2] 随书代码仓库路径:https://github.com/brant-ruan/cloud-native-security-book/tree/main/code/0302- 开发侧攻击/02-CVE-2018-15664/symlink_race。

[3] https://github.com/brant-ruan/metarget.git。