问答
发起
提问
文章
攻防
活动
Toggle navigation
首页
(current)
问答
商城
实战攻防技术
漏洞分析与复现
NEW
活动
摸鱼办
搜索
登录
注册
CVE-2024-45216 Apache Solr 身份验证绕过漏洞简析
漏洞分析
在目前可得的描述中可以得出这个漏洞主要是因为使用 PKIAuthenticationPlugin 的 Solr 实例(在使用 Solr 身份验证时默认启用)容易受到身份验证绕过的影响,下来着重分析绕过数据的传递过程
前言 == 最近solr爆出了新的身份绕过漏洞,工作中要对该漏洞进行复现,正好将分析的过程记录一下 在通报中可以看到该漏洞的评级那是相当高,CVSS3直接高达9.8分,在描述中可以得出这个漏洞主要是因为使用 PKIAuthenticationPlugin 的 Solr 实例(在使用 Solr 身份验证时默认启用)容易受到身份验证绕过的影响 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-3af5a6cea4ceedfd1424b9415008328e6e6b4de3.png) 在solr的身份验证中,PKIAuthenticationPlugin 是 Apache Solr 中用于身份验证的一个插件。它主要用于在分布式 SolrCloud 环境中,通过公钥基础设施(PKI)来实现节点之间的相互认证和通信安全 而PKIAuthenticationPlugin的验证是通过什么数据来体现的呢,通过查询官方文档可知对于每个传出的请求,PKIAuthenticationPlugin添加一个特殊的标题'SolrAuth',其中包含使用该节点的私钥加密的时间戳和主体。公钥通过API公开,任何节点只要需要就可以读取。 环境搭建 ==== 当我们初步了解了漏洞的基本信息之后,开始搭建环境,因为之前咱们说的PKIAuthenticationPlugi主要用于在分布式 SolrCloud 环境中,所以咱们要搭建一个SolrCloud集群环境 集群环境基本身份验证需要我们首先创建一个security.json文件,对于基本身份验证,security.json文件必须有一个authentication部分,它定义用于身份验证的类。可以在创建文件时添加用户名和密码(例如:sha256(password+salt) hash),或者可以稍后使用基本验证API添加。 下面的配置信息为: 1.启用基本身份验证和基于规则的授权插件。 2.参数 "blockUnknown": true 表示不允许未经身份验证的请求通过。 3.已定义了一个名为 "solr" 的用户,其中有密码 "SolrRocks"。 4."admin" 角色已定义,并且具有编辑安全设置的权限。 5."solr" 用户已被定义为 "admin" 角色。 ```php { "authentication": { "class": "solr.BasicAuthPlugin", "credentials": { "solr": "IV0EHq1OnNrj6gvRCwvFwTrZ1+z1oBbnQdiVC3otuq0= Ndd7LKvVBAaZIF0QAVi1ekCfAJXr1GGfLtRUXhgrF8c=" }, "blockUnknown": false, "": { "v": 0 } }, "authorization": { "class": "solr.RuleBasedAuthorizationPlugin", "permissions": [ { "name": "security-edit", "role": "admin" } ], "user-role": { "solr": "admin" } } } ``` 然后我们通过镜像来搭建集群环境 下面的配置文件docker-compose.yaml中首先要注意的就是把上面的权限配置设置复制到容器中,\[your-local-path\]security.json记得替换为自己的目录 然后在文件中我们也打开了5006的java远程调试端口 创建了一个solr的network网络 ```php version: '3.7' services: solr1: image: solr:9.6.0 container_name: solr1e ports: - "8983:8983" - "5006:5006" environment: - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 - SOLR_OPTS=-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5006 volumes: - [your-local-path]security.json:/var/solr/data/security.json networks: - solr depends_on: - zoo1 - zoo2 - zoo3 solr2: image: solr:9.6.0 container_name: solr2 ports: - "8982:8983" environment: - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 networks: - solr depends_on: - zoo1 - zoo2 - zoo3 solr3: image: solr:9.6.0 container_name: solr3 ports: - "8981:8983" environment: - ZK_HOST=zoo1:2181,zoo2:2181,zoo3:2181 networks: - solr depends_on: - zoo1 - zoo2 - zoo3 zoo1: image: zookeeper:3.8 container_name: zoo1 restart: always hostname: zoo1 ports: - 2181:2181 - 7001:7000 environment: ZOO_MY_ID: 1 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" networks: - solr zoo2: image: zookeeper:3.8 container_name: zoo2 restart: always hostname: zoo2 ports: - 2182:2181 - 7002:7000 environment: ZOO_MY_ID: 2 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" networks: - solr zoo3: image: zookeeper:3.8 container_name: zoo3 restart: always hostname: zoo3 ports: - 2183:2181 - 7003:7000 environment: ZOO_MY_ID: 3 ZOO_SERVERS: server.1=zoo1:2888:3888;2181 server.2=zoo2:2888:3888;2181 server.3=zoo3:2888:3888;2181 ZOO_4LW_COMMANDS_WHITELIST: mntr, conf, ruok ZOO_CFG_EXTRA: "metricsProvider.className=org.apache.zookeeper.metrics.prometheus.PrometheusMetricsProvider metricsProvider.httpPort=7000 metricsProvider.exportJvmInfo=true" networks: - solr networks: solr: ``` 在环境搭建之后,进入到solr1e的容器内部执行命令,这是因为在SolrCloude模式中必须上传security.json到ZooKeeper ```php solr zk cp /var/solr/data/security.json zk:/security.json -z zoo1:2181,zoo2:2181,zoo3:2181 ``` 通过账号 solr/SolrRocks登录上之后,因为咱们之前的blockUnknown设置的是false,所以需要手动开一下禁止未经身份验证的请求通过 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-f8414bfda46c5e2b4d8bfc6afff6d16f45168131.png) 漏洞分析 ==== 前期分析 ---- 在茫茫大海中搜寻,找到了当时的issues,该issues被创建于2024-8-21 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-5d39d44fad1f8fdcd9e0667f53905ee0aaab18bd.png) 在描述中我们可以看到作者是这样描述的 By using ":/admin/info/key" at the end of the URL, the PKIAuthenticationPlugin can be bypassed, so that non-authorized users can access protected APIs. 通过这个我们找到了代码修复位置 <https://issues.apache.org/jira/secure/attachment/13071024/SOLR-17417.patch> ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-3796850969b95d6e1a338b58ef678e2af643ce3b.png) 调试分析(GET请求) ----------- 找到位置了,开始调试分析具体过程,先构造一个符合漏洞的请求包 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-77de8d627c382db529c31e4f786924f80e482eef.png) 我们在dofilter过滤链中的身份验证哪里进行断点开始调试 运行到SolrDispatchFilter.java的authenticateRequest方法时,刚开始authenticationPlugin还是BasicAuthPlugin ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-855493dab5090c9393c715f5e1a1bd6ce9129562.png) 当继续运行后,在下面会对是否为PKIAuthenticationPlugin进行验证,当程序检测到我们呢请求头中的SolrAuth: aaaa时,就会把当前的验证authenticationPlugin更换为PKIAuthenticationPlugin ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-fc53060eb139e428306db5cd6dbb9b46e4218dd8.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-0aefbcc717b44e410149dd8c3fa04539a11863cf.png) 在最后根据authenticationPlugin的值执行不同身份验证插件的authenticate方法 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-42a826910c06c6bf3145c7b7497ce8fcd9933bf4.png) 我们在这里就进入到了漏洞代码处,也就是PKIAuthenticationPlugin.java的doAuthenticate方法 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-42719951ab5c52521f88dee69a3d6de353fded83.png) 在下面的153行代码以后才是PKIAuthenticationPlugin的decipherHeader方法对于SolrAuth的提取以及有效性验证,在最后如果验证全部通过之后会返回return ture ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-a0d46005136af24ea14bccd88470cc1fa52da987.png) 我们着重查看142行-151行的这部分代码,如果if语句符合条件,然后顺利执行完就能返回true,绕过了下来的验证部分。 这里先是提取出来了requesturI,然后检测requesturI是否是以PublicKeyHandler.PATH结尾 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-122f28db79f002a2269c49459bc11334a4030088.png) 我们查找,发现PATH的定义为/admin/info/key,也就是说只要我们访问的url链接只要是以它结尾就能进入if代码块 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-5f6d6f5157c70b43b3a88804978612b3d1333596.png) 然后通过filterChain.doFilter(request, response)将当前请求和响应传递给过滤器链中的下一个过滤器进行处理。 当前只是探索出了绕过的第一步,就是在结尾加上/admin/info/key,我们接着代码往下一个Filter过滤器分析 在下一个filter过滤器中,经过了dispatch()---->call()---->init(),直到init()方法中我们可以看到有一点微妙 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-23ce87029cab5d9a95fb3a86d174407391a0f562.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-80a581150405300fef6477c6c3ef544d90bf052f.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-6c8259bdc9d565157c58102b4bea4c5160a71cb2.png) 在init()方法中我们可以看到代码会对我们请求的url进行操作,从“:”开始截断,只获取“:”之前的路径,然后进入到cores.getRequestHandler对path进行检查 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-d0fcab6e3908e82e5260b26b33c790c40394cd69.png) 在getRequestHandler方法中我们可以看到会对我们传入的path,也就是handlerName进行检查,我们需要保证handler的值不能为空,如果是空的话,在上面的代码init方法中我们就无法进入if代码块,从而执行以下代码获得admin权限了: requestType = RequestType.ADMIN; action = ADMIN; ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-390f468cc4736c81b75b2300c30b86a68f2bb5d9.png) 进入get方法,发现只是查看name也就是咱们截断后的路径在不在registry中 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-f9a9998fd621db2684ccde8b12780c3c4b70d0b7.png) registry为: ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-5317fa2246de86a31d1ff1c502c615527bd4fcab.png) 整理后也就是下面的接口路径 ```php [/admin/zookeeper, /admin/configs, /admin/info/key, /admin/collections, /admin/authorization, /admin/cores, /admin/info, /admin/zookeeper/status, /admin/authentication, /admin/metrics] ``` 通过之后,我们继续看call()中init()后面的代码 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-9cddb9b24b58c6e50f50698776d853db8946a00b.png) 在这其中我们着重看一下authorize()方法 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-d33bad9ec068b77e63967e95a4d3b5aeb1cd4bf3.png) 我们先获取授权 authzPlugin = cores.getAuthorizationPlugin() 正常的authzPlugin数据为 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-5978de037c00b6b95956a535f9e42927ea5a93b4.png) 然后进入 authzPlugin.authorize(context)进行操作,对权限进行了一次校验,但是因为我们之前已经得到了admin权限,很容易就通过了 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-7cffc16babddb5cde4e2e3d0662e0e75b4e423b4.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-6fcf88459177ebaae4427c763e522bfd111cd4a0.png) 接下来一路代码会默认的走,在最后返回的时候有一个checkPathPerm()方法,进入该方法 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-8ba3005ccdf2f18847504deb26300f2c1e7b12c6.png) 在这段代码中一直在传递的permissions,它的值为{"name":"security-edit","role":"admin"},这个其实就是咱们在刚开始的security.json中设置的admin权限 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-4c36043195f5c2ec6174b814b7428b8801c3eafe.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-83a13dee661d1cb19572a94ba05d7a64993d7338.png) 我们继续,下来就是一些对于权限和咱们请求之间的判断,一路走到getPermissionName()方法 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-abdc1d3276adab24c6d915b37efb79270d86965d.png) 在这里的话会对get请求和post请求两个区分开,如果是get请求的话,会返回security-read,post的话是返回security-edit,这里要记住,咱们之前在搭建环境的时候设置的就是admin权限为security-edit ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-1f55aa223cab98ab0c2d009cbf8a1f4d6b8ba9aa.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-744582a92d3e9d06e5de812c3b50ea58ba3e587c.png) 随后会对咱们的security-edit和刚才获得的get请求的security-read进行了对比,得到applies为false ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-0678ec91a0888c7ad9c4505834e1337bf097e2c7.png) 那么返回到这里就是false,会跳转到return null进行返回 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-2ca5d2aa7f6abd40a33e3a246a84f8c98819fe38.png) 为null之后,checkPathPerm方法返回的就是rsp为200的一个返回值 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-56f2941b186fdab88e9389782d61e543bfe21692.png) 最后返回到authorize方法,我们可以看到statusCode就是刚才rsp.statusCode,也就是200,然后一路跳过下面四个if判断,成功返回null ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-a01df5bb051413391e1da657e0c43efdd1796c36.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-ec6f392d14c7da8341c1fe1f52ee4259cc690515.png) 执行完成返回到call()方法,authzFailure经过一系列判断为null,走到下面因为admin权限进入handleAdminRequest()执行,成功绕过了身份验证这一段得到了执行 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-aa8531e0fba313edd076a224c0b86d2d1cd1ea24.png) POST请求 ------ 上面的调试是GET请求进行的过程,但是我发现大家使用poc全都是止步于get请求,那么POST进行数据的修改可不可以呢,我们进行尝试 首先的话,接口肯定要挑咱们上面分析的registry支持的接口 整理后也就是下面的接口路径 ```php [/admin/zookeeper, /admin/configs, /admin/info/key, /admin/collections, /admin/authorization, /admin/cores, /admin/info, /admin/zookeeper/status, /admin/authentication, /admin/metrics] ``` 刚刚好,添加用户的接口/admin/authentication就在其中,咱们进行尝试,奇怪的是,竟然报了401 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-1a62016b66f872b02313ff263b47740fa54578cb.png) 造成这样的原因是因为在上面的get与post请求进行区分的时候,POST获得的是PermissionNameProvider.Name.SECURITY\_EDIT\_PERM,即为security-edit ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-f03707cd5e265ed9ea00b9d668423775c1fa208c.png) 咱们的admin权限为security-edit,于是两个进行判断之后的applies值为true ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-19958f5e1f4456600a6a33294259c236d88470e0.png) 在这里于是也因为if判断成功发生了变动,返回的是permission,并不是null ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-cda7357d37ac96c11bfa3d993df1c7f929bb14d0.png) 在这里并没有直接返回MatchStatus.NO\_PERMISSIONS\_FOUND,而是走到了下面determineIfPermissionPermitsPrincipal中进行了一次getUserPrincipal() ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-f77e51b3e46a39d578caa742bebc04189cad88ba.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-39be419a5f47f5f42b1a7e3ebac05beeddd2eb52.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-420f10a7cf6da44c53c7ccbf6f0ec7f5cccdc95b.png) 在这里进行了一次对当前身份的检测,咱们因为是绕过走到这里的,当然没有,返回为null,于是喜提401 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-41b01064ea9633311f0e4ee5a3ee527fb932452d.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-473589191700146fff3a4c2500316ba85273beae.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-97f96c1f79585a22b720ee2289b04906240c1c0d.png) 最后喜提401,"Authentication failed, Response code: 401" 那么,根本原因是在哪里的呢,就是这里,post请求的security-edit和咱们当前admin请求的对比 POST请求成功的条件 ----------- 当我们修改最初的权限 将admin的security-edit权限删去之后 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-d53fdd0e2f16d88322b0c3eb3e550bff3e735af3.png) 回到了perssions判断这里,因为之前get请求时,咱们的权限配置为{"name":"security-edit","role":"admin"},但是咱们这次将这些都删除了,所以在这里perssions自然为null ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-12d585fdf5b4d07ee851b15e04dbe3ee660f8537.png) 直接返回rep.statusCode=200,下来就直接运行成功了 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-8424b720be1df25210f611bfbd0c0666afbee3f9.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-399e2ab85355f462d58a5b317d99745ceb75329f.png) ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-e440171b85a01ba9253af9261100cffb5a74989f.png) 账号添加成功 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-9529dca439ac0cd48f42ba5640b7dac0c7dd995c.png) 总结 == 这次solr的权限绕过漏洞主要还是下面的这部分代码造成的。而在漏洞的分析复现中也是发现get和post竟然不都是成功的,post请求的利用会更加苛刻一些,再此也是简单的分析了一遍,下来更期待大佬们的进一步拓展。 ![image.png](https://shs3.b.qianxin.com/attack_forum/2024/11/attach-a4ffd1bd0b2af670c5979d75bfdad9cc44ff9dd2.png)
发表于 2024-11-19 09:00:02
阅读 ( 437 )
分类:
Web应用
0 推荐
收藏
0 条评论
请先
登录
后评论
鸣蜩十四
1 篇文章
×
发送私信
请先
登录
后发送私信
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!