
Lokales Docker-Lab zur Demonstration von CVE-2026-34234 in CtrlPanel.
Dieses Repository vergleicht:
vuln: CtrlPanel 1.1.1, per Digest festgepinntpatched: CtrlPanel 1.2.0, per Digest festgepinntDas Lab ist rein lokal und bindet Dienste an 127.0.0.1.
CVE-2026-34234 ist eine nicht authentifizierte RCE im Web-Installer von CtrlPanel.
Das Problem wird durch zwei miteinander verkettete Fehler verursacht:
install.lock-Sperre erreichbar.In diesem Lab führt der verwundbare Container einen harmlosen Proof-Befehl aus und schreibt dessen Ausgabe innerhalb des Containers. Der gepatchte Container empfängt dieselbe Anfrage, erstellt jedoch keine Proof-Datei.
Erwartetes Ergebnis:
vulnerable => proof file created
patched => no proof file
1.1.1Verwundbare Originaldatei:
public/installer/src/functions/shell.php
Relevanter Upstream-Code in 1.1.1:
function run_console(string $command, ...) {
$path = dirname(__DIR__, 4);
$handle = proc_open("cd '$path' && bash -c 'exec -a ServerCPP $command'", ...);
}
Problem:
run_console() akzeptiert einen einzelnen Shell-Befehlsstring.bash -c übergeben.Verwundbare Originaldatei:
public/installer/src/forms/pterodactyl.php
Relevantes Upstream-Verhalten in 1.1.1:
run_console("php artisan settings:set 'PterodactylSettings' 'panel_url' '$url'", ...);
run_console("php artisan settings:set 'PterodactylSettings' 'admin_token' '$key'", ...);
run_console("php artisan settings:set 'PterodactylSettings' 'user_token' '$clientkey'", ...);
Problem:
url, key und clientkey stammen aus den POST-Daten des Installers.Das Advisory besagt, dass public/installer/index.php install.lock erst nach dem Laden/Ausführen der Installer-Formularlogik prüfte. Dadurch waren Installer-Handler selbst auf bereits installierten Instanzen erreichbar.
Der Fix führt die install.lock-Prüfung aus, bevor die Formularhandler geladen werden.
Gepatchtes Verhalten:
if (file_exists('../../install.lock')) {
exit("The installation has been completed already. Please delete the File 'install.lock' to re-run");
}
Gepatchte Originaldatei:
public/installer/src/functions/shell.php
Relevanter Upstream-Code in 1.2.0:
function run_console(array $command, ...): string {
$cwd = $cwd ?? $path;
$handle = proc_open($command, $descriptors, $pipes, $cwd, null, $options);
}
Warum dies das Problem behebt:
run_console() akzeptiert nun ein Array im argv-Stil.$() bleibt wörtliche Eingabe und wird nicht als Shell-Syntax interpretiert.Das gepatchte Formularverhalten in 1.2.0 verwendet die Befehlsausführung im Array-Stil:
run_console(['php', 'artisan', 'settings:set', 'PterodactylSettings', 'panel_url', $url], ...);
run_console(['php', 'artisan', 'settings:set', 'PterodactylSettings', 'admin_token', $key], ...);
run_console(['php', 'artisan', 'settings:set', 'PterodactylSettings', 'user_token', $clientkey], ...);
127.0.0.1:8081 -> vulnerable CtrlPanel 1.1.1
127.0.0.1:8082 -> patched CtrlPanel 1.2.0
127.0.0.1:9100 -> fake Pterodactyl API
Dienste:
vuln: echtes CtrlPanel 1.1.1patched: echtes CtrlPanel 1.2.0fake-api: lokale Fake-Pterodactyl-API, die nur zur Erfüllung der Installer-Prüfungen dientmysql_vuln / mysql_patched: separate MariaDB-Instanzenredis_vuln / redis_patched: separate Redis-InstanzenDas Lab verändert den Quellcode der CtrlPanel-Anwendung nicht.
Die Dockerfiles kapseln lediglich den ursprünglichen Container-Entrypoint, um die Laufzeitberechtigungen von Docker Desktop zu normalisieren:
/var/www/html/storage
/var/www/html/bootstrap/cache
Nach der Korrektur der Berechtigungen führt der Wrapper den ursprünglichen Produkt-Entrypoint aus.
Primärer PoC:
poc/poc_http_only.py
Eigenschaften:
docker execid, whoami, hostnameHilfsskript:
poc/poc_lab.py
Zweck:
docker compose execProof-Datei im App-Container:
/var/www/html/storage/logs/cve_2026_34234_proof.txt
Starte mit einem sauberen Lab-Zustand:
docker compose down -v --remove-orphans
docker compose up -d --build
Warte, bis die App-Container gestartet sind, und führe dann Folgendes aus:
python3 poc/poc_lab.py
Erwartete Ausgabe:
== Testing vulnerable ==
proof_exists: True
result: PASS expected_proof=True
== Testing patched ==
proof_exists: False
result: PASS expected_proof=False
[+] Expected result reached:
vulnerable => proof file created
patched => no proof file
Sende den HTTP-Only-PoC an das verwundbare Ziel:
python3 poc/poc_http_only.py --target http://127.0.0.1:8081
Verifiziere den Proof manuell:
docker compose exec vuln sh -lc 'cat /var/www/html/storage/logs/cve_2026_34234_proof.txt'
Erwarteter Inhalt der Proof-Datei:
uid=1000(laravel) gid=1000(laravel) groups=1000(laravel)
laravel
<container-hostname>
Sende dieselbe Anfrage an die gepatchte Instanz:
python3 poc/poc_http_only.py --target http://127.0.0.1:8082
Verifiziere das gepatchte Verhalten:
docker compose exec patched sh -lc 'test -f /var/www/html/storage/logs/cve_2026_34234_proof.txt && cat /var/www/html/storage/logs/cve_2026_34234_proof.txt || echo "no proof file"'
Erwartet:
no proof file
Entferne Container, Netzwerke und Lab-Volumes:
docker compose down -v
Dieses Repository wird ausschließlich zu Ausbildungszwecken, für Sicherheitsforschung und defensive Validierung bereitgestellt.
Alle Demonstrationen sind für die Ausführung in der bereitgestellten lokalen Docker-Lab-Umgebung vorgesehen. Der Proof-of-Concept vermeidet destruktive Aktionen, Persistenz, Diebstahl von Zugangsdaten, Datenexfiltration und Angriffe auf reale Systeme.
Verwende dieses Projekt nicht gegen ein System ohne ausdrückliche Genehmigung. Der Autor übernimmt keine Verantwortung für Missbrauch oder Schäden, die aus diesem Material entstehen.