Fastjson基础

介绍

Fastjson 是阿里巴巴开源的一个 Java 语言编写的高性能 JSON 库

主要就是序列化:将 Java Bean转换成 JSON 字符串(JSON.toJSONString(obj))。

​ 反序列化:将 JSON 字符串转换回 Java Bean(JSON.parseObject(jsonStr, User.class))。

前端(App/网页)给后端发了一个 JSON,后端 Java 代码里有一个 User 类。如果要把这个 JSON 里的数据取出来, Fastjson 这样的工具可以实现自动化

先导入依赖

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

1.2.24是最先报出漏洞的版本

分析

引出漏洞

  • 最先开始演示一段代码

    将 JSON 字符串转化为 JSONObject 对象。(转成通用的java对象)

    1
    2
    3
    4
    5
    6
    7
    public class JSONUnser {
        public static void main(String[] args) throws Exception {
            String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";//将字符串键值对变成json格式
    
            JSONObject jsonObject = JSON.parseObject(s);
            System.out.println(jsonObject);
            System.out.println(jsonObject.getString("param1"));

    通过parseObject将字符串键值对变成json格式

    输出:image-20260309210002119

  • 演示将写个将 JSON 字符串转化为 JavaBean 对象,获取这个对象的方法(转成特定的Person对象)

    先写个java Bean的规范的Person类

    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
    package org.example;
    
    public class Person {
        private int age;
        private String name;
    
    
        public Person() {
            System.out.println("constructor");
        }
    
        public int getAge() {
            System.out.println("getAge");
            return age;
        }
    
        public void setAge(int age) {
            System.out.println("setAge");
            this.age = age;
        }
    
        public String getName() {
            System.out.println("getName");
            return name;
        }
    
        public void setName(String name) {
            System.out.println("setName");
            this.name = name;
        }
    }

    image-20260309211249040

    也就是说传入age 和 name,通过set方法传入对象

  • 还有一个功能也就是我们的漏洞产生点

    image-20260309213114470

    代码里明明写的是 JSON.parseObject(s),但 Fastjson 却听从了 JSON 字符串里的指令,去加载了 Person。因为写了 @type

    当 Fastjson 看到 @type 时,它会:

    去加载 这个类:org.example.Person。

    去实例化 这个类(创建对象)。—————>输出constructor

    **还原属性 ** 解析到后面有 age 和 name 的值,找对应的 Setter————–>输出setAge、setName

    序列化输出 执行了 System.out.println(jsonObject),Fastjson 在把 jsonObject 转换成字符串打印出来时,会通过 Getter 获取值。————->输出:getAge、getName。

    要是我们传入的是一个恶意方法,就会造成问题

调试

调试一下它是如何返回这些东西的

image-20260309222927982

  • 先进来是一个只传String text的parseObject

    image-20260311212819836

    主要逻辑就是先将这个已经解析(parse)好的文本传成obj对象(其实就是Person)

    要是这个对象继承JSONObject,就按照JSONObject类型返回对象

    这个JSONObject就是个map键值对

    image-20260311213654770

  • 进parse方法

    1
    2
    3
    public static Object parse(String text) {
        return parse(text, DEFAULT_PARSER_FEATURE);
    }

    再进

    image-20260311213956524

    到这个地方就是将这些东西传到DefaultJSONParser对象中

    他的作用主要就是一个解析器,对你传入的text进行解析

    后面的features就是对于解析的规范(比如:对出现的多个逗号的处理 啥的)

  • 接下来就是调用parse(),进行解析

    image-20260311220302546

  • 到parse()的核心逻辑

    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
    82
    83
    84
    public Object parse(Object fieldName) {
        final JSONLexer lexer = this.lexer;
        switch (lexer.token()) {
            case SET:
                lexer.nextToken();
                HashSet<Object> set = new HashSet<Object>();
                parseArray(set, fieldName);
                return set;
            case TREE_SET:
                lexer.nextToken();
                TreeSet<Object> treeSet = new TreeSet<Object>();
                parseArray(treeSet, fieldName);
                return treeSet;
            case LBRACKET:
                JSONArray array = new JSONArray();
                parseArray(array, fieldName);
                if (lexer.isEnabled(Feature.UseObjectArray)) {
                    return array.toArray();
                }
                return array;
            case LBRACE:
                JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
                return parseObject(object, fieldName);
            case LITERAL_INT:
                Number intValue = lexer.integerValue();
                lexer.nextToken();
                return intValue;
            case LITERAL_FLOAT:
                Object value = lexer.decimalValue(lexer.isEnabled(Feature.UseBigDecimal));
                lexer.nextToken();
                return value;
            case LITERAL_STRING:
                String stringLiteral = lexer.stringVal();
                lexer.nextToken(JSONToken.COMMA);
      
                if (lexer.isEnabled(Feature.AllowISO8601DateFormat)) {
                    JSONScanner iso8601Lexer = new JSONScanner(stringLiteral);
                    try {
                        if (iso8601Lexer.scanISO8601DateIfMatch()) {
                            return iso8601Lexer.getCalendar().getTime();
                        }
                    } finally {
                        iso8601Lexer.close();
                    }
                }
      
                return stringLiteral;
            case NULL:
                lexer.nextToken();
                return null;
            case UNDEFINED:
                lexer.nextToken();
                return null;
            case TRUE:
                lexer.nextToken();
                return Boolean.TRUE;
            case FALSE:
                lexer.nextToken();
                return Boolean.FALSE;
            case NEW:
                lexer.nextToken(JSONToken.IDENTIFIER);
      
                if (lexer.token() != JSONToken.IDENTIFIER) {
                    throw new JSONException("syntax error");
                }
                lexer.nextToken(JSONToken.LPAREN);
      
                accept(JSONToken.LPAREN);
                long time = ((Number) lexer.integerValue()).longValue();
                accept(JSONToken.LITERAL_INT);
      
                accept(JSONToken.RPAREN);
      
                return new Date(time);
            case EOF:
                if (lexer.isBlankInput()) {
                    return null;
                }
                throw new JSONException("unterminated json string, " + lexer.info());
            case ERROR:
            default:
                throw new JSONException("syntax error, " + lexer.info());
        }
    }

    image-20260311220329728

    就是进行匹配解析,下面的case就对于xxxx,进行的解析逻辑(有什么左大括号,右大括号这些)

    1
    String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"ttt\"}";

    我们传的s先是有{,所以就跳到了左大括号的解析这里

    image-20260311220720110

    以对象解析,存到空的JSONObject(它是一个map)里面

  • 接下来就是parseObject()接收map对你的object解析,我们来看一下parseObject()(它是在fastjson中最重要的一个函数)

    image-20260311221238046

    这个函数的逻辑主要就是将map解析成对象,先解析key,在解析value

    image-20260312162551185

    这里就出现了我们前面提到的features,此处是对逗号进行处理的

  • 解析key,对里面的值进行匹配解析

    image-20260312162828760

  • 对key值的形式进行匹配

    image-20260312163014636

    要是@type,就进第一个if逻辑、

  • 我们看一下第一个if的逻辑

    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
    if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
                        String typeName = lexer.scanSymbol(symbolTable, '"');
                        Class<?> clazz = TypeUtils.loadClass(typeName, config.getDefaultClassLoader());
    
                        if (clazz == null) {
                            object.put(JSON.DEFAULT_TYPE_KEY, typeName);
                            continue;
                        }
    
                        lexer.nextToken(JSONToken.COMMA);
                        if (lexer.token() == JSONToken.RBRACE) {
                            lexer.nextToken(JSONToken.COMMA);
                            try {
                                Object instance = null;
                                ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
                                if (deserializer instanceof JavaBeanDeserializer) {
                                    instance = ((JavaBeanDeserializer) deserializer).createInstance(this, clazz);
                                }
    
                                if (instance == null) {
                                    if (clazz == Cloneable.class) {
                                        instance = new HashMap();
                                    } else if ("java.util.Collections$EmptyMap".equals(typeName)) {
                                        instance = Collections.emptyMap();
                                    } else {
                                        instance = clazz.newInstance();
                                    }
                                }
    
                                return instance;
                            } catch (Exception e) {
                                throw new JSONException("create instance error", e);
                            }
                        }
                        
                        this.setResolveStatus(TypeNameRedirect);
    
                        if (this.context != null && !(fieldName instanceof Integer)) {
                            this.popContext();
                        }
                        
                        if (object.size() > 0) {
                            Object newObj = TypeUtils.cast(object, clazz, this.config);
                            this.parseObject(newObj);
                            return newObj;
                        }
    
                        ObjectDeserializer deserializer = config.getDeserializer(clazz);
                        return deserializer.deserialze(this, clazz, fieldName);
                    }
    • 先进行类加载

      image-20260312163212910

      类加载逻辑:加载过,就到mapping缓存里面直接加载

      后面的if是对于类加载的具体形式

      字节码第一位是{,则为数组形式加载

      字节码第一位是L以 ; 结尾的,之则自动把前后的 L 和 ; 删掉,提取出中间真正的类名,然后递归调用 loadClass 去加载

      image-20260312163414508

    • 接下来就是放东西

      image-20260312163921331

      每次加载完一轮,然后就往里面放东西,现在由于我们object是0(这个我们前面创建的那个空JSONobject),所以直接就跳过了这个if

      疑问:

      size 一直是 0,这个 if 块不久永远进不去了???

      当你使用 {"@type": "..."} 时,Fastjson 的解析逻辑是 流式解析。

      • 常规路径:如果 JSON 只是普通的 {"name":"abc"},Fastjson 会把键值对一个个放进 object 这个容器里,此时 size 会大于 0,然后通过 TypeUtils.cast 转成类。
      • AutoType 路径:当解析器读到关键的 @type 时,它会立刻跳出当前的常规解析流程,直接去调用 checkAutoType 加载类并创建对象。
      • 结果:数据直接被塞进了新创建的 Person 实例里,而没有经过这个临时的 object 容器。所以对这个容器来说,它确实从头到尾都是空的(size = 0)。
  • 接下来,也是最重要的一部,反序列化

    image-20260312164737051

    先getDeserializer获取反序列化器,然后利用反序化器的deserialze来反序列化,得到Person

    • 获取到PEerson后,进到getDeserializer

      image-20260312165029210

    • 在getDeserializer逻辑里面,这里是获取注解(自己写的活,相当于自己配了反序列化器)

      image-20260312165259185

    • 接着里面有一个黑名单

      image-20260312165538713

      里面禁止了java.lang.Thread,不允许解析线程(既然存在黑名单,接下来的修复可能就会在这个地方有限制)

    • 一直匹配匹配,最后到了

      image-20260312174525992还是匹配,都不是的话就创建一个反序列化器

      image-20260312174641648

      按照javabean解析

    • 到内部createJavaBeanDeserializer

      asmEnable存在于java底层是动态创建类,动态加载的一个技术

      asmEnable默认是true

      image-20260312174901124

      这是他不支持的一些判断

    • 接下来到了**JavaBeanInfo.build**

      image-20260312175012357

      创建类对应的反序列化器的时候,要了解这个对象里面的所有东西(getname,setname….),然后组成JavaBeanInfo

      说一下这个函数的逻辑:

      先是遍历这个对象的字段,对应到我们这里就是age name

      接下来获取默认构造器,对应我们这里的无参构造

      然后就把构造函数setAccessible,允许访问

      之后就到了一个for循环

      先整体看一下

      image-20260312175940896

      先遍历methods,主要是为了得到所有的set字段

      再遍历field

      又遍历methods,主要是为了得到所有的get‘字段

      • 第一个methodsimage-20260312175707951遍历所有methods来获取字段(这个地方先获取的是name)
        下面的if就是在满足set的条件(返回void ,字段长度…..)

        image-20260312180246615
        上面都满足之后,就要在这个地方将其转为小写
        此处propertyName值就变成了name

        这里的判断是为了应对一些特殊写法(_ f)
        image-20260312204549235

        获取字段
        image-20260312204713695
        还要找is的
        看一下getfield

        1
        2
        3
        4
        5
        6
        public static Field getField(Class<?> clazz, String fieldName, Field[] declaredFields) {
             for (Field field : declaredFields) {
                 if (fieldName.equals(field.getName())) {
                     return field;
                 }
             }

        就是在所有已经有的字段去匹配

        找到之后就要创建FieldInfo
        image-20260312205121323

        FieldInfo:
        image-20260312205339031
        有个逻辑就是说,先创建一个getOnly字段,走else(如果长度不是一),就把getOnly改成true

        接下来走出FieldInfo,add就是把字段放到filedlist里面

      • Field(字段/成员变量)

        主要作用:
        1处理“没有 Setter”的公有字段(public类里面没有set方法。Fastjson 遍历 field 时,发现这个字段是 public 的,它就会记录下来。这样反序列化时,它能直接通过反射把值塞进这个变量里,而不必非要找 set 方法。)
        2识别注解(遍历 field 才能读到这个注解。)
        3为“私有字段直接注入”做准备(遍历 field 拿到所有字段的名单。如果在第一步遍历 methods 时没找到对应的 set 方法,它会回过头来看看能不能直接通过修改 field.setAccessible(true) 来暴力赋值。)

      • 第二个methods

        遍历完setter之后,获取所有的public

        然后遍历所有getter

        说一下接下来的逻辑

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25
        26
        27
        28
        29
        if (Collection.class.isAssignableFrom(method.getReturnType()) //
                            || Map.class.isAssignableFrom(method.getReturnType()) //
                            || AtomicBoolean.class == method.getReturnType() //
                            || AtomicInteger.class == method.getReturnType() //
                            || AtomicLong.class == method.getReturnType() //
                        ) {//规定返回形式
                            String propertyName;
        
                            JSONField annotation = method.getAnnotation(JSONField.class);
                            if (annotation != null && annotation.deserialize()) {
                                continue;
                            }
                            
                            if (annotation != null && annotation.name().length() > 0) {
                                propertyName = annotation.name();
                            } else {
                                propertyName = Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
                            }
                            
                            FieldInfo fieldInfo = getField(fieldList, propertyName);//从fieldList里面获取propertyName,查看是否符合只有get,没有set的情况(上面说了set会放到fieldList里面)
                            if (fieldInfo != null) {
                                continue;
                            }
        
                            if (propertyNamingStrategy != null) {
                                propertyName = propertyNamingStrategy.translate(propertyName);
                            }
                            
                            add(fieldList, new FieldInfo(propertyName, method, null, clazz, type, 0, 0, 0, annotation, null, null));

        如果返回的是Map、AtomicBoolean、AtomicInteger、AtomicLong这几种形式,而且是只有get,没有set的情况的话,就会放在新的fieldList里面

        不满足的话就到了

        image-20260316221708865

        进JavaBeanInfo看看

        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
        public JavaBeanInfo(Class<?> clazz, //
                            Class<?> builderClass, //
                            Constructor<?> defaultConstructor, //
                            Constructor<?> creatorConstructor, //
                            Method factoryMethod, //
                            Method buildMethod, //
                            JSONType jsonType, //
                            List<FieldInfo> fieldList){
            this.clazz = clazz;
            this.builderClass = builderClass;
            this.defaultConstructor = defaultConstructor;
            this.creatorConstructor = creatorConstructor;
            this.factoryMethod = factoryMethod;
            this.parserFeatures = TypeUtils.getParserFeatures(clazz);
            this.buildMethod = buildMethod;
        
            this.jsonType = jsonType;
            if (jsonType != null) {
                String typeName = jsonType.typeName();
                if (typeName.length() != 0) {
                    this.typeName = typeName;
                } else {
                    this.typeName = clazz.getName();
                }
            } else {
                this.typeName = clazz.getName();
            }
        
            fields = new FieldInfo[fieldList.size()];
            fieldList.toArray(fields);
        
            FieldInfo[] sortedFields = new FieldInfo[fields.length];
            System.arraycopy(fields, 0, sortedFields, 0, fields.length);
            Arrays.sort(sortedFields);
        
            if (Arrays.equals(fields, sortedFields)) {
                sortedFields = fields;
            }
            this.sortedFields = sortedFields;
        
            defaultConstructorParameterSize = defaultConstructor != null ? defaultConstructor.getParameterTypes().length : 0;
        }

        就是把我们刚刚得到的东西传到JavaBeanInfo

        image-20260320212528989

        通过上面的一系列操作,找到了两个字段name 以及age。字段里面还有对应的setter方法,没有getter(前面我们提到过,有setter就不会有getter,因为反序列化的时候主要是传值赋值,主要用的就是setter)

        再往下就是对于修改asmEnable值的if
        对于asmEnable这个开关的作用:
        如果开关是关着的,就会调用原生的JavaBeanDeserializerimage-20260320213218388
        如果是开着的,就会调用asmFactory临时创建一个createJavaBeanDeserializer
        image-20260320213354878

        所以现在就有一个问题,因为是临时创建的,所以我们调试不到,流程在走但是我们跳到了JavaBeanDeserializer(应为这是他的父类,为了能看到代码,咱们就直接看这个类)

        image-20260320213829738

      扫描 Methods:首选 setXXX,因为这是最正规的。

      扫描 Fields:查漏补缺。看看有没有公有变量,或者有没有被注解标记的特殊字段。

      二次扫描 Methods:寻找符合特定条件的 getXXX(比如返回类型是 Map 或 Collection 的,这种 getter 也能用来还原数据)。

    • 现在我们要想能够调试的到,就不能让asmEnable开关开着,想办法给他关了,这样就会走默认的javabeaninfo

      前面我们提到了很多让开关关闭的限制

      image-20260320214318906

      找到了这个方法,就是字段是getonly的

      image-20260320214705513

      只有走这个地方才能让getonly打开,让asmEnable关了

      image-20260320215336745

      要想实现,就得不进if,进else,得满足条件,要求方法的参数长度不等于一

      前面我们提到了,在setter的时候长度不为1则退出,所以在setter的时候添加不到fieldinfo

      image-20260320215633613

      所以我们只能从getter那边入手,要想走getter就得满足上面说的Map、AtomicBoolean、AtomicInteger、AtomicLong这几种形式返回值

    • 所以我们现在就要在Person类里面加点东西,以符合条件

      image-20260320220616017

      只有get方法,而且返回值是map

      现在的相对于上面的变化呢,就是在第二个method这里

      咱们先把前两个setname和setage过了,直接看getmap

      image-20260320222455472

      符合条件,直接就add到了fieldinfo里面

      进fieldinfo:

      image-20260320223421329

      方法的参数长度,参数值是0,所以直接走else,让getonly变成true

      接下里就走到了

      image-20260320223743824

      将开关asmEnable关闭

      达到了我们的目的,走内置的javabeaninfo来断点分析

      image-20260320223855250

      这个地方要build一下,所以还会走前面的逻辑,咱们就直接跳过

      image-20260321140929997

      接着往下走就是将值放进去,derializer里面就有三个值(加了一个map)

      image-20260321141604276

      先获取这个反序列化器,然后调用反序列化方法

      image-20260321141755708

      进deserialze

    • 具体看deserialze里面到底是如何反序列化调用 构造函数 setter getter

      遍历所有字段

      image-20260321142642878

      如果有反序列化器(前面我们调用的默认的反序列化构造器)

      接下来又很多if,走的是json字符串,我们跟着代码逻辑看看

      创建实例:

      image-20260321144225847

      内部逻辑就是:要是接口就创建动态代理,否则就调构造函数

      image-20260321144507297

      调用完构造函数之后,赋值

      image-20260321144643641

      赋值的话,要么反射赋值,要么就是使用set方法赋值

      这个地方用的是setter,直接就走到了invoke

      image-20260321144807242

      image-20260321144836798

      invoke method对象的setAge方法

      接下来的流程就是name和map的

      name和age的流程一样样

      看map

      调用getter:

      image-20260321150417431

      obj就变成了Person对象,后面parseObject调用toJSON的时候就调用了getter

      逻辑就是前面先是将json变成对象(调setter),后面tojson将对象变成json(调getter)

      image-20260321150938673

      走getObjectWriter,因为前面走的反序列化,这边就是序列化

      image-20260321200720565

      在这添加到field,下面在获取fieldlist

      image-20260321200850800

      接下来寻找method

      image-20260321201241409

      如何找到method的:

      **ParserConfig.parserAllFieldToCache**: 这个方法的名字里带有 Field。它进去之后第一件事就是调用 clazz.getDeclaredFields()。

      利用反射实现

      TypeUtils.computeGetters,内部会调用 clazz.getMethods()

      image-20260321221825273

      调用这个方法getFieldValuesMap

      image-20260321201331722

      内部要获取值getPropertyValue

      image-20260321201457915

      getPropertyValue里面的get方法

      image-20260321201717328

      image-20260321204437278

      到了invoke

总结:

  1. 先是按照字符串解析,对于有@type的类型,就按照javabean解析,而且需要一个反序列化器,获取到之后调用反序列化方法

  2. image-20260321221959724

    • 使用反序列化解析调用构造方法,constructor
    • 赋值使用setter
    • parseObject(s)会走到tojson,将getter方法调用

    image-20260321222808154

  3. image-20260321222244310

    这个map满足我们上面提到的,四种类型返回,而且只有getter,那么也会调用

    image-20260321222554305

    image-20260321222721493

    多的这个map就是上面满足条件的map

漏洞原理

Fastjson 盲目地根据这个 @type 去创建对象,我们就可以指定一个系统自带的、具有破坏性的类。

演示:

测试类:

1
2
3
4
5
6
7
8
9
package org.example;

import java.io.IOException;

public class Test {
    public void setCmd(String cmd) throws IOException {
        Runtime.getRuntime().exec(cmd);
    }
}

触发:

1
2
3
4
5
6
7
8
public class JSONUnser {
    public static void main(String[] args) throws Exception {
//        String s = "{\"param1\":\"aaa\",\"param2\":\"bbb\"}";
//         String s = "{\"age\":18,\"name\":\"abc\"}";
//         String s = "{\"@type\":\"org.example.Person\",\"age\":18,\"name\":\"ttt\",\"map\":{}}";
        String s = "{\"@type\":\"org.example.Test\",\"cmd\":\"calc\"}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);

成功image-20260321223306680


Fastjson基础
http://example.com/2026/03/26/Fastjson基础/
作者
Piggy Sprint
发布于
2026年3月26日
许可协议