Java XXE漏洞实验及总结

0x01 前言

之前分析了下Jenkins Nested View插件XXE漏洞(CVE-2021-21680),就想着总结下Java的XXE。

Google了下发现有前辈已经总结好了,所以就照着做下实验。

0x02 XXE漏洞基础

XXE漏洞原理:

XXE全称XML External Entity,XML外部实体注入。通过在XML中声明DTD外部实体,并在XML中引入,便可以获取我们想要的数据。

XML文档结构:

XML主要由7个部分组成:文档声明、标签/元素、属性、注释、实体字符、CDATA 字符数据区、处理指令。

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
<!--XML文档声明;另外也是一个处理指令,<? xxx ?>就是处理指令的格式-->
<?xml version="1.0" encoding="ISO-8859-1"?>
<!--bookstore根元素、book子元素-->
<bookstore>
<!--category、lang都是属性-->
<book category="COOKING">
<title lang="en">Everyday Italian</title>
<!--&lt;实体字符 是一个预定义的实体引用,这里也可以引用dtd中定义的实体,以 & 开头, 以;结尾-->
<author>Giada De Laurentiis&lt;</author>
<year>2005</year>
<price>30.00</price>
<!--script这里是CDATA,不能被xml解析器解析,可以被JavaScript解析-->
<script>
<![CDATA[
function matchwo(a,b)
{
if (a < b && a < 0) then
{return 1;}
else
{return 0;}
}
]]>
</script>
</book>
</bookstore>

CDATA 指的是不应由 XML 解析器进行解析的文本数据(Unparsed Character Data)。CDATA 部分中的所有内容都会被解析器忽略。

CDATA 部分由 “**” 结束,某些文本比如 JavaScript 代码,包含大量 “<” 或 “&” 字符。为了避免错误,可以将脚本代码定义为 CDATA。

XML示例:

(1)如下实例,<?xml version="1.0" encoding="ISO-8859-1"?>是XML声明,<!DOCTYPE description [<!ENTITY abc "123213123123123" >]>就是DTD文档类型定义,<description>&abc;</description>是文档元素 其中abc就是引用的dtd中的abc。

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE description [
<!ENTITY abc "123213123123123" >
]>
<description>&abc;</description>

DTD文档类型定义:

DTD(文档类型定义)的作用是定义 XML 文档的合法构建模块。DTD 可以在 XML 文档内声明,也可以从外部引用。DTD实体也分为外部引用和内部定义。

DTD两种引入方式:

在XML内部声明DTD:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0"?>
<!DOCTYPE note [
<!ELEMENT note (to,from,heading,body)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT message (#PCDATA)>
]>
<note>
<to>George</to>
<from>John</from>
<message>Reminder</message>
</note>

在XML中引入外部DTD文档

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE note SYSTEM "note.dtd">
<note>
<to>George</to>
<from>John</from>
<message>Reminder</message>
</note>

<!--而note.dtd的内容为:-->
<!ELEMENT note (to,from,message)>
<!ELEMENT to (#PCDATA)>
<!ELEMENT from (#PCDATA)>
<!ELEMENT message (#PCDATA)>
DTD实体两种方式:

实体又分为一般实体和参数实体:一般实体的声明语法&实体名;;参数实体只能在DTD中使用,参数实体的声明格式:%实体名;

内部实体:

在dtd中定义了一个名为test的实体,实体的值为test-value:

1
<!ENTITY test "test-value">

外部实体:

在dtd中定义了一个名为test的实体,实体的值为URL:

1
2
3
<!ENTITY test SYSTEM "URI/URL">
<!ENTITY test SYSTEM "file:///C://1.dtd">
<!ENTITY test SYSTEM "http://test.com/1.dtd">

常见的xml解析方式有以下四种:

1)DOM生成和解析XML文档

2)SAX生成和解析XML文档

3)DOM4J生成和解析XML文档

4)JDOM生成和解析DOM文档

