메뉴
HN
Hacker News 22일 전

루트리스 컨테이너와 Copy Fail 취약점 분석

IMP
8/10
핵심 요약

이 글은 4월 29일 공개된 'Copy Fail' 취약점(CVE-2026-31431)이 Podman의 루트리스(Rootless) 컨테이너 환경에 미치는 영향을 분석합니다. 취약점 악용 시 컨테이너 내부에서 루트 권한을 획득할 수 있으나, Podman의 구조적 특성상 호스트 시스템에 미치는 피해 반경은 매우 제한적입니다. 실무자 관점에서 리눅스 컨테이너의 격리 원리와 보안 심층 방어의 중요성을 잘 보여주는 중요한 사례입니다.

번역된 본문

4월 29일, CVE-2026-31431이 https://copy.fail/ 웹사이트를 통해 공개되었습니다. 이 취약점은 권한이 없는 일반 로컬 사용자가 작성자가 공유한 파이썬 스크립트를 실행하여 루트 셸(root shell)을 획득할 수 있게 합니다. 이 익스플로잇은 퍼블릭 서비스, 개발 환경, CI/CD(지속적 통합) 작업 등 다양한 용도로 널리 사용되는 리눅스 컨테이너를 공격하는 데 사용될 수 있습니다. Copy Fail에 의해 해킹된 컨테이너는 다양한 종류의 공격에 매우 효과적으로 악용될 수 있습니다.

이 CVE는 제가 컨테이너 실행을 Docker에서 Podman으로 전환한 지 약 1년이 된 시점에 나와서 특히 흥미로웠습니다. 이러한 변경을 결정한 데는 여러 이유가 있었지만, 무엇보다 Podman의 강력한 보안 태세(security posture)가 결정적이었습니다. Podman은 권한 없는 일반 사용자로 컨테이너를 실행하는 것을 매우 간단하게 만들며, 이를 소위 '루트리스(rootless)' 컨테이너 실행이라고 부릅니다.

Docker와 달리 Podman은 fork/exec 모델을 사용하므로 컨테이너 프로세스는 결국 컨테이너를 실행하는 데 사용된 podman run 프로세스의 하위 프로세스가 됩니다. 결과적으로 표준 UID 분리를 활용하여 컨테이너 프로세스를 시스템의 루트나 다른 사용자로부터 격리할 수 있습니다.

Copy Fail에 대한 글을 읽으면서도 루트리스 컨테이너에서의 구체적인 활용 사례에 대한 정보는 거의 찾지 못했습니다. 몇 가지 간단한 테스트를 수행한 후, 저는 Copy Fail이 실제로 루트리스 컨테이너에서 악용되어 컨테이너 루트 셸을 얻을 수 있다는 것을 확인했습니다. 하지만 Podman의 여러 기능을 통해 그 피해 반경(blast radius)이 상당히 제한된다는 사실도 확인했습니다.

발행 시점을 기준으로 컨테이너 탈출(container escape)에 대한 정보는 많지 않습니다. 근본 원인, scatterlist 다이어그램, 2011 → 2015 → 2017의 연혁 및 익스플로잇 연구는 Xint 블로그에서 확인할 수 있습니다. 파트 2(쿠버네티스 컨테이너 탈출)는 향후 공개될 예정입니다.

제가 테스트한 결과, 컨테이너 루트 권한은 여전히 컨테이너를 실행하는 권한 없는 일반 사용자가 호스트 수준에서 할 수 있는 작업에만 제한됩니다. 결론적으로 Copy Fail은 Podman의 루트리스 컨테이너 구현을 설명할 때 참고할 수 있는 아주 훌륭한 사례로 입증되었습니다.

이 글에서는 다양한 컨테이너 구성에 걸쳐 익스플로잇을 재현하여 루트리스 컨테이너가 침해되었을 때 노출되는 범위를 이해해 보고자 합니다. 글이 다소 길어졌으므로, 필요한 경우 관련 부분으로 바로 건너뛰셔도 좋습니다:

  • 루트리스 컨테이너, 사용자 네임스페이스 및 리눅스 기능(Capabilities)에 대한 실용적인 검토
  • 루트리스 컨테이너에서 Copy Fail 사용하기
  • 침해 발생 시 노출을 추가로 제한하기 위한 심층 방어(Defence in depth) 실천

