ImpressCMS: from unauthenticated SQL Injection to RCE

According to the official website ImpressCMS is an open source Content Management System (CMS) designed to easily and securely manage multilingual web sites. With this tool maintaining the content of a website becomes as easy as writing a word document. ImpressCMS is the ideal tool for a wide range of users: from business to community users, from large enterprises to people who want a simple, easy to use blogging tool. ImpressCMS is a powerful system that gets outstanding results and it’s free!

The application comes with a built-in security module – Protector – which is designed to improve the overall security of ImpressCMS websites and prevent certain web attacks such as Cross-Site Scripting (XSS) and SQL Injection. In this blog post we will see how to bypass such a security mechanism to exploit a couple vulnerabilities I discovered about a year ago, which might eventually allow unauthenticated attackers to execute arbitrary PHP code on the web server (RCE)…

• Vulnerabilities analysis:

Let’s start to analyze the two vulnerabilities which can be exploited in tandem to bypass access control (KIS-2022-03) and reach a script vulnerable to SQL Injection (KIS-2022-04). Both of them are located in the /include/findusers.php script, which is intended to be used by authenticated users to search for other users. However, due to the following vulnerable lines of code, it can be accessed by unauthenticated attackers as well:

16include "../mainfile.php";
17xoops_header(false);
18 
19$denied = true;
20if (!empty($_REQUEST['token'])) {
21    if (icms::$security->validateToken($_REQUEST['token'], false)) {
22        $denied = false;
23    }
24} elseif (is_object(icms::$user) && icms::$user->isAdmin()) {
25    $denied = false;
26}
27if ($denied) {
28    icms_core_Message::error(_NOPERM);
29    exit();
30}

The “elseif” statement at lines 24-26 will check whether the user is currently authenticated and they have administrator privileges, if so it will grant access to the script functionalities. While the “if” statement at lines 20-23 will do the same by solely checking the provided security token, without verifying whether the user is currently authenticated or not. This means that if an attacker provides a valid security token, then they will get unauthorized access to the script. Such security tokens will be generated in several places within the application – just grep the code searching the string icms::$security->getTokenHTML() – and some of them do not require the user to be authenticated, like the misc.php script, here at line 181.

Moving forward to some lines later we can see the following:

281$total = $user_handler->getUserCountByGroupLink(@$_POST["groups"], $criteria);
282 
283$validsort = array("uname", "email", "last_login", "user_regdate", "posts");
284$sort = (!in_array($_POST['user_sort'], $validsort)) ? "uname" : $_POST['user_sort'];
285$order = "ASC";
286if (isset($_POST['user_order']) && $_POST['user_order'] == "DESC") {
287    $order = "DESC";
288}
289 
290$criteria->setSort($sort);
291$criteria->setOrder($order);
292$criteria->setLimit($limit);
293$criteria->setStart($start);
294$foundusers = $user_handler->getUsersByGroupLink(@$_POST["groups"], $criteria, TRUE);

At lines 281 and 294 the “groups” POST parameter is being used in a call to the getUserCountByGroupLink() and getUsersByGroupLink() methods from the icms_member_Handler class, and both of them use the first argument to construct an SQL query without proper validation (assuming it is an array of integers), as shown in the following code snippet:

512public function getUserCountByGroupLink($groups, $criteria = null) {
513    $ret = 0;
514 
515    $sql[] = "  SELECT COUNT(DISTINCT u.uid) "
516            . " FROM " . icms::$xoopsDB->prefix("users") . " AS u"
517            . " LEFT JOIN " . icms::$xoopsDB->prefix("groups_users_link") . " AS m ON m.uid = u.uid"
518            . " WHERE 1 = '1'";
519    if (! empty($groups)) {
520        $sql[] = "m.groupid IN (" . implode(", ", $groups) . ")";
521    }
522    if (isset($criteria) && is_subclass_of($criteria, 'icms_db_criteria_Element')) {
523        $sql[] = $criteria->render();
524    }
525    $sql_string = implode(" AND ", array_filter($sql));
526    if (! $result = icms::$xoopsDB->query($sql_string)) {
527        return $ret;
528    }
529    list($ret) = icms::$xoopsDB->fetchRow($result);
530    return $ret;
531}