SAX是以流的形式读取XML文档中的内容,并在读取过程中自动调用预先定义的处理方法;DOM是将整个xml文档中的内容以tree的形式存储在内存当中,可以对这个tree中的任意一个节点进行操作,SAX则不能。SAX不适合用来修改xml内容,DOM需要耗费大量内存

0x03 实验

在Java中有很多jar包可以来解析XML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
javax.xml.parsers.DocumentBuilder
javax.xml.parsers.SAXParser
javax.xml.parsers.SAXParserFactory
javax.xml.transform.TransformerFactory
javax.xml.validation.Validator
javax.xml.validation.SchemaFactory
javax.xml.transform.sax.SAXTransformerFactory
javax.xml.transform.sax.SAXSource
org.xml.sax.XMLReader
org.xml.sax.helpers.XMLReaderFactory
org.dom4j.io.SAXReader
org.jdom.input.SAXBuilder
org.jdom2.input.SAXBuilder
javax.xml.bind.Unmarshaller
javax.xml.xpath.XpathExpression
javax.xml.stream.XMLStreamReader
org.apache.commons.digester3.Digester

笔者本次通过web servlet实现引用以上部分jar包解析xml来进行实验,并补充其中的修复方案。本次实验借鉴了前辈总结的代码

实验1 SAXBuilder解析xml并修复漏洞

实验编写的servlet如下,其中postNoFixXxe是未修复时的方法,postWithFixXxe是修复后的方法,修复时通过 builder.setFeature设置外部实体不能方法从而防御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
59
60
61
62
63
64
65
66
67
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;

import org.jdom2.JDOMException;
import org.jdom2.input.SAXBuilder;
import org.jdom2.Document;
import org.jdom2.output.XMLOutputter;

public class SAXBuilderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// postNoFixXxe(req, resp);
postWithFixXxe(req, resp);

}

private void postNoFixXxe(HttpServletRequest req, HttpServletResponse resp) {
try {
//获取请求输入流
BufferedReader reader = req.getReader();
//将xml读取到doc
SAXBuilder builder = new SAXBuilder(); //使用默认解析器
Document doc = builder.build(reader);
// 获取响应输出流
PrintWriter writer = resp.getWriter();
//将doc写到输出流
XMLOutputter outputter = new XMLOutputter();
outputter.output(doc, writer);

} catch (JDOMException | IOException e) {
e.printStackTrace();
}
}

private void postWithFixXxe(HttpServletRequest req, HttpServletResponse resp) {
try {
//获取请求输入流
BufferedReader reader = req.getReader();
//将xml读取到doc,通过setFeature设置dtd、外部实体读取 防止xxe漏洞
SAXBuilder builder = new SAXBuilder();
builder.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
builder.setFeature("http://xml.org/sax/features/external-general-entities", false);
builder.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
builder.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = builder.build(reader);
// 获取响应输出流
PrintWriter writer = resp.getWriter();
//将doc写到输出流
XMLOutputter outputter = new XMLOutputter();
outputter.output(doc, writer);

} catch (JDOMException | IOException e) {
e.printStackTrace();
}
}

}

当在doPost中调用postNoFixXxe时,构造xml,成功访问文件内容,可以导致xxe漏洞。

image-20210905191051910

当在doPost中调用postWithFixXxe时,成功防御了xxe漏洞,响应未输出xml,并且log报错不允许dtd。image-20210905190828341

实验2 DocumentBuilder(原生dom解析xml)

为了方便输出用了TransformerFactory将document输出到响应流。

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
59
60
61
62
63
import org.w3c.dom.Document;
import org.xml.sax.InputSource;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;

public class DocumentBuilderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
postNoFixXxe(req, resp);
// postWithFixXxe(req, resp);
}

private void postNoFixXxe(HttpServletRequest req, HttpServletResponse resp) {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
BufferedReader br = new BufferedReader(req.getReader());
Document document = documentBuilder.parse(new InputSource(br));
// 获取响应输出流
PrintWriter writer = resp.getWriter();
String textContent = document.getDocumentElement().getTextContent();
writer.print(textContent);
} catch (Exception e) {
e.printStackTrace();
}

}

