对xxe漏洞的一些思考
XML的基础知识
什么是XML
我在知乎上找到相对通俗的解释,如下:
以一种约定的字符串来表示某些常用数据类型,例如Map或者Array、Two-Dimensional Array。 因为双方都有约定,不同的应用就可以通过这些字符串进行数据的交换。 例如作为配置文件,那么应用本身可以读取这些配置,而文本编辑软件也可以按照约定的格式来编辑这些字符串,那么就实现了人肉和应用的数据的交换。 又例如作为网络应用,服务端发出这些字符串,客户端接收到后parse,就可以实现服务端传输一个Map的数据(举例)到客户端。 另外XML带有大量信息冗余,这样也方便人的阅读。
文档结构
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素。
1 | <!--XML声明--> |
什么是DTD
文档类型定义(DTD)可定义合法的XML文档构建模块,它使用一系列合法的元素来定义文档的结构。DTD 可被成行地声明于XML文档中(内部引用),也可作为一个外部引用。
内部声明DTD
1 | <!DOCTYPE 根元素 [元素声明]> |
引用外部DTD
SYSTEM
关键字表示DTD文件是私有的
1 | <!DOCTYPE 根元素 SYSTEM "文件名"> |
引用公有的DTD
1 | <!DOCTYPE 根元素名称 PUBLIC "DTD名称" "公用DTD的URI"> |
什么是实体
实体其实可以理解为是一个变量,我们可以在 XML 中通过&
符号进行引用。从上述的引用方式来看,实体可以分为内部实体
与外部实体
。从另一个角度来说,实体又可以分为通用实体
和参数实体
。我们一个个来说。
内部实体
示例代码:
1 | "1.0" encoding="ISO-8859-1" xml version= |
这里 定义元素为ANY
说明接受任何元素,但是定义了一个 xml 的实体,那么 XML 就可以写成这样
1 | <message> |
我们使用&xxe
对 上面定义的 xxe 实体进行了引用,到时候输出的时候&xxe
就会被"flag"
替换。
外部实体
示例代码:
1 | "1.0" encoding="ISO-8859-1" xml version= |
通用实体
我个人的理解是与上述外部实体是一回事,只是叫法不同
参数实体
(1)使用 % 实体名
(这里%后面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名;
来进行引用
(2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
(3)和通用实体一样,参数实体也可以外部引用
xxe能做什么?
PHP在安装扩展以后还能支持的协议:
有回显读取本地文件
示例代码:
xml.php
1 | <?php |
payload:
1 | <?xml version="1.0" encoding="utf-8"?> |
可以看到文件内容被读取了,结果如下:
然后我们创建一个文件test.txt
,内容如下
然后尝试读取时,发现报错了,结果如下:
CDATA标签
CDATA 全名:character data。所有 XML 文档中的文本均会被解析器解析,除了 CDATA 区段(CDATA section)中的文本会被解析器忽略。
CDATA的形式如下:<![CDATA[文本内容]]>
。
CDATA的文本内容中不能出现字符串“]]>”。另外,CDATA不能嵌套。
XML 实例: 在CDATA标记中的信息被解析器原封不动地传给应用程序,并且不解析该段信息中的任何控制标记。 CDATA区域是由<![CDATA["为开始标记,以“]]>
为结束标记,注意CDATA为大写。
那我们把我们的读出来的数据放在 CDATA 中输出就能进行绕过,下面我们来分析一下如何做到。
首先,找到问题出现的地方,问题出现在
1 | ... |
在XML中,有时实体内包含了些字符,如&
,<
,>
,"
,'
等。这些均需要对其进行转义,否则会对XML解释器生成错误,所以我们把数据放在"<![CDATA["和 “]]>"
中,但是好像没有任何语法告诉我们字符串能拼接的,这里多个实体连续引用的方法是行不通的,所以我们就需要引入DTD文件,然后使用参数实体。
在引入外部DTD声明之后,想要嵌套其它参数实体就必须要用一个“中间参数实体”去搭桥,这个中间参数实体可以理解为eval。具体实现方法看下面的POC
payload:
这里dtd引用的内容可以是本地的,也可以是公网上的
1 | "1.0" encoding="utf-8" xml version= |
test.dtd
1 | "1.0" encoding="UTF-8" xml version= |
结果如下:
调用过程:这里从上到下执行,首先执行payload第7行的中的%dtd;
,然后把http://localhost:82/WWW/test.dtd
内容引入到了payload中,这里有点类似于将 test.dtd
包含进来。然后再执行第10行的&all;
,然后就是一次执行%start;%test;%end;
,最后把文件内容加载了出来
无回显读取本地文件
xml.php
1 | <?php |
test.dtd
1 | <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///Users/xianyu123/test.txt"> |
payload:
1 | <!DOCTYPE payload [ |
结果如下:
这里执行的原理与上述CDATA标签的原理类似,就不再过多赘述
HTTP 内网主机探测
HTTP 内网主机端口扫描
PHP expect 命令执行(RCE)
PHP 的 expect 扩展并不是默认安装的,如果安装了这个expect 扩展我们就能直接利用 XXE 进行 RCE
示例代码:
1 | <!DOCTYPE root[<!ENTITY cmd SYSTEM "expect://id">]> |
利用 XXE 进行 DOS 攻击
示例代码:
1 | "1.0" xml version= |
防御
方案一:使用语言中推荐的禁用外部实体的方法
PHP:
1 | libxml_disable_entity_loader(true); |
JAVA:
Python:
方案二:黑名单过滤(不推荐)
过滤关键词:
1 | DOCTYPE、ENTITY SYSTEM、PUBLIC |