루트리스 컨테이너 개요 HTML을 서비스하기 위해 HTTP 서버를 실행해야 한다고 가정해 보겠습니다. 이 서버는 UID가 1001인 'bar'라는 권한 없는 일반 사용자가 소유한 컨테이너 내에서 실행될 것입니다. Podman을 설치하고, 'bar' 사용자를 생성한 후 해당 계정으로 전환합니다. 그런 다음 podman build를 사용하여 이미지를 빌드하고 podman run을 사용하여 컨테이너를 실행합니다:

root@debian:# apt install -y podman root@debian:# useradd -m -d /var/lib/bar -s /bin/bash -u 1001 bar root@debian:~# su - bar

bar@debian:~$ cat > Containerfile <<EOF FROM ubuntu:latest RUN apt update && apt install -y python3 && apt clean RUN mkdir -p /var/www/html WORKDIR /var/www/html RUN cat > index.html <<HTML HTML EXPOSE 8000 CMD ["python3", "-m", "http.server", "-b", "0.0.0.0", "8000"] EOF

bar@debian:$ podman build -t http-server . bar@debian:$ podman run --rm -it --name http-server-1 -d -p 127.0.0.1:8000:8000/tcp localhost/http-server:latest

이제 서버가 요청에 정상적으로 응답해야 합니다: bar@debian:~$ curl localhost:8000

루트리스(Rootless)와 루트풀(Rootful) 비교 이 컨테이너 프로세스가 어떻게 보이는지 살펴보겠습니다. ps 명령을 사용하면 이 python3 프로세스가 'bar' 사용자에게 속해 있음을 확인할 수 있습니다:

root@debian:~# ps -fC python3 UID PID PPID C STIME TTY TIME CMD bar 4861 4859 0 19:26 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000

서론에서 언급했듯이 Podman은 컨테이너를 실행하기 위해 fork/exec 모델을 사용합니다. 'bar' 사용자가 podman run 명령을 실행했습니다.

