Share
## 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