private void postWithFixXxe(HttpServletRequest req, HttpServletResponse resp) {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
documentBuilderFactory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
documentBuilderFactory.setAttribute(XMLConstants.FEATURE_SECURE_PROCESSING, true);
documentBuilderFactory.setExpandEntityReferences(false);
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
BufferedReader br = new BufferedReader(req.getReader());
Document document = documentBuilder.parse(new InputSource(br));

// 获取响应输出流
PrintWriter writer = resp.getWriter();
String textContent = document.getDocumentElement().getTextContent();
writer.print(textContent);
} catch (Exception e) {
e.printStackTrace();
}

}
}

调用postNoFixXxe

image-20210909133913303

调用postWithFixXxe

image-20210909133805647

实验3 SAXReader

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
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


import org.dom4j.Document;
import org.dom4j.io.SAXReader;
import org.dom4j.io.XMLWriter;


public class SAXReaderServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
postNoFixXxe(req,resp);
// postWithFixXxe(req,resp);
}

private void postNoFixXxe(HttpServletRequest req, HttpServletResponse resp){
try{
SAXReader saxReader = new SAXReader();
Document doc = saxReader.read(req.getReader());
XMLWriter xmlWriter = new XMLWriter(resp.getWriter());
xmlWriter.write(doc);
}catch (Exception e){
e.printStackTrace();
}

}

private void postWithFixXxe(HttpServletRequest req, HttpServletResponse resp){
try{
SAXReader saxReader = new SAXReader();
saxReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
saxReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxReader.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
Document doc = saxReader.read(req.getReader());
XMLWriter xmlWriter = new XMLWriter(resp.getWriter());
xmlWriter.write(doc);
}catch (Exception e){
e.printStackTrace();
}

}
}

调用postNoFixXxe

image-20210905204358001

调用postWithFixXxe

image-20210905204721397

实验4 SAXTransformerFactory

SAXTransformerFactory解析xml,需要xslt,否则会报错,所以构造时要构造xslt。正常情况需要将xml输出,本次实验为了节省时间没有输出。

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
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.transform.Result;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;

public class SAXTransformerFactoryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
postNoFixXxe(req, resp);
// postWithFixXxe(req, resp);
}

private void postNoFixXxe(HttpServletRequest req, HttpServletResponse resp){
try{
SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
StreamSource source = new StreamSource(req.getReader());
TransformerHandler transformerHandler = sf.newTransformerHandler(source);
// 创建Result对象,并通过transformerHandler将目的流与其关联
Result result = new StreamResult(resp.getWriter());
transformerHandler.setResult(result);
}catch (Exception e){
e.printStackTrace();
}

}

private void postWithFixXxe(HttpServletRequest req, HttpServletResponse resp){
try{
SAXTransformerFactory sf = (SAXTransformerFactory) SAXTransformerFactory.newInstance();
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
sf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
StreamSource source = new StreamSource(req.getReader());
TransformerHandler transformerHandler = sf.newTransformerHandler(source);
// 创建Result对象,并通过transformerHandler将目的流与其关联
Result result = new StreamResult(resp.getWriter());
transformerHandler.setResult(result);
}catch (Exception e){
e.printStackTrace();
}

}
}

构造xml和xslt如下:

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
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE xxe [
<!ENTITY file SYSTEM "file:///E://111.txt" >
]>
<?xml-stylesheet type="text/xsl" href="E://222.xsl"?>
<catalog>
<test>&file;</test>
</catalog>

<!--xslt文件内容如下-->
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

<xsl:template match="/">
<html>
<body>
<h2>teat</h2>
<table border="1">
<tr bgcolor="#9acd32">
<th align="left">File</th>
</tr>
<xsl:for-each select="catalog">
<tr>
<td><xsl:value-of select="test"/></td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>

