F4de's blog F4de's blog
首页
WP整理
技术文章
学习笔记
其它随笔
关于|友链

F4de

Syclover | Web
首页
WP整理
技术文章
学习笔记
其它随笔
关于|友链
  • php安全

  • python安全

  • Java安全

    • Java反射特性摘要
    • Java反序列化-URLDNS
    • Java反序列化-CC1
    • Java反序列化-CC5
    • Java反序列化-CC6
    • Fastjson(1)-初探以及利用方式
      • 前言
      • Fastjson初次使用
        • 环境配置
        • 序列化
        • 反序列化
      • 漏洞Demo
      • 最后
    • Fastjson(2)-TemplatesImpl利用链
    • 谈一谈Java动态加载字节码
  • 其他

  • 技术文章
  • Java安全
F4de
2020-11-30

Fastjson反序列化(1)-初探利用方式

# 前言

Fastjson是Alibaba所维护了一套开源的JSON解析库,它可以把Java对象转换成JSON文本进行广解析。

Fastjson 源码地址

Fastjson 中文 Wiki

近几年来Fastjson爆出了许多反序列化漏洞,本着学习的目的,我打算写一个系列就Fastjson历史上爆出的一些漏洞以及绕过patch的方式进行分析,本意是想总结自己学习以及调试的过程,当然,如果读者能有一定的收获,那是我莫大的荣幸了。

# Fastjson初次使用

# 环境配置

环境配置

  • Fastjson 1.2.24
  • JDK 8u101

maven依赖:

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

# 序列化

我下面用一个Demo来展示Fastjson进行序列化和反序列化的基本过程。

首先定义一个学生类,一个标准的JavaBean:

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

    public Student() {
        System.out.println("调用无参构造函数");
    }

    public Student(int age, String name) {
        System.out.println("调用有参构造函数");
        this.age = age;
        this.name = name;
    }

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

    public void setAge(int age) {
        System.out.println("调用age的setter方法");
        this.age = age;
    }

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

    private void setName(String name) {
        System.out.println("调用name的setter方法");
        this.name = name;
    }
}
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

Fastjson中使用的序列化方法是JSON#toJSONString,尝试用Fastjson将学生类的对象进行序列化:

public class FJTest {
    public static void main(String[] args) {
        Student student = new Student(20, "f4de");
        String jsonString = JSON.toJSONString(student);
        System.out.println(jsonString);
    }
}
1
2
3
4
5
6
7

image-20201130213847987

可以看到,在进行序列化的时候调用了两个private属性的getter方法。

但是如果一个private属性没有对应的getter方法,则无法进行序列化(我把age的getter方法注释掉了):

image-20201130214330820

那么如果一个修饰的成员变量性是public、protected、static呢?我们可以再进行下面的测试:

public class TT {
    public String name = "F4de";
    protected int age = 20;
    static String address = "xxx";
}
1
2
3
4
5
public class FJTest {
    public static void main(String[] args) {
        TT tt = new TT();
        System.out.println(JSON.toJSONString(tt));
    }
}
1
2
3
4
5
6

image-20201130214722436

在没有对应的getter方法的时候,只有被public修饰的成员变量序列化成功了。如果给每一个成员变量添加对应的getter方法(当然实际环境中没有人这么写,这里只是为了演示):

image-20201130214944325

经过测试我们就可以得到如下的结论:

  • 序列化的时候会优先调用成员变量的getter方法(不论成员变量的修饰符是什么)
  • public成员变量如果没有getter方法仍是可以被序列化成功的
  • private、protected修饰符所修饰的成员变量如果没有getter方法则无法被序列化成功
  • static的成员变量无法被序列化(static修饰的成员变量是属于类的)

# @type

在序列化的时候可以指定SerializerFeature.WriteClassName,它是JSON#toJSONString的一个属性值,如果指定了这个属性,则在序列化的时候会JSON字符串前面加上一个"@type":"xxx.xxx.xxx"的键值对,表示正在进行序列化的对象所属的类。我们还用一开始的例子进行演示:

public class FJTest {
    public static void main(String[] args) {
        Student student = new Student();

        System.out.println(JSON.toJSONString(student, SerializerFeature.WriteClassName));
    }
}
1
2
3
4
5
6
7

image-20201130215800263

