Description
A malicious volume can specify a volume mount on /proc. Since Docker populates the volume by copying data present in the image, it's possible to build a fake structure that will trick runc into believing it had successfully written to /proc/self/attr/exec:
runc/libcontainer/apparmor/apparmor.go
Lines 23 to 25 in 7507c64
This is possible because apparmor.ApplyProfile is executed in the container rootfs, after pivot_root in prepareRootfs:
runc/libcontainer/standard_init_linux.go
Lines 115 to 117 in 7507c64
checkMountDestinations is supposed to prevent mounting on top of /proc:
runc/libcontainer/rootfs_linux.go
Lines 464 to 469 in 7507c64
... but the check does not work. I believe the reason is that the dest argument is resolved to an absolute path using securejoin.SecureJoin (before pivot_root), unlike the blacklist in checkMountDestinations, which is relative to the rootfs:
runc/libcontainer/rootfs_linux.go
Lines 414 to 419 in 7507c64
Minimal proof of concept (on Ubuntu 18.04):
mkdir -p rootfs/proc/self/{attr,fd}
touch rootfs/proc/self/{status,attr/exec}
touch rootfs/proc/self/fd/{4,5}
cat <<EOF > Dockerfile
FROM busybox
ADD rootfs /
VOLUME /proc
EOF
docker build -t apparmor-bypass .
docker run --rm -it --security-opt "apparmor=docker-default" apparmor-bypass
# container runs unconfinedNot a critical bug on its own, but should get a CVE assigned.
Discovered by Adam Iwaniuk and disclosed during DragonSector CTF (https://twitter.com/adam_iwaniuk/status/1175741830136291328).
The CTF challenge mounted a file to /flag-<random> and denied access to it using an AppArmor policy. The bug could then be used to disable the policy and read the file: https://gist.github.com/leoluk/2513b6bbff8aa5cd623f3d7d7f20871a