</xsl:stylesheet>

构造xml请求,在未禁止外部实体情况下可以正常响应,控制台未报错:

image-20210909152036586

修复后报错:

image-20210909152247156

实验5 TransformerFactory

这个就是Jenkins Nested View插件XXE漏洞(CVE-2021-21680)用到的xml解析工具。

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
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import java.io.IOException;

public class TransformerFactoryServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doGet(req, resp);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
postNoFixXxe(req, resp);
// postWithFixXxe(req, resp);
}

private void postNoFixXxe(HttpServletRequest req, HttpServletResponse resp){

try {
TransformerFactory tf = TransformerFactory.newInstance();
StreamSource source = new StreamSource(req.getReader());
tf.newTransformer().transform(source, new StreamResult(resp.getWriter()));
} catch (TransformerException | IOException e) {
e.printStackTrace();
}

}

private void postWithFixXxe(HttpServletRequest req, HttpServletResponse resp){

try {
TransformerFactory tf = TransformerFactory.newInstance();
StreamSource source = new StreamSource(req.getReader());
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
tf.setAttribute(XMLConstants.ACCESS_EXTERNAL_STYLESHEET, "");
tf.newTransformer().transform(source, new StreamResult(resp.getWriter()));
} catch (TransformerException | IOException e) {
e.printStackTrace();
}

}
}

postNoFixXxe:

image-20210905211112297

postWithFixXxe:

image-20210905212020646

Unmarshaller

使用默认的解析方法不会存在XXE问题,这也是唯一一个使用默认的解析方法不会存在XXE的一个库。

1
2
3
4
5
Class tClass = Some.class;
JAXBContext context = JAXBContext.newInstance(tClass);
Unmarshaller um = context.createUnmarshaller();
Object o = um.unmarshal(ResourceUtils.getPoc1());
tClass.cast(o);

小结:

基本上所有的修复都是设置禁止外部实体,但是需要注意通过setFeature或者setAttribute或者setProperty设置属性时,一定要注意在解析xml之前就设置,否则修复也是无效的。

0x04 危害

xxe漏洞危害:

1、读取系统文件,信息泄露

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE xxe [
<!ENTITY file SYSTEM "file:///E://111.txt" >
]>
<xxe>&file;</xxe>

2、Blind XXE 可以实施OoB数据外带攻击:https://www.cnblogs.com/3ichae1/p/13140229.html

3、探测内网端口-SSRF

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE xxe [
<!ENTITY url SYSTEM "http://192.168.116.1:90/" >
]>
<xxe>&url;</xxe>

4、执行系统命令

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE xxe [
<!ENTITY url SYSTEM "expect://id" >
]>
<xxe>&url;</xxe>

5、作为中间隧道,通过请求间接攻击内网网站

1
2
3
4
5
<?xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE xxe [
<!ENTITY url SYSTEM "带payload的url可getshell" >
]>
<xxe>&url;</xxe>

6、拓展,不同语言支持的协议:

libxml2 PHP Java .NET
file
http
ftp
file
http
ftp
php
compress.zlib
compress.bzlip2
data
glob
phar
file
http
https
ftp
jar
netdoc
mailto
gopher
file
http
https
ftp

0x05 总结

Java XXE漏洞的修复或预防主要在设置禁止dtd,一般出现漏洞需要注意:

  1. 是否禁止dtd或者entity
  2. 参数是否可控
  3. 传入参数格式为REST XML格式,X-RequestEntity-ContentType: application/xml

0x06 参考链接:

https://blog.spoock.com/2018/10/23/java-xxe/
https://www.leadroyal.cn/p/562/
https://www.cnblogs.com/r00tuser/p/7255939.html
https://www.runoob.com/dtd/dtd-entities.html
https://www.cnblogs.com/avivahe/p/5701392.html
https://stackoverflow.com/questions/27985355/outputting-xml-on-java-servlet-using-printwriter
https://security.tencent.com/index.php/blog/msg/69