DEV Community

Rocky
Rocky

Posted on

Root Inside the Container Isn't Root. Here's What Actually Is.

A foothold on an internal engagement finally lands through a vulnerable web app running inside a container, and the shell comes back and answers id with uid=0(root). For a second that reads like the engagement is over: root is root. Then cat /etc/shadow returns nothing interesting, the "host" filesystem is just the container's own minimal image, and none of the credentials cached anywhere on the real network are reachable from here. Root inside a container is root inside a namespace, a boundary drawn around one process tree, one filesystem view, one set of resource limits. It says nothing about what that same root can touch on the machine actually running the container.

Getting from container root to host root usually comes down to checking what the container was handed that it shouldn't have been. The single most common one is a bind-mounted Docker socket: /var/run/docker.sock sitting inside the container's filesystem, usually left there by a CI runner or a monitoring sidecar that needed to talk to the host's Docker daemon and got mounted in without much thought about what else could reach it. If that socket is present and the Docker CLI or a raw curl against it works, the container can ask the host's own Docker daemon to start a brand-new container, mount the host's root filesystem into it, and hand back a shell running as root on the actual machine. The escape doesn't exploit a bug in Docker. It just uses the API the way it's designed to be used, from a place it should never have had access to use it from.

The second thing worth checking is what capabilities the container actually has, because --privileged isn't the only way to end up dangerous. CAP_SYS_ADMIN on its own is enough to mount the host's cgroup filesystem from inside the container and abuse the release_agent mechanism, a documented feature that lets a cgroup run an arbitrary program on the host when a cgroup empties out. Write a path to that file, write a script to the location it points at, trigger the cgroup to empty, and the host executes whatever was written there. Checking for this takes one command, capsh --print or reading the Cap lines out of /proc/self/status, and it's worth running on every container shell before assuming the boundary actually held.

None of this is a reason to distrust containers as a technology. It's a reason to stop treating "I'm root in here" as an answer and start treating it as the first question in a checklist: what's mounted in that shouldn't be, what capabilities were granted that go past what the app actually needs, and does the host's own daemon socket sit reachable from inside. That checklist looks nothing like the SUID-binary, sudo-misconfig, cron-job version of Linux privilege escalation most people learn first, which is exactly why it gets skipped by testers who never had a reason to build the container-specific half of the muscle memory.

Codelivly's Linux Privilege Escalation Playbook covers the full arc, from classic host enumeration through container escape and the hardening that closes each path, across 12 labs built to be run, not just read.

Top comments (0)