Matryoshka
Can you escape the Matryoshka Containment Unit? -by Nemo777 and stefan.apostol
The following post by 0xb0b is licensed under CC BY 4.0
Scenario
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!
Use the credentials below to connect to the target VM.
ssh matryoshka@<IP> password:n03sk@p3
Summary
Summary
In Matryoshka, we begin with SSH credentials for matryoshka@<IP> and land inside a Docker container, confirmed by the presence of /.dockerenv at the root of the filesystem. Initial enumeration reveals the docker CLI is available from within the container and that an alpine:3.20 image is present locally, indicating the Docker socket is exposed to us. We spawn a new Alpine container with --privileged --pid=host --net=host -v /:/host and chroot into the bind-mounted host filesystem, granting us a root shell on the daemon host where the first flag sits in /root.
Filesystem enumeration of the alpine container exposes a shared directory at /mnt/level3share containing inbox and outboxfolders, the hint suggests a script-runner pattern where files dropped into inbox are executed by another container. We drop a Bash reverse shell script targeting our listener on port 4445 and receive a short-lived callback that yields the second flag before the connection dies.
To stabilise enumeration of the second container, we pivot to a script that writes its output to outbox/pwn.sh.out, dumping id, hostname, uname, cgroups, mounts, network interfaces, and a full ps auxf listing. The process tree confirms that level 3 itself was launched with --privileged --pid=host, meaning its PID 1 is the EC2 host's systemd and CAP_SYS_ADMIN is available. We abuse this by dropping a script that runs nsenter -t 1 -a -- sh -c '...' to enter all of PID 1's namespaces (mount, UTS, IPC, network, PID, cgroup, user), effectively breaking out onto the underlying EC2 host where the final flag is recovered from /root.
Level 1
The scenario description makes gives us a hint that we may be dealing with a sandbox or a Docker escape. We connect to the target via SSH using the provided credentials and skip the enumeration for now.
On the target, we can see that we are indeed inside a Docker container; we see the /.dockerenv file in the root directory. We can now proceed manually or using enumeration scripts like deepce.sh. However, we'll start by doing it manually first and see if we can control Docker from within the container. We see that the docker command is available and the matryoshka-level1 is running. Perhaps the container we're currently in... Starting the container takes us to an environment similar to the one we are already in.

We query for other images and find an alpine image.

We spawn the alpine container, via --privileged --pid=host --net=host -v /:/host, to obtain a root shell and mount the host's root filesystem into the container, but without success. The first flag can be found at /root/flag_level2.txt of the alpine container.

Level 2
Inside the container we find the folder level3share mounted. There we have an inbox and outbox folder.


The hint provided by the scenraio gives us the clue, that the folder allows script exection. Maybe if we place a script inside that folder it gets executed by another container or the host.
We place a simple reverse shell script inside the inbox folder and set up a listener on port 4445.

We receive a connection, but the connection dies after some seconds. Nevertheless we are able to grab the second flag.

Level 3
To enumerate the second Docker container in one go without relying on a stable reverse shell, we craft the following script (enhanced by Claude). It pulls the current user/ID, hostname, kernel info, cgroup, mounts, network interfaces, and running processes, checks for a mounted docker.sock (potential escape vector), and hunts the filesystem for flags. From a previous attempt we saw that the the runner is already redirecting our script's stdout/stderr to outbox/pwn.sh.out
We check the outbox and see the results. We can identfy the second flag, which we already got from our reverse shell.

The most interesting part is the output from ps aux.

The Level 3 container was started with --privileged --pid=host. With --pid=host, Level 3 shares the host's PID namespac, PID 1 inside the container is the EC2 host's systemd. The --privileged flag gives us CAP_SYS_ADMIN, which we then leverage with nsenter to attach to PID 1's namespaces and gain a shell on the host.
So with nsenter -t 1 -a we enter PID 1's mount/UTS/IPC/net/PID/cgroup namespaces... that's the host. From there we could reach the host. We craft the following script to reach out to the host. A small explanation what happens:
-t 1 means target PID 1. Normally PID 1 inside a container is the container's own init. But level 3 was started with --pid=host, which means it shares the host's PID namespace. So what level 3 calls PID 1 is actually the EC2 host's systemd. That's exactly the process whose namespaces we want to enter.
-a is shorthand for all namespaces. Equivalent to writing out -m -u -i -n -p -C -U (mount, UTS/hostname, IPC, network, PID, cgroup, user). Without it we'd only enter some namespaces and get a weird half-container/half-host hybrid.
-- is the standard POSIX argument terminator. Everything after it is the command to run inside the new namespaces, not more flags for nsenter.
sh -c '...' runs the quoted string as a shell command. we need a shell because we want to chain multiple commands with, use pipes, and run a while loop.
We run the script and receive the final flag.

We could also try to get a reverse shell...

... we get a connection to the host, but it dies after some seconds too.

Last updated