## https://sploitus.com/exploit?id=34F15F9E-3DE3-5F98-9A00-51E6DAA3B16B
# CVE-2026-34234 - CtrlPanel Installer RCE Lab
Local Docker lab for demonstrating CVE-2026-34234 in CtrlPanel.
This repository compares:
- `vuln`: CtrlPanel `1.1.1` pinned by digest
- `patched`: CtrlPanel `1.2.0` pinned by digest
The lab is local-only and binds services to `127.0.0.1`.
---
## Summary
CVE-2026-34234 is an unauthenticated RCE in CtrlPanel's web installer.
The issue is caused by two bugs chained together:
1. Installer form handlers were reachable before the `install.lock` gate.
2. Installer input was interpolated into shell command strings.
In this lab, the vulnerable container executes a harmless proof command and writes its output inside the container. The patched container receives the same request but does not create the proof file.
Expected result:
```text
vulnerable => proof file created
patched => no proof file
```
---
## Root Cause
### 1. Vulnerable shell execution in `1.1.1`
Original vulnerable file:
```text
public/installer/src/functions/shell.php
```
Relevant upstream code in `1.1.1`:
```php
function run_console(string $command, ...) {
$path = dirname(__DIR__, 4);
$handle = proc_open("cd '$path' && bash -c 'exec -a ServerCPP $command'", ...);
}
```
Problem:
- `run_console()` accepts one shell command string.
- That string is passed into `bash -c`.
- User-controlled installer values can become part of that command string.
- Shell metacharacters can change command structure.
### 2. Vulnerable installer form path
Original vulnerable file:
```text
public/installer/src/forms/pterodactyl.php
```
Relevant upstream behavior in `1.1.1`:
```php
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`, and `clientkey` originate from installer POST data.
- The values are embedded into shell command strings.
- The installer endpoint is reachable without authentication.
### 3. Installer gate order
The advisory states that `public/installer/index.php` checked `install.lock` only after loading/executing installer form logic. That made installer handlers reachable even on already-installed instances.
---
## Patch / Fix
### 1. Early installer lock check
The fix moves the `install.lock` check before form handlers are loaded.
Patched behavior:
```php
if (file_exists('../../install.lock')) {
exit("The installation has been completed already. Please delete the File 'install.lock' to re-run");
}
```
### 2. Avoid shell string execution
Original patched file:
```text
public/installer/src/functions/shell.php
```
Relevant upstream code in `1.2.0`:
```php
function run_console(array $command, ...): string {
$cwd = $cwd ?? $path;
$handle = proc_open($command, $descriptors, $pipes, $cwd, null, $options);
}
```
Why this fixes the issue:
- `run_console()` now accepts an argv-style array.
- The command is no longer composed as a single shell string.
- Payload syntax such as `$()` remains literal input instead of shell syntax.
Patched form behavior in `1.2.0` uses array-style command execution:
```php
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], ...);
```
---
## Lab Design
```text
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
```
Services:
- `vuln`: real CtrlPanel `1.1.1`
- `patched`: real CtrlPanel `1.2.0`
- `fake-api`: local fake Pterodactyl API used only to satisfy installer checks
- `mysql_vuln` / `mysql_patched`: separate MariaDB instances
- `redis_vuln` / `redis_patched`: separate Redis instances
The lab does not modify CtrlPanel application source code.
The Dockerfiles only wrap the original container entrypoint to normalize Docker Desktop runtime permissions for:
```text
/var/www/html/storage
/var/www/html/bootstrap/cache
```
After fixing permissions, the wrapper executes the original product entrypoint.
---
## PoC Design
Primary PoC:
```text
poc/poc_http_only.py
```
Properties:
- Sends HTTP POST only
- Does not use `docker exec`
- Does not inspect containers
- Does not start reverse shells
- Uses harmless commands only: `id`, `whoami`, `hostname`
Helper script:
```text
poc/poc_lab.py
```
Purpose:
- Sends the same HTTP request
- Verifies proof inside containers using `docker compose exec`
- Intended for demo and regression testing only
Proof file inside the app container:
```text
/var/www/html/storage/logs/cve_2026_34234_proof.txt
```
---
## Run
Start from a clean lab state:
```bash
docker compose down -v --remove-orphans
docker compose up -d --build
```
Wait until the app containers are up, then run:
```bash
python3 poc/poc_lab.py
```
Expected output:
```text
== 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
```
---
## Manual HTTP-only Test
Send the HTTP-only PoC to the vulnerable target:
```bash
python3 poc/poc_http_only.py --target http://127.0.0.1:8081
```
Verify proof manually:
```bash
docker compose exec vuln sh -lc 'cat /var/www/html/storage/logs/cve_2026_34234_proof.txt'
```
Expected proof:
```text
uid=1000(laravel) gid=1000(laravel) groups=1000(laravel)
laravel
```
Run the same request against patched:
```bash
python3 poc/poc_http_only.py --target http://127.0.0.1:8082
```
Verify patched behavior:
```bash
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"'
```
Expected:
```text
no proof file
```
---
## Cleanup
Remove containers, networks, and lab volumes:
```bash
docker compose down -v
```
---
## Notes
- This lab is for local security research only.
- Do not run the PoC against systems you do not own or have permission to test.
- The proof is intentionally limited to local command output inside the container.
- The vulnerable and patched services use separate databases and Redis instances.
- The fake API exists only to emulate the minimum Pterodactyl API responses required by the installer flow.
---
## Disclaimer
This repository is provided for educational security research and defensive validation only.
All demonstrations are intended to run inside the provided local Docker lab environment. The proof-of-concept avoids destructive actions, persistence, credential theft, data exfiltration, and real-world targeting.
Do not use this project against any system without explicit authorization. The author is not responsible for misuse or damage resulting from this material.
---
## References
- GitHub Security Advisory: https://github.com/Ctrlpanel-gg/panel/security/advisories/GHSA-jmhr-q9q5-fqwh
- CVE Record / NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-34234
- Patched release: https://github.com/Ctrlpanel-gg/panel/releases/tag/1.2.0
- Upstream repository: https://github.com/Ctrlpanel-gg/panel