To sum up, a remote unauthenticated attacker might be able to manipulate the executed SQL queries, and this could be exploited to e.g. read sensitive data from the “users” database table through boolean-based SQL Injection attacks, without the knowledge of the tables prefix (which is randomly generated during the installation). This is possible by injecting a payload like this:

1) AND ORD(SUBSTR(u.pass,1,1)) = XX #

At a first glance, this seems to be a quite limited vulnerability: first of all, users’ passwords are hashed with “salting”, so they cannot be cracked without first disclosing the salt; another option would be leaking the admin’s email address and attack the password reset mechanism, but this won’t do the trick, because a random password will be generated and emailed to the user… So, here comes the question: is it possible to leverage these vulnerabilities to login into ImpressCMS as admin and escalate the attack to an RCE? And the answer is: yesss! However, we have to deal with the Protector module…

• Exploitation:

In a nutshell, without going deeper into the details, the anti SQL Injection measures provided by the Protector module check for suspicious strings within the request parameters, such as select, concat, or information_schema, and if they are found then the request will be blocked and the event will be logged. As such, we can’t use something like UNION SELECT… to complete the query and fetch data from an arbitrary table. On the other hand, (by default) ImpressCMS uses PDO as a database driver, which allows execution of stacked SQL queries separated by a semicolon. So, we can inject something like this:

0); INSERT INTO i36fd6f18_users (uname, pass) VALUES (0x65676978, 0x32333964) #

This will create a new record into the “users” database table, allowing an attacker to login as an ImpressCMS administrator, which would mean game over! However, in order to do that, the attacker should first guess the database tables prefix… And this could be achieved again with a boolean-based SQL Injection attack, by injecting something like the following:

1) AND ORD(SUBSTR((SELECT table_name FROM information_schema.tables WHERE table_schema=impresscms AND table_name LIKE %users), 1, 1)) = XX #

Unfortunately, this one will get blocked by the Protector module because it contains suspicious SQL strings, so we need to find another way… And here it comes: since stacked queries are allowed, an attacker might be able to bypass the Protector module by assigning to a variable the hex representation of the query they want to execute (by using SET), and then use the PREPARE and EXECUTE MySQL statements to ultimately execute the query. This means we should inject something like this:

0); SET @q = 0x53454c45435420534c454550283129; PREPARE stmt FROM @q; EXECUTE stmt; #

This one will not be catched by the Protector module, because the “suspicious strings” are hex-encoded! At this point we have all the pieces to put together the puzzle, and here are all the steps to get from unauthenticated SQL injection to RCE:

  • Retrieve a valid security token from /misc.php?action=showpopups&type=friend
  • Use the token to get unauthorized access to /include/findusers.php
  • Exploit the SQL injection in a boolean-based fashion to fetch the database name
  • Exploit the SQL injection in a time-based fashion to fetch the tables prefix (by using the trick to bypass Protector)
  • Exploit the SQL injection to create a new admin user
  • Login as admin and abuse the “Auto Tasks” feature to execute arbitrary PHP code

Here you can find a full working Proof of Concept (PoC) script which reproduces the above steps. It’s a PHP script supposed to be used from the command line (CLI), and you should see an output like the following:

Conclusion

I think it’s very hilarious and ironic that Prepared Statements, which are generally intended as a protection against SQL injection vulnerabilities, can also be abused to bypass a security mechanism designed to prevent SQL injection attacks! Furthermore, I have a feeling that this SQL injection exploitation technique can be used to bypass most Web Application Firewalls (WAF) out there, and this makes me think about a lesson I learnt some time ago: application security is a process, not a product!

Probably WAF vendors will point the finger at me, but I truly believe that too often the concept of “application security” is being confused with the “network security” one, cause most people think they are safe just because they have a firewall: ok, you can also implement security protections at the application level, like a WAF, and by doing that the overall application security could be definitely increased. On the other hand, I believe these solutions shouldn’t be considered bullet proof, as they can’t completely save your ass if you also have security bugs in your code, and this ImpressCMS case is a clear example of that.

Update – March 26, 2022

During these days I’ve been wondering whether my feeling was a good one or not, so I decided to test some web application firewalls to figure out if this technique could actually be abused to bypass popular WAF products. Here are some results:

• Case study 1: OWASP ModSecurity CRS

Like described in the official page, OWASP ModSecurity Core Rule Set (CRS) is a set of generic attack detection rules for use with ModSecurity or compatible web application firewalls, which provides protection against many common attack categories, including SQL Injection, Cross-Site Scripting, Local File Inclusion, etc… When configured with Paranoia Level 1, such as by default configuration, its SQL Injection detection rules can be fooled by using a slightly modified version of the above technique: CRS also relies on libinjection to detect SQL Injection attacks, in which I discovered a bug that allows to bypass the library’s detection mechanism. The payload that would bypass the detection looks like this:

0); SHOW WARNINGS; SET @q = 0x53454c45435420534c454550283129; PREPARE stmt FROM @q; EXECUTE stmt; #

This will bypass libinjection detection rules, but not all CRS rules, because for the above payload it also detects a Remote Command Execution pattern with the “SET ” string – specifically, it detects “Windows Command Injection”. So, we can just omit the whitespace after “SET” and bypass this CRS rule as well:

0); SHOW WARNINGS; SET@q = 0x53454c45435420534c454550283129; PREPARE stmt FROM @q; EXECUTE stmt; #

Don’t ask me why, but this SET@q syntax, without the whitespace in between, will work both on MySQL 8 and MariaDB 10! Anyway, yesterday I reported this Paranoia Level 1 (PL1) bypass to security@coreruleset.org, and they promptly replied saying:

A PL1 bypass is unfortunate, but also not highly critical from a CRS perspective. I mean, there will always be bypasses and if it’s only on PL1, then we are lucky… So yes, I suggest you create a github issue for the libinjection bug and we will see if we find a way to detect this at PL1.

• Case study 2: Cloudflare WAF

Cloudflare is a web infrastructure and website security company that mainly provides content delivery network (CDN) and DDoS mitigation services. They also have a WAF which – according to the official company website – is the cornerstone of their advanced application security portfolio that keeps applications and APIs secure. Well, I was too curious not to try whether their web application firewall would have catched this “new” SQL injection technique. So, I decided to buy a Pro plan for my website, and tried it out:

When you buy a Pro plan, you get access to more WAF features, and by default Cloudflare provides four “Managed Rulesets“: Cloudflare Leaked Credentials Check, Cloudflare Managed Ruleset, Cloudflare OWASP Core Ruleset, and Cloudflare Free Managed Ruleset (which is the only one available for free plans). Since I was already aware of the bypass working with OWASP CRS, I only deployed the “Cloudflare Managed Ruleset“, which has a predefined set of 40+ rules to detect SQL injection attacks… Well, none of them detected the above SQL injection payloads:

• Final Exploit

At this point I wanted to modify the above PoC script to make it working even when ImpressCMS is behind a web application firewall like OWASP ModSecurity CRS (Paranoia Level 1) or the Cloudflare WAF. So, I’ve modified it a bit, and pressed enter… After a while, I saw the message “Something went wrong!”: basically, CRS rules were blocking the PHP code injection request, so that the autotask with arbitrary PHP code were not be created. This means I had to find another bypass in CRS (Paranoia Level 1), and after playing a bit, I found the first bypass with the following PHP payload:

$v='_SERVER';$c=$$v['HTTP_CMD'];if(isset($c)){print(____);$o=`$c`;print($o);die;}

This leverages the PHP backtick operator to execute arbitrary OS commands. However, for some reason this code wouldn’t execute through the ImpressCMS “Auto Tasks” feature, so I had to find another way… And then I came up with a simpler solution:

print(____); $o=`id`; print($o); print(____);

This bypassed all CRS PL1 rules! However, if we try to execute another command such as ls -al it will be detected by CRS. So, here it is a further bypass to execute arbitrary commands – just hex-encode the command and then use the backtick operator:

print(____); $c="\x6c\x73\x20\x2d\x61\x6c"; $o=`$c`; print($o); print(____);

We’re finally done: here you can find the modified version of the PoC, which will bypass both the combos ImpressCMS Protector + OWASP ModSecurity CRS (Paranoia Level 1), and ImpressCMS Protector + Cloudflare WAF (and maybe other WAFs too?).