它的主要用处就是给JSON文本添加自省的功能,也就是说,在反序列化的时候,反序列化引擎会根据@type的值来得到当前进行反序列化的JSON文本的对象类型,而Fastjson的反序列化漏洞就是由它引起的。

# 反序列化

# JSON#parse

JSON#parse是Fastjson反序列化的一个方法,我们对{"@type":"com.demo1.Student","age":20,"name":"F4de"}进行一次反序列化:

public class FJTest {
    public static void main(String[] args) {
//        Student student = new Student(20, "F4de");
//        String jsonString = JSON.toJSONString(student, SerializerFeature.WriteClassName);
//        System.out.println(jsonString);
        /*
        {"@type":"com.demo1.Student","age":20,"name":"F4de"}
         */
        String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
        Object student = JSON.parse(jsonString);
        System.out.println(student.getClass().getName());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20201130221614202

所以目前我们得到了这样的一个结论:

  • 反序列化的过程中调用了类的无参构造函数

  • 反序列化的过程中调用了对应成员变量的setter方法来对各个成员变量进行赋值

但是如果把setter方法改为private的话,仍然是无法进行序列化的:

    private void setName(String name) {
        System.out.println("调用了name的setter方法");
        this.name = name;
    }
1
2
3
4

image-20201130221905700

如果在序列化的时候没有设置SerializerFeature.WriteClassName的话,我们可以看一下用parse方法反序列化后得到的结果:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
        Object student = JSON.parse(jsonString);
        System.out.println(student.getClass().getName());
    }
}
1
2
3
4
5
6
7

image-20201130222159592

我们看不到成员变量setter方法的调用,反序列化后得到的结果也不是Student类,所以FastJson序列化后得到的文本只有指定了@type之后才支持自省。

# JSON#parseObject

Fastjson中除了使用JSON#parse方法来反序列化,还有另外一个方法JSON#parseObject,与前者不同的是,后者可以在反序列化的时候指定类名:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
        Student student = JSON.parseObject(jsonString, Student.class);
        System.out.println(student.getClass().getName());
    }
}
1
2
3
4
5
6
7

image-20201130222732097

对于这个例子中的JSON字符串,我们没有指定@type,但是在反序列化的时候往方法中传入了第二个参数Student.class,这样就可以让Fastjson在进行反序列化的时候拥有自省的效果。同样,我们也可以试一下不指定Class的效果:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"age\":20,\"name\":\"F4de\"}";
        JSONObject student = JSON.parseObject(jsonString);
        System.out.println(student.getClass().getName());
    }
}
1
2
3
4
5
6
7

image-20201130223020621

如果在JSON文本中指定了@type,在使用JSON.parseObject方法的时候第二个参数是可以指定为Object.class,仍然是可以得到函数的调用和正确的类名的:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
        Object student = JSON.parseObject(jsonString, Object.class);
        System.out.println(student.getClass().getSimpleName());
    }
}
1
2
3
4
5
6
7

image-20201130223606246

如果JSON文本存在@type,但是在JSON#parseObject进行反序列化的时候没有指定类型:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\"}";
        Object student = JSON.parseObject(jsonString);
        System.out.println(student);
        System.out.println(student.getClass().getSimpleName());
    }
}
1
2
3
4
5
6
7
8

image-20201130224045793

我们可以看到还额外调用了对应成员变量的getter方法,并且得到的结果是一个JSON字符串的形式,它是一个JSONObject的对象。

为什么会得到这样的结果呢?我们可以跟进parseObject的源码看一下:

image-20201130224229120

可以看到parseObject就是调用了一次pasrse方法,如果得到的对象的类不是JSONObject的话,就对它进行一次toJSON的处理,再跟进查看(关键代码如下):

image-20201130224504018

因为要把结果处理成JSON文本的形式,所以需要额外调用一次getter方法。

那么结果就很明显了,我们的JSON文本指定了@type,在经过parse方法处理之后会得到一个Student类的对象,所以还会对它用toJSON处理之后再用JSONObject进行包装,得到了我们最后看到的结果。

所以我们之前得到的结论可以补充如下(方法修饰符为public):

  • 当JSON.parseObject没有指定类型的时候,会调用无参构造函数,对应成员变量的setter和getter方法。
  • 当JSON.parseObject指定了类型的时候,会调用无参构造函数,对应成员变量的setter方法。

