We scanned the at the time current version 1.3.11 of ImpressCMS and found an unauthorized SQL Injection vulnerability. The exploit affects installations that use PDO as a database driver. The issue was fixed in version 1.4.0, though the patch does not follow best practices and might not be sufficient. The attack vector does not require user interaction, thus it can be used to automatically manipulate many ImpressCMS sites on a large scale. With this extremely dangerous vulnerability present, the users are highly advised to update their ImpressCMS installations.

The truncated analysis results are available in our RIPS demo application. Please note that we limited the results to the issues described in this post in order to ensure a fix is available.

Spotting the SQL Query

ImpressCMS modules are extensions that provide certain functionality. For instance, the content module allows users to post articles. The basic structure requires a directory for each module. If a user wants to edit content, a PHP file in the according subdirectory is called. Consequently, the directory name is part of the URL. To load the module data from the database, the directory name is used as an identifier. In the case of the content module the URL looks like this: https://example.com/modules/content/admin/content.php.

The susceptible class icms_module_Handler is in the Handler.php file and handles the modules of the system. Each request goes through the service method of that class to provide the according module which needs to be loaded. In detail, the directory name is determined through PHP_SELF, which is part of the $_SERVER superglobal environment information array. PHP_SELF contains a short part of the current URL. If a file inside the module directory is called, PHP_SELF contains such directory/module name.

For example, when calling /modules/profile/admin/user.php, the module profile needs to be loaded. Therefore, first, a substring from /modules/ to the end of the string is extracted. Second, that substring is separated by the slashes as seen in line 417. The array $url_array now has the string profile in the third position, index 2. If index 2 of the array is set the method getByDirname() is called with its value as seen in line 420.

htdocs/libraries/icms/module/Handler.php

416
417
418
419
420
421
if ($inAdmin || file_exists('./xoops_version.php') || file_exists('./icms_version.php')) {
  $url_arr = explode('/', strstr($_SERVER['PHP_SELF'], '/modules/'));
  if (isset($url_arr[2])) {
    /* @var $module icms_module_Object */
    $module = icms::handler("icms_module")->getByDirname($url_arr[2], TRUE);
    

In getByDirname() the parameter received as $dirname is not sanitized, concatenated into a SQL query, and executed right after in the following code:

htdocs/libraries/icms/module/Handler.php

133
134
$sql = "SELECT * FROM " . $this->db->prefix('modules') . " WHERE dirname = '" . trim($dirname) . "'";
if (!$result = $this->db->query($sql)) return $module;

Exploitation via PHP_SELF

To exploit the vulnerability, the $_SERVER['PHP_SELF'] value must be manipulated to set up $url_arr[2] so that the value contains an arbitrary SQL command. This can be achieved by changing the corresponding module directory name to a SQL statement inside the URL. Though the directory does not exist and thus the request results in a 404 file not found error. However, the vulnerable method in Handler.php is executed on any page, including pages outside of the modules directory.

So to exploit it we call /admin.php instead because this URL does not try to load a module already. We can append slashes to that URL and it still resolves to the admin.php file, e.g. /admin.php/foo, which is crucial for this exploit. This results in PHP_SELF being user-controlled after the file name, a fact many developers are not aware of. As seen in this exploit the PHP_SELF value was helpful to determine the directory name, but the developers did not think of it being user-controlled and therefore, dangerous in the context of the method.

Calling https://example.com/admin.php/modules/bar has the consequence that the script admin.php in the web root is executed and $_SERVER['PHP_SELF'] has the value /admin.php/modules/bar. The code from line 417 onwards proceeds, trying to load the “bar” module. As this leads us to the SQL query, we can manipulate “bar” to inject a SQL statement. Calling /admin.php/modules/' OR SLEEP(1000) -- - is an example for escaping the single quote and executing an arbitrary SQL command.

Furthermore, although we are not logged in, the variable $isAdmin is set to true. This allows us to pass the Handler.php check in line 416.

Patch

The best practice to prevent malicious statements from being injected is to use prepared statements. As the vendor never used prepared statements in their code, this fix can not be applied. We do not want to rewrite a large base of the code and therefore, only provide a hotfix. The single quote, which allowed us to break out of the string, needs to be escaped using the quote() method. Replace line 133 of the Handler.php file with the following:

htdocs/libraries/icms/module/Handler.php

133
$sql = "SELECT * FROM " . $this->db->prefix('modules') . " WHERE dirname = " . $this->db->quote(trim($dirname));

Summary

In this blog post, we have seen that the innocent appearing $_SERVER['PHP_SELF'] value is user-controlled and can lead to vulnerabilities like every other user input. Often it is not obvious which variables can contain user input, thus it is necessary to always use prepared statements and sanitize inputs to maintain a secure application.

We contacted the vendor about the vulnerability but did not receive an answer to our request. The issue was resolved in version 1.4.0, though, the patch does not follow best practices and thus its security can not be guaranteed.

Timeline

DateEvent
12/09/19First contact attempt via contact form
12/16/19Second contact attempt via contact form
01/08/20Third contact attempt via email setting 30 days deadline