VMware Carbon Black App Control漏洞分析
碎碎念
“像是發起了一場衝鋒,確沒有找到敵人,無疾而終”
漏洞分析
關於VMware Carbon Black App Control詳細在這不贅述,詳細可到【經典回顧系列】CVE-2021-21988 VMware Carbon Black App Control認證繞過漏洞分析文章中查看。
漏洞介紹
查看了一下漏洞大概,CVE-2021-21998這個漏洞是因爲路由轉發的時候,不支持URL編碼,使用URL編碼未授權訪問接口獲取認證Token。
漏洞分析
環境分析
下載cwp.ova,導入鏡像,進行導入即可
後端是java啓動的,分別在3030,3010,3020端口
80和443端口是envoy啓動的,查看靜態配置文件
Envoy
的幾個clusters映射
相關配置文件
service_vsw -> 127.0.0.1:3030
service_apw -> 127.0.0.1:3020
service_acs -> 127.0.0.1:3010
service_plugin -> 127.0.0.1:8080
相關配置內容
clusters:
-
connect_timeout: 5s
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 3030
lb_policy: round_robin
name: service_vsw
type: LOGICAL_DNS
-
connect_timeout: 5s
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 3020
lb_policy: round_robin
name: service_apw
type: LOGICAL_DNS
-
connect_timeout: 5s
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 3010
lb_policy: round_robin
name: service_acs
type: LOGICAL_DNS
-
connect_timeout: 5s
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 8080
lb_policy: round_robin
name: service_plugin
type: LOGICAL_DNS
-
connect_timeout: 5s
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 3020
name: configServer
type: STATIC
listeners:
-
/usr/java/jre-vmware/bin/java -server -XX:+UseG1GC -Xms128M -Xmx256M -XX:+UseStringDeduplication -XX:ErrorFile=/var/log/cwp/access-control-service-java-error%%p.log -Xlog:gc:file=/var/log/cwp/access-control-service-gc.log::filesize=200M,filecount=2 -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cwp/heapdump/access-control-service/access-control-service.hprof -jar /opt/vmware/cwp/access-control-service/lib/access-control-service-1.0.1-SNAPSHOT.jar --logging.file=/var/log/cwp/access-control-service.log
/usr/java/jre-vmware/bin/java -server -XX:+UseG1GC -Xms128M -Xmx512M -XX:+UseStringDeduplication -XX:ErrorFile=/var/log/cwp/vsphere-worker-java-error%%p.log -Xlog:gc:file=/var/log/cwp/vsphere-worker-gc.log::filesize=200M,filecount=2 -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cwp/heapdump/vsphere-worker/vsphere-worker.hprof -Dservice-token=eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJ2c3ciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJuYmYiOjE2NjM3MDYyODAsImV4cCI6MTc1MDEwNjI4MCwicG9saWN5Ijp7InJvbGUiOiJTRVJWSUNFX1VTRVIiLCJwZXJtaXNzaW9ucyI6eyIqIjpbIioiXX19LCJyZWZyZXNoYWJsZSI6ZmFsc2UsImlhdCI6MTY2MzcwNjI4MH0.jGj3j4qNS_6pZUCHTxbpu-QYPr_ejveRsoQ7tOe8Gr_i1DNg6EbR1KALvlc8HxcV70vo5Sa4uyJykq0vzWQcnT29X-6Oev6lhwAwuAf3Vb2srg_RV8rtd7t9gbWVKzh0ZFC8_lexvgS9RRPZ0D8obrrnj74O24C6Cedabj-ynYW5AB4yDyJSHOrjr8uK2NcyL-B6IZ3KSfWeMa1DLDl4p9LEWwfoG1kZfVjJVVx-hUMsUcOwgTjcWFhoGiHVOu7BHc3QDKD4GTVStALskM-1o4EOUaeOdKTBBzlw_7MXp9fagw2_kpFOZPp9YlM5wyNO5o4ujsDIMUJhWjI0p5oQ-Q -jar /opt/vmware/cwp/vsphere-worker/lib/vsphere-worker-1.0.1-SNAPSHOT.jar --logging.file=/var/log/cwp/vsphere-worker.log
/usr/java/jre-vmware/bin/java -server -XX:+UseG1GC -Xms128M -Xmx512M -XX:+UseStringDeduplication -XX:ErrorFile=/var/log/cwp/appliance-worker-java-error%%p.log -Xlog:gc:file=/var/log/cwp/appliance-worker-gc.log::filesize=200M,filecount=2 -XX:-OmitStackTraceInFastThrow -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/cwp/heapdump/appliance-worker/appliance-worker.hprof -Dservice-token=eyJhbGciOiJSUzUxMiJ9.eyJzdWIiOiJhcHciLCJpc3MiOiJ1c2VyLXNlcnZpY2UiLCJuYmYiOjE2NjM3MDYyODAsImV4cCI6MTc1MDEwNjI4MCwicG9saWN5Ijp7InJvbGUiOiJTRVJWSUNFX1VTRVIiLCJwZXJtaXNzaW9ucyI6eyIqIjpbIioiXX19LCJyZWZyZXNoYWJsZSI6ZmFsc2UsImlhdCI6MTY2MzcwNjI4MH0.kvJ2Gn9-FsNQ-nARtdUnkOydpmGZptrn1cH_NPIokHkxDmBbs61LoHyMdkv6JzMm9gqqns0fOf3iXe_toENIuNrwmhwDcp1TZF-He9quXfu_IQ_YdRGv6uX_nfgU33Ndy9L_pGFJPYiLSoaRrgdw_b04BMc-4OQeei2hB9vvlZpNHYWLZoiWOA7Vgb3c5btb965_FFECgBmPGeqRLovUeJVqisL6Z4-ceXfEhXC5pVhH9cy7UdYPWn6DRQkCvX0yIKXJb3lD-kBmS5Jp9QW7c2L9AQPYHzRCwvYU2hG5OcabmhXs_PgbpNlbNPHcEjjO5DRDgSxqdQRB6PWTr3cpsw -jar /opt/vmware/cwp/appliance-worker/lib/appliance-worker-1.0.1-SNAPSHOT.jar --logging.file=/var/log/cwp/appliance-worker.log
代碼中com.vmware.cwp.appliance.applianceworker.service.impl.EnvoyXDSServiceImpl#routeDiscovery
類來進行路由的映射轉發
public String routeDiscovery(final DiscoveryRequest discoveryRequest) {
CloudSettingsDTO cloudSettings = this.cloudSettingsService.getSettings("FULL_DETAILS");
if (Objects.equals(this.cloudSettings, cloudSettings)) {
this.applianceService.registerAppliance();
}
this.cloudSettings = cloudSettings;
String hostName = this.getHostName(cloudSettings);
String applianceIPv4Address = this.networkService.getNetworkConf().getIpv4Address();
Iterable headers = this.populateHeaders();
Route rds_block = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/apw/v2/discovery:routes").build()).setRoute(RouteAction.newBuilder().setCluster("service_vsw").setPrefixRewrite("/no_cloud").build()).build();
Route service_token_block = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/acs/api/v1/service-token").build()).setRoute(RouteAction.newBuilder().setCluster("service_vsw").setPrefixRewrite("/no_cloud").build()).build();
Route cds_block = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/apw/v2/discovery:clusters").build()).setRoute(RouteAction.newBuilder().setCluster("service_vsw").setPrefixRewrite("/no_cloud").build()).build();
Route apw = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/apw/").build()).setRoute(RouteAction.newBuilder().setCluster("service_apw").setPrefixRewrite("/").setHostRewrite(applianceIPv4Address).build()).build();
Route no_cloud_connectivity = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/plugin/index.html").build()).setRoute(RouteAction.newBuilder().setCluster("service_apw").setPrefixRewrite("/no_cloud").setHostRewrite(applianceIPv4Address).build()).build();
Route plugin_api = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/plugin/api/").build()).setRoute(RouteAction.newBuilder().setCluster("api_cloud_cluster").setHostRewrite(hostName).setPrefixRewrite("/").build()).addAllRequestHeadersToAdd(headers).build();
Route vc_plugin_ui = Route.newBuilder().build();
if (this.hasSupportingPluginURL()) {
vc_plugin_ui = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/plugin/").build()).setRoute(RouteAction.newBuilder().setCluster("plugin_cluster").setHostRewrite(this.getPluginHostURL(cloudSettings).getHost()).setPrefixRewrite(this.getPluginHostURL(cloudSettings).getUrlReWrite()).build()).build();
}
Route apw_vc_plugin = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/plugin/apw/").build()).setRoute(RouteAction.newBuilder().setCluster("service_apw").setPrefixRewrite("/").setHostRewrite(applianceIPv4Address).build()).build();
Route vsw_vc_plugin = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/plugin/vsw/api/").build()).setRoute(RouteAction.newBuilder().setCluster("service_vsw").setPrefixRewrite("/vsw/api/").setHostRewrite(applianceIPv4Address).build()).build();
Route inventory = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/inventory/").build()).setRoute(RouteAction.newBuilder().setCluster("api_cloud_cluster").setHostRewrite(hostName).build()).addAllRequestHeadersToAdd(headers).build();
Route lcm = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/lcm/").build()).setRoute(RouteAction.newBuilder().setCluster("api_cloud_cluster").setHostRewrite(hostName).build()).addAllRequestHeadersToAdd(headers).build();
Route acs = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/acs/").build()).setRoute(RouteAction.newBuilder().setCluster("service_acs").setHostRewrite(applianceIPv4Address).build()).build();
Route vsw = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/vsw/").build()).setRoute(RouteAction.newBuilder().setCluster("service_vsw").setTimeout(Duration.newBuilder().setSeconds(60L)).setHostRewrite(applianceIPv4Address).build()).build();
Route appliance = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/applianceservice/").build()).setRoute(RouteAction.newBuilder().setCluster("api_cloud_cluster").setHostRewrite(hostName).build()).addAllRequestHeadersToAdd(headers).build();
Route default_route = Route.newBuilder().setMatch(RouteMatch.newBuilder().setPrefix("/").build()).setRedirect(RedirectAction.newBuilder().setPathRedirect("/apw/login").build()).build();
Builder virtualHostOrBuilder = VirtualHost.newBuilder().setName("backend").addDomains("*");
virtualHostOrBuilder.addRoutes(cds_block);
virtualHostOrBuilder.addRoutes(rds_block);
virtualHostOrBuilder.addRoutes(service_token_block);
if (this.hasCloudURL(cloudSettings)) {
virtualHostOrBuilder.addRoutes(appliance);
}
if (this.hasSupportingPluginURL()) {
virtualHostOrBuilder.addRoutes(plugin_api);
virtualHostOrBuilder.addRoutes(apw_vc_plugin);
virtualHostOrBuilder.addRoutes(vsw_vc_plugin);
virtualHostOrBuilder.addRoutes(vc_plugin_ui);
virtualHostOrBuilder.addRoutes(lcm);
virtualHostOrBuilder.addRoutes(inventory);
} else {
virtualHostOrBuilder.addRoutes(no_cloud_connectivity);
}
virtualHostOrBuilder.addRoutes(acs);
virtualHostOrBuilder.addRoutes(apw);
virtualHostOrBuilder.addRoutes(vsw);
virtualHostOrBuilder.addRoutes(default_route);
VirtualHost virtualHost = virtualHostOrBuilder.build();
RouteConfiguration routeConfiguration = RouteConfiguration.newBuilder().setName("route").addVirtualHosts(virtualHost).build();
DiscoveryResponse discoveryResponse = DiscoveryResponse.newBuilder().setVersionInfo("1").addResources(Any.pack(routeConfiguration)).build();
TypeRegistry typeRegistry = TypeRegistry.newBuilder().add(DiscoveryResponse.getDescriptor()).add(ClusterLoadAssignment.getDescriptor()).add(RouteConfiguration.getDescriptor()).build();
String response = null;
try {
response = JsonFormat.printer().usingTypeRegistry(typeRegistry).print(discoveryResponse);
} catch (InvalidProtocolBufferException var28) {
log.error("Error while serializing response", var28);
}
return response;
}
以以上圖舉例,如果匹配acs
前綴則走到service_acs
這個clusters中。這個clusters在前面配置文件可見
hosts:
-
socket_address:
address: "127.0.0.1"
port_value: 3010
lb_policy: round_robin
name: service_acs
type: LOGICAL_DNS
轉發到3010端口處理,以此類推。
漏洞分析
再來看到漏洞,漏洞位置在com.vmware.cwp.appliance.acs.api.controller.TokenGeneratorApi#getServiceToken
public ResponseEntity getServiceToken(final String serviceName) {
AccessTokenDTO response = this.tokenGeneratorService.getServiceToken(serviceName);
return new ResponseEntity(response, HttpStatus.OK);
}
public AccessTokenDTO getServiceToken(final String serviceName) {
if (StringUtils.isBlank(serviceName)) {
return (new AccessTokenDTO()).token("XXXXXX.XXXXXXXXXX.XXXXX");
} else {
JwtRbacPolicy jwtRbacPolicy = this.getJwtRbacPolicy("SERVICE_USER");
Date expiryDate = new Date(System.currentTimeMillis() + this.serviceProperties.getServiceTokenDuration().toMillis());
return (new AccessTokenDTO()).token(this.tokenGenerator.generate(jwtRbacPolicy, serviceName, expiryDate, false));
}
}
調用JwtTokenGenerator
生成token進行返回。
構造一下請求,該路由對應的是access-control-service
模塊,需要加入acs
前綴
https://192.168.31.84/acs/api/v1/service-token/admin
請求會發現404
回到com.vmware.cwp.appliance.applianceworker.service.impl.EnvoyXDSServiceImpl#routeDiscovery
以上可以看到/acs/api/v1/service-token
被他轉發到service_vsw
中去了,即便轉發到3030端口,對應vsphere-worker
模塊
該模塊並沒有該路由,本地請求測試
發現直接請求3010端口是可以未授權獲取token的。
回看到這個地方,發現/acs/api/v1/service-token/
,優先級高於/acs
漏洞是使用了URL編碼,在來到envoy的時候,並不會自動URL解碼。所以不走/acs/api/v1/service-token/
,路由規則,走到下面的acs
規則,使端口轉發到3010,而3010端口的springboot是會解析url編碼的,從而走入/api/v1/service-token/{serviceName}
這條路由規則中。
Envoy
框架默認是不啓動URL解析,需要手工開啓。使用RBAC過濾器
時啓用normalize_path_settings
特性。
Reference
CVE-2021-21988 VMware Carbon Black App Control認證繞過漏洞分析
結尾
調完該漏洞,挺巧妙,在envoy層進行繞過。