# properties修饰符

其实我们上面得到的结论并不是准确的,因为在Jackjson中对于properties修饰符修饰的成员变量,在反序列化的过程中,如果这个成员变量没有setter方法,则会把getter方法当作setter方法来使用,也就是说通过getter方法来对成员变量进行赋值。我们这里先说明这个结论,具体原理会在之后的调试中具体说明。我们可以看下面这里例子:

public class Student {
    private int age;
    private String name;
    private Properties properties;

    public Student() {
        System.out.println("调用无参构造函数");
    }

    public Student(int age, String name, Properties properties) {
        System.out.println("调用有参构造函数");
        this.age = age;
        this.name = name;
        this.properties = properties;
    }

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

    public void setAge(int age) {
        System.out.println("调用age的setter方法");
        this.age = age;
    }

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

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

    public Properties getProperties() {
        System.out.println("调用properties的getter方法");
        return properties;
    }

//    public void setProperties(Properties properties) {
//        System.out.println("调用properties的setter方法");
//        this.properties = properties;
//    }
}
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

这里的properties只有getter方法,我们进行下面的反序列化:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.demo1.Student\",\"age\":20,\"name\":\"F4de\",\"properties\":{}}";
        Object student = JSON.parseObject(jsonString, Object.class);
        System.out.println(student);
        System.out.println(student.getClass().getSimpleName());
    }
}
1
2
3
4
5
6
7
8

image-20201201000714204

可以看到在反序列化的过程中调用了properties的getter方法,那什么时候Fastjson在反序列化的时候会调用getter方法呢?我这里先说结论(同样,具体原因会在调试的过程中对源码进行分析):

满足条件的setter:

  • 函数名长度大于4且以set开头
  • 非静态函数
  • 返回类型为void或当前类
  • 参数个数为1个

满足条件的getter:

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

properties属性所属的类是Properties,而这个类又继承自Hashtable:

image-20201201093417451

Hashtable又实现了Map接口,所以properties属性的成员的getter方法可以被触发:

image-20201201093525286

# Feature.SupportNonPublicField

我们前面测试过了,如果一个成员变量(public修饰的成员变量除外)没有对应的setter方法或者其setter方法(或者满足条件的getter方法)的修饰符不是public的话,那么在反序列化的时候是无法成功的:

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

    public TT() {

    }

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

    private void setAge(int age) {
        System.out.println("age的private的setter方法被调用");
        this.age = age;
    }

    @Override
    public String toString() {
        return "TT{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
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
public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.demo1.TT\",\"name\":\"F4de\",\"age\":20}";
        Object parse = JSON.parse(jsonString);
        System.out.println(parse.toString());
    }
}
1
2
3
4
5
6
7

image-20201201002134740

可以看到,两个成员变量均没有得到JSON文本中的值。

下面我们开始介绍 Feature.SupportNonPublicField,它是JSON.parseObject和JSON.parse的一个属性。我们设置了它之后,再去反序列化上面的JSON文本:

public class FJTest {
    public static void main(String[] args) {
        String jsonString = "{\"@type\":\"com.demo1.TT\",\"name\":\"F4de\",\"age\":20}";
        Object parse = JSON.parse(jsonString, Feature.SupportNonPublicField);
        System.out.println(parse.toString());
    }
}
1
2
3
4
5
6
7

image-20201201004543982

可以看到得到了我们想要的结果,而且设置了它之后是不需要调用setter或者getter方法的。 所以对于没有setter方法的private成员、或者其setter方法被private修饰时,反序列化的时候如果传入Feature.SupportNonPublicField这个属性则会反序列化成功。

# 总结

