Podman rootless containers and the Copy Fail exploit
Podman rootless containers and the Copy Fail exploit
Podman 无根容器与 Copy Fail 漏洞
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. 4 月 29 日,CVE-2026-31431 在 https://copy.fail/ 被公开披露。该漏洞允许本地非特权用户通过运行作者提供的 Python 脚本来获取 root shell。
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. 此漏洞可用于攻击 Linux 容器,而容器被广泛用于运行各种服务:面向公众的服务、开发环境、持续集成作业等。被 Copy Fail 漏洞利用的容器可以非常有效地用于多种攻击。
This CVE is quite interesting to me as it’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’s security posture. 这个 CVE 对我来说非常有趣,因为我从 Docker 转向 Podman 运行容器已经大约一年了。促使这一转变的原因有很多,但其中最主要的是 Podman 的安全态势。
Podman makes it trivial to run containers as an unprivileged user, and this is known as running a container “rootless”. 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.
Podman 可以轻松地以非特权用户身份运行容器,这被称为“无根(rootless)”运行容器。与 Docker 不同,Podman 使用 fork/exec 模型,使得容器进程最终成为运行该容器的 podman run 进程的后代。因此,你可以依靠标准的 UID 分离来将容器进程与系统中的 root 或其他用户隔离开来。
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 severely limited using several features in Podman. 当我阅读关于 Copy Fail 的资料时,并没有发现太多关于它在无根容器中具体应用的信息。经过一些简单的测试,我确认 Copy Fail 确实可以在无根容器中被利用以获取容器内的 root shell,但由于 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. 在本文发布时,关于容器逃逸的信息并不多:根本原因、散列表(scatterlist)图表、2011 → 2015 → 2017 的历史演变以及漏洞利用演示都在 Xint 博客上。第二部分(Kubernetes 容器逃逸)即将发布。
In my testing, the container root is still limited to what the unprivileged user running the container can do at the host level. 在我的测试中,容器内的 root 权限仍然受限于运行该容器的非特权用户在宿主机层面所能执行的操作。
All in all, Copy Fail has proven to be a great example to refer to when writing about Podman’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. 总而言之,Copy Fail 已被证明是撰写关于 Podman 无根容器实现时的一个极佳参考案例。在本文中,我将在不同的容器配置中重现该漏洞,以试图了解受损无根容器的暴露风险。
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
- 无根容器、用户命名空间和 Linux 能力的实践回顾
- Using Copy Fail in rootless containers
- 在无根容器中使用 Copy Fail
- Practicing defence in depth to further limit exposure in the event of a compromise
- 实践纵深防御,以在发生入侵时进一步限制暴露风险
An overview of rootless containers
无根容器概述
Let’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.
假设我需要运行一个 HTTP 服务器来提供一些 HTML 页面。该服务器将运行在一个由 UID 为 1001 的非特权用户 bar 所拥有的容器中。
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:
我安装 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
<!DOCTYPE html><html lang="en"></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
The server should now be responding to requests: 现在服务器应该可以响应请求了:
bar@debian:~$ curl localhost:8000
<!DOCTYPE html><html lang="en"></html>
Rootless rootful
无根环境下的 root 权限
Let’s examine what this container process looks like. Using ps I can confirm that this python3 process is owned by the user bar:
让我们检查一下这个容器进程是什么样的。使用 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
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:
正如引言中所述,Podman 使用 fork/exec 模型来运行容器。用户 bar 执行了 podman run 命令,而容器命令 python3 是该进程的后代。这与标准的 Docker 设置形成对比,在 Docker 中,以非特权用户身份运行 docker run 会执行一个 Docker 客户端,该客户端与一个拥有 root 权限的守护进程交互,最终由该守护进程生成容器:
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
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.
现在,容器也有用户和组来确定容器内部的权限。大多数镜像在 Containerfile 中没有明确的 USER 指令,或者在运行容器时没有使用 --user 标志的情况下,默认以 root 身份运行容器命令。
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:
使用 podman top,我可以确认 python3 容器进程是以 root 身份运行的,因为我没有声明哪个用户执行该进程:
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’re using an unprivileged user? 请记住,容器与宿主机共享内核。在容器内成为 root 意味着什么?既然我们使用的是非特权用户,这肯定与宿主机的 root 不同,对吧?
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.
Podman 为无根容器使用用户命名空间。用户命名空间允许进程在容器内外拥有不同的 UID/GID。在前面的例子中,python3 进程在命名空间内拥有 UID 0(即容器 root),而在命名空间外被映射为 UID 1001(即宿主机用户 bar)。
The range of UIDs that can be allocated to namespaced processes of user bar is determined in /etc/subuid:
可以分配给用户 bar 的命名空间进程的 UID 范围由 /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).
除了 UID 1001 之外,还有 65,537 个 UID 可以分配给 bar 的进程,从 165536 开始,到 231072 (165536 + 65537) 结束。