VMware Carbon Black App Control漏洞分析
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层进行绕过。