01
漏洞概述
该漏洞是Black Hat Europe 2019的议题《New Exploit Technique In Java Deserialization Attack》。
只要用户能控制客户端的JDBC连接串,在连接阶段即可触发该漏洞,无需继续执行SQL语句。
mysql-connector-java是Java进行JDBC执行的依赖包,支持在Java代码中对数据库进行连接与SQL语句执行等。
02
影响范围
mysql-connector-java-5.1.18 ~ 5.1.40
mysql-connector-java-6.x
mysql-connector-java-8.0.7以上
03
修复建议
建议在项目开发时,不允许MySQL连接串被用户控制。
如果存在功能需求(如DolphinScheduler),建议将mysql-connector-java-5版本升级到5.1.40以上,6、8版本升级到最新版本。
04
漏洞分析
该漏洞在mysql-connector-java-5、6、8的某些版本中都存在,只不过利用链不同,下面首先分析mysql-connect-java-5版本。
mysql-connect-java-5版本
注:本次分析利用的环境为mysql-connector-java-5.1.29 。
mysql-connect-java-5版本中利用的是detectCustomCollations的触发方式,触发点在com.mysql.jdbc.ConnectionImpl的buildCollationMapping方法中。
可以看到,只要MySQL版本大于4,且getDetectCustomCollations( )为true即可。之后会再次判断服务器的MySQL版本是否在5以上,接着调用Util.resultSetToMap( ) 。
然后会调用传入参数result的getObject( )方法,跟进getObject( )方法。
注:此处直接Ctrl+鼠标左键不能直接进入调用getObject( )方法的地方,只会跳到interface类的方法中,真正的getObject( )方法在该interface类的impl类中。
补充说明:传入的result对象是stmt.executeQuery("SHOW COLLATION")的返回值,stmt对象是通过getMetadataSafeStatement( )获取的,跟进该方法,可以看到声明stmt的方法为createStatement( ) 。在这个方法中可以看到,该链实际返回的stmt对象是StatementImpl类创建出的对象,因此原来的stmt.executeQuery("SHOW COLLATION")中执行的executeQuery( )方法,是在StatementImpl类中重写的executeQuery( )方法,即传入的result对象是executeQuery( )方法执行后的返回值。
而在executeQuery("SHOW COLLATION")方法中,返回的result对象是通过this.results = locallyScopedConn.execSQL( )赋值的,locallyScopedConn对象是this.connetion赋值的,所以需要找到this.connetion的赋值处。存在于StatementImpl的构造方法中,传入的是MySqlConnection的对象。根据刚刚创建stmt的流程,传入的是getLoadBalanceSafeProxy( )的返回结果。根据结果,最后定位在ConnectionImple类的execSQL( )方法,返回的是ResultSetInternalMethods类的对象,但ResultSetInternalMethods是一个接口类,所以最终的实例化对象必然是其实现类,而ResultSetImpl恰好是该类的实现类,所以继续向下跟踪,必然会找到返回该类的实例化对象或是其子类的实例化对象的语句。最后,执行完成后返回的对象是JDBC4ResultSet类的对象,且该类继承了ResultSetImpl类。
综上所述,在Util.resultSetToMap( )中调用的getObject( )方法如果在JDBC4ResultSet类中未被重写的话,会调用父类的getObject( )方法。经过审计代码,发现JDBC4ResultSet类只重写了如下的重载方法,所以此处应该调用父类的getObject( )方法。
public T getObject ( int columnIndex, Class type ) throws SQLException
在跟进该方法时,代码如下:
可以看出,此处获取了查询结果,且对于结果集的每一列进行了getSQLType判断。当此处的返回值为-2时,会判断该字段是否为二进制串,且是否为BLOB类型,接着会判断是否开启了反序列化,以及是否为Java反序列化开头的二进制数据,最终可以实现readObject( )方法进行反序列化。
也就是说,只要字段2或字段3搭载了序列化数据,在其处理过程中就会进行反序列化,从而执行恶意代码。
POC构造
想要复现该漏洞,需要准备一个带有恶意对象的数据库表,由于要在SHOW COLLATION执行处触发,所以带有恶意对象的表应该在系统表里(即INFORMATION_SCHEMA COLLATIONS表),当mysql_jdbc_connect对恶意数据库进行连接请求时会触发反序列化。
但是修改系统表可能会引入其他未知问题。针对这种情况,可以采用其他方法。
这里要提到mysql_jdbc_connect的工作原理。mysql_jdbc_connect是通过与数据库端口进行私有协议(即MySQL协议)来交互的,那么可以伪造一个MySQL服务端,让mysql_jdbc_connect连接虚假服务端,将恶意序列化对象插入返回数据中,由mysql_jdbc_connect进行反序列化执行即可。
想要构造这个数据包,需要了解MySQL私有协议的交互流程。这里抓取正常的本地loop报文,简单分析交互流程。
握手报文如下:
连接请求如下:
认证响应如下:
流程大致如上图,后续每执行一次SQL相关操作,就会通过该协议与数据库进行一次交互。
上文提到,触发点在连接时数据库执行SHOW COLLATION的地方,所以重点关注SHOW COLLATION的请求与响应。
SHOW COLLATION请求如下:
SHOW COLLATION响应如下:
使用python构造一个恶意的MySQL服务端,用于监听客户端的连接(握手报文构造与上面一致即可),SHOW COLLATION响应代码如下:
注:1、由于connector 5版本的getObject( )方法需要用到第三列数据,所以构造响应时需要至少构造三列响应数据;
2、需要FLAGS大于128、来源表不为空,否则会被当成Text 。
复现结果
启动模拟MySQL监听后,使用Java作为客户端发出连接请求。
执行getConnection( )方法时计算器弹出。
mysql-connect-java-8版本
注:本次分析利用的环境为mysql-connector-java-8.0.14 。
8版本connector的触发点与5版本的触发点不同。
前文提到,5版本的触发点是detectCustomCollations为true,且触发位置在com.mysql.jdbc.ConnectionImpl的buildCollationMapping方法中。
而8版本则使用ServerStatusDiffInterceptor的触发方式,这也是原议题中给出的环境与触发方式。触发点位于com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor类的populateMapWithSessionStatusValues方法中。
进入该方法,会调用到ResultSetUtil类的resultSetToMap方法。
再进入该方法,会调用ResultSet的派生类重写的getObject( )方法,此处的ResultSet对象是createStatement( )创建的对象执行executeQuery("SHOW SESSION STATUS")后返回的结果,通过跟踪,最终定位到真正调用getObject( )方法的是com.mysql.cj.jdbc.result.ResultSetImpl类的getObject( )方法。
之后和5版本的判断基本相同,判断是否为二进制,判断是否为BLOB类型的数据,最后再判断自动反序列化是否开启。
从ServerStatusDiffInterceptor到反序列化这条链已经弄通,接下来分析如何在JDBC连接的时候,调用ServerStatusDiffInterceptor类的populateMapWithSessionStatusValues方法。
首先看到ServerStatusDiffInterceptor类,它是一个拦截器,并且实现了QueryInterceptor接口。
补充说明:Connector/J拦截器类,仅在8.0.7版本以上使用。
拦截器是一种软件设计模式,提供了一种透明的方式来扩展或修改程序的某些方面,类似于用户出口,无需重新编译。使用Connector/J,通过更新连接字符串以引用实例化的不同组的拦截器类,可以启用和禁用拦截器。
可以在queryInterceptors中指定实现com.mysql.cj.interceptors.QueryInterceptor接口的类的标准名称。在这种拦截器类中,可能会更改或增强某些类型的语句所完成的处理,例如自动检查内存缓存服务器中的查询数据,重写慢速查询,记录有关语句的执行信息或请求路由到远程服务器。
在JDBC的连接串中可以控制拦截器的连接属性,如:
jdbc:mysql://your-ip:3306/xxx?queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
当拦截器在JDBC连接串中使用时,其作用是在执行SQL语句查询前后,通过一些特定操作来影响查询结果。所以要触发queryInterceptors则需要能执行SQL查询语句,而在getConnection过程中,会触发SET NAMES utf、set autocommit=1一类的请求,因此会触发配置的queryInterceptors 。
从代码层面也可以看出这套逻辑:
1、在初始化过程中,会调用ConnectImpl类的setAutoCommit( )方法,该方法会执行SET autocommit语句。
2、执行execSQL( )方法会调用到sendQueryString( ),之后会调用sendQueryPacket( )方法。
3、在sendQueryPacket( )方法中会判断是否设置了queryInterceptors,如果设置了拦截器,则通过反射调用其preProcess( )方法。
4、最后从ServerStatusDiffInterceptor的preProcess( )方法中调用populateMapWithSessionStatusValues( )方法,进入利用链。
POC构造
与5版本类似,只有两个地方不同:
1、JDBC连接串的URL
在5版本中需要设置detectCustomCollations为true;而在8版本中,需要设置queryInterceptors,连接串如下:
jdbc:mysql://127.0.0.1:3306/vaethink?queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true
2、恶意响应的构造
在5版本中,根据查询语句“SHOW COLLATION”的Query包进行恶意响应构造;而在8版本中,需要对“SHOW SESSION STATUS”的Query包进行恶意响应构造。
模拟的MySQL服务端只需要增加对show session status包的响应即可。下附网上的POC代码,其中get_payload_content方法中获取的是可弹出计算器的序列化对象。
复现结果
启动模拟MySQL监听后,使用Java作为客户端发出连接请求。
计算器弹出,使用的是该POC的默认序列化对象,即弹出计算器的序列化对象。
05
参考链接
https://xz.aliyun.com/t/8159
https://blog.csdn.net/fnmsd/article/details/106232092
安博通 ,可视化网络安全技术创新者 威胁情报 【漏洞预警】Apache Shiro绕过权限漏洞(CVE-2020-17510) 【漏洞预警】ZenTaoPMS 文件上传漏洞(CNVD-C-2020-121325) 【漏洞预警】Apache Kylin未授权配置泄露漏洞(CVE-2020-13937)![e32badff71df09b6d7e3ae12ececc5de.gif](https://i-blog.csdnimg.cn/blog_migrate/9eb7155d6dc2b749fd37ccbac754f2b1.gif)
点击,了解安博通