원문 보기
원문 보기 (영어)
Contents On April 29th CVE-2026-31431 was publicly disclosed at https://copy.fail/ . This vulnerability allows a local unprivileged user to obtain a root shell by running the Python script shared by the author. This exploit can be used to exploit Linux containers, which are widely used to run all sorts of things: public-facing services, development environments, continuous integration jobs, etc. A container exploited with Copy Fail can used quite effectively for many kinds of attacks. This CVE is quite interesting to me as it&rsquo;s been about a year since I moved away from Docker to Podman to run containers. Several reasons motivated this change, but chief among them was Podman&rsquo;s security posture 1 . Podman makes it trivial to run containers as an unprivileged user, and this is known as running a container &ldquo;rootless&rdquo;. Unlike Docker, Podman uses a fork/exec model such that the container process is ultimately a descendant of the podman run process that is used to run the container. As a result, you can rely on standard UID separation to isolate your container processes from root or other users in the system. As I read about Copy Fail I did not find much information about its use in rootless containers specifically. After performing some simple tests I confirmed that Copy Fail is indeed exploitable in rootless containers to obtain a container root shell , but the blast radius of this is severaly limited using several features in Podman. At the time of publishing, there is not a lot of information about container escapes: Root cause, scatterlist diagrams, the 2011 → 2015 → 2017 history, and the exploit walkthrough are on the Xint blog. Part 2 (Kubernetes container escape) is forthcoming. In my testing, the container root is still limited to what the unprivileged user running the container can do at the host level. All in all, Copy Fail has proven to be a great example to refer to when writing about Podman&rsquo;s implementation of rootless containers. In this note I reproduce the exploit across distinct container configurations to try to understand the exposure of a compromised rootless container. This article ended up being a bit long so feel free to jump ahead to the relevant parts if you need to: A practical review of rootless containers , user namespaces and Linux capabilities Using Copy Fail in rootless containers Practicing defence in depth to further limit exposure in the event of a compromise An overview of rootless containers Let&rsquo;s assume that I need to run an HTTP server to serve some HTML. The server will run in a container owned by an unprivileged user bar whose UID is 1001. I install Podman, create the user bar , and switch to it. Then, I build the image using podman build and run the container using podman run : root@debian:~# apt install -y podman root@debian:~# useradd -m -d /var/lib/bar -s /bin/bash -u 1001 bar root@debian:~# su - bar bar@debian:~$ cat > Containerfile <<EOF FROM ubuntu:latest RUN apt update && apt install -y python3 && apt clean RUN mkdir -p /var/www/html WORKDIR /var/www/html RUN cat > index.html <<HTML <!DOCTYPE html><html lang=&#34;en&#34;></html> HTML EXPOSE 8000 CMD [&#34;python3&#34;, &#34;-m&#34;, &#34;http.server&#34;, &#34;-b&#34;, &#34;0.0.0.0&#34;, &#34;8000&#34;] EOF bar@debian:~$ podman build -t http-server . bar@debian:~$ podman run --rm -it --name http-server-1 -d -p 127.0.0.1:8000:8000/tcp localhost/http-server:latest The server should now be responding to requests: bar@debian:~$ curl localhost:8000 <!DOCTYPE html><html lang = &#34;en&#34; ></html> Rootless rootful Let&rsquo;s examine what this container process looks like. Using ps I can confirm that this python3 process is owned by the user bar : root@debian:~# ps -fC python3 UID PID PPID C STIME TTY TIME CMD bar 4861 4859 0 19:26 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000 As mentioned in the introduction, Podman uses a fork/exec model to run containers. User bar executed the podman run command, and the container command python3 descended from that process. This is in contrast to the standard Docker setup, in which running docker run as an unprivileged user executes a Docker client that interacts with a rootful daemon that ultimately spawns the container: bar@debian:~$ docker run --rm -it -d --name http-server-1 http-server bar@debian:~$ ps -fC python3 UID PID PPID C STIME TTY TIME CMD root 5198 5175 5 19:20 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000 bar@debian:~$ docker container top http-server-1 UID PID PPID C STIME TTY TIME CMD root 4844 4820 0 14:51 pts/0 00:00:00 python3 -m http.server -b 0.0.0.0 8000 Now, containers also have users and groups to determine permissions inside the container. Most images default to running the container commands as root in the absence of an explicit USER instruction in the Containerfile or a --user flag when running the container. Using podman top I can confirm that the python3 container process is running as root as I did not declare which user executes the process: bar@debian:~$ podman top http-server-1 huser,user,pid,args HUSER USER PID COMMAND 1001 root 1 python3 -m http.server -b 0.0.0.0 8000 Remember that containers share the kernel with the host. What does being root inside the container mean? Surely this is not the same as host root given that we&rsquo;re using an unprivileged user? User namespaces Podman uses user namespaces for rootless containers. User namespaces allow processes to have different a UID/GID inside and outside the container. In our previous example, the python3 process has a UID of 0 (i.e container root ) inside the namespace while being mapped to UID 1001 (i.e host bar ) outside it. The range of UIDs that can be allocated to namespaced processes of user bar is determined in /etc/subuid : bar@debian:~$ grep bar /etc/subuid bar:165536:65536 Besides UID 1001, there are 65,537 UIDs can be allocated to processes of bar , starting with 165536 and ending with 231072 (165536 + 65537). Our current image is based off of ubuntu , which brings its own set of users: bar@debian:~$ podman run --rm -it --name http-server-1 localhost/http-server:latest cat /etc/passwd root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin irc:x:39:39:ircd:/run/ircd:/usr/sbin/nologin _apt:x:42:65534::/nonexistent:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin ubuntu:x:1000:1000:Ubuntu:/home/ubuntu:/bin/bash Processes and objects from these users have the UIDs shown above within the bar user namespace. Outside the namespace these are mapped to a UID within the 165537-231072 range, with the exception of root which is mapped to host UID 1001. For example, let&rsquo;s have bar run sleep in the container as user www-data : bar@debian:~$ podman run --rm -it -d --name http-server-1 --user = www-data localhost/http-server:latest sleep 60 bar@debian:~$ podman top http-server-1 huser,user,args HUSER USER COMMAND 165568 www-data sleep 60 The sleep process is running as www-data inside the user namespace but is mapped to 165568 on the host. The user namespace affords standard UID isolation across processes of the same user. That is to say, from the host&rsquo;s perspective, a process of www-data in the bar user namespace is separate from one of bar . Docker does support using user namespaces, but it must be configured accordingly and only