试了这么多例子,我们可以把上面的结果进行一次总结:

  • 使用JSON.parse进行反序列化的时候,如果JSON文本中指定了@type之后,则会调用对应类中的无参构造函数、非私有属性的setter方法、满足条件的getter方法,反序列化的结果获得到对应类的对象。如果没有指定@type,则不会调用任何方法,会得到一个JSONObject对象。
  • 使用JSON.parseObject(String json)进行反序列化的时候,如果没有使用第二个参数指定Class,并且JSON文本指定了@type的时候,它会再调用一次parse方法,同时将得到的结果进行包装,所以会调用对应类中的无参构造函数、非私有属性的getter方法、非私有属性的setter方法,如果是properties类型的成员,如果没有setter方法的话,则它的getter方法会调用两次。最后得到的结果是一个JSONObject对象。
  • 使用JSON.parseObject(String json, Class<T> class)这样指定了Class来进行反序列化的,则和第1点的结论一样,会调用对应类的无参构造函数、非私有属性的setter方法、满足条件的getter方法。最后得到的结果是对应类的一个对象。
  • 反序列化的时候会优先调用setter方法,如果setter方法不存在才会去寻找对应的并且满足条件的getter方法。
  • 对于没有setter方法的private成员、或者其setter方法被private修饰时,反序列化的时候如果传入Feature.SupportNonPublicField这个属性则会反序列化成功。

为了方便观看,笔者这里做了一个简单的表格(均未传入Feature.SupportNonPublicField):

方法 JSON文本是否指定@type 最终调用的方法 反序列化的结果
parse(String json) 否 不调用方法 JSONObject对象
parse(String json) 是 构造方法 + setter + 满足条件的getter 对应类的对象
parseObject(String json) 否 不调用方法 JSONObject对象
parseObject(String json) 是 构造方法 + settet + getter + 满足条件的getter JSONObject对象
parseObject(String json,Class class) 否 构造方法 + settet + 满足条件的getter 对应类的对象
parseObject(String json,Class class) 是 构造方法 + settet + 满足条件的getter 对应类的对象

# 漏洞Demo

Fastjson之所以存在反序列化漏洞,主要原因有两点:

  • JSON文本中的@type可以指定反序列化的类,我们可以把它指定成我们所构造的恶意类
  • 反序列化的过程中会自动调用恶意类中的构造方法、setter或者getter方法、或者静态代码块static{}

我们下面就写一个简单的POC:

首先编写一个恶意类,其中它的无参构造方法会触发calc:

public class EvilClass {
    public EvilClass() throws Exception {
        Runtime.getRuntime().exec("calc");
    }
}
1
2
3
4
5
public class Main {
    public static void main(String[] args) throws Exception {
        EvilClass evilClass = new EvilClass();

        JSON.parse(JSON.toJSONString(evilClass, SerializerFeature.WriteClassName));
    }
}
1
2
3
4
5
6
7

运行,发现弹出了两次计算器。

image-20201201092050097

当然,因为我们写的代码中第一次调用了它的无参构造函数,所以会额外的弹出一次计算器。第二次弹出计算器原因就是因为在反序列化的过程中触发了EvilClass的无参构造方法。

我们再测试一下触发setter或者getter的情况:

public class EvilClass {
    private Properties properties;

    public EvilClass() {
    }

    public Properties getProperties() throws Exception {
        Runtime.getRuntime().exec("calc");
        return properties;
    }

    @Override
    public String toString() {
        return "EvilClass{" +
                "properties=" + properties +
                '}';
    }

    //    public EvilClass() throws Exception {
//        System.out.println("调用无参构造方法");
//        Runtime.getRuntime().exec("calc");
//    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {
    public static void main(String[] args) throws Exception {
        String jsonString = "{'@type':'com.pagetest.EvilClass','properties':{}}";
        jsonString = jsonString.replace("'", "\"");
        System.out.println(jsonString);
        Object parse = JSON.parse(jsonString);
        System.out.println(parse.toString());
    }
}
1
2
3
4
5
6
7
8
9

image-20201201093158923

# 最后

这一篇文章主要简单介绍了一下Fastjson序列化和反序列化的基本原理以及一个简单的POC编写,总体难度不是很大,算是一个开头,在后面的文章中笔者会就历史上的反序列化漏洞以及patch的绕过进行调试和源码分析,希望读者和我都能有一些收获。

Java反序列化-CC6
Fastjson(2)-TemplatesImpl利用链

← Java反序列化-CC6 Fastjson(2)-TemplatesImpl利用链→

最近更新
01
Java8-Stream
01-03
02
谈一谈Java动态加载字节码的方式
12-18
03
Fastjson反序列化(2)-TemplatesImpl利用链
12-01
更多文章>
Theme by Vdoing | Copyright © 2019-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式