FastJson反序列化学习-01

🌸 FastJson

FastJson是一个由阿里巴巴开发的高性能JSON处理库,支持Java对象与JSON字符串之间的互相转换。

本次漏洞研究基于FastJson1.2.24版本。也就是最早出现FastJson反序列化漏洞的版本。

CVE-2017-18349,FastJson<=1.2.24

🍂 Demo

先来熟悉一下什么是Json

复制代码
package org.y4y17;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Main {
    public static void main(String[] args) {
        String s = "{\"name\":\"Y4y17\",\"age\":17}";

        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
        System.out.println(jsonObject.get("name"));
        System.out.println(jsonObject.get("age"));
    }
}

这里简单的写了一个DemoString s = "{\"name\":\"Y4y17\",\"age\":17}";创建了一个字符串,然后利用JSON.parseObject方法来将字符串解析为对象。

  • JSON.parseObject,是将Json字符串转化为相应的对象;
  • JSON.toJSONString,是将对象转化为Json字符串。

然而在反序列化的时候,可以指定转化的对象类型!此时JSON.parseObject方法便会将其转化为对应的一个javaBean,比如我们这里存在一个JavaBean

复制代码
package org.y4y17;

public class Person {
    private String name;
    private int age;
    public Person() {
        System.out.println("调用了constructor方法");
    }
    public String getName() {
        System.out.println("调用了getName方法");
        return name;
    }

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

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

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

那么在转化为Java对象的时候可以通过指定要转化的类,来完成对应对象的转化。如下代码:

复制代码
package org.y4y17;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Main {
    public static void main(String[] args) {
        String s = "{\"name\":\"Y4y17\",\"age\":17}";

        Person person = JSON.parseObject(s, Person.class);
        System.out.println(person.getAge());
    }
}

在上面的Person类中的各个setget方法中,打印了相关的方法名,以便更加清晰的看到调用关系。如上代码的执行结果如下:

当我们指定了要转化的类的时候,发现整个转化的过程中,先调用了构造器,然后就是调用相关的set方法和get方法(在这里的get方法是在getAge()调用的时候触发的)。

然而在FastJson反序列化的时候,可以指定一个@type字段,用来表明指定反序列化的目标恶意对象类。比如我们在String字符串里面添加一个@type字段。

String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}"

复制代码
package org.y4y17;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Main {
    public static void main(String[] args) {
        String s = "{\"@type\":\"org.y4y17.Person\",\"name\":\"Y4y17\",\"age\":17}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
    }
}

然而在上面的JSON转化为Java对象的时候,通过写入@type字段,实现了指定类的反序列化,成功的调用了Person类中的setget方法以及构造器。

🍂 流程分析

下断点进行调试:

跟进到JSONparseObject方法中:在JSON类中可以看到存在很多种方法,其中他们的参数是不同的:

parseObject方法中:

首先parse主要负责解析我们传递的text,最后便会返回这个Person类对象,在这个过程中就会调用构造器和set方法,而最后的return,将对象强制转化为JSONObject对象,这个过程中会调用到Person类中的get方法!继续跟进到parse方法中:

其中这个parse方法也是一个静态的方法,可以直接在外部进行调用:

在上述的代码中,会创建一个默认的JSON解析器:

复制代码
DefaultJSONParser parser = new DefaultJSONParser(text, ParserConfig.getGlobalInstance(), features);
Object value = parser.parse();

然后利用创建的解析器进行解析,最终返回Person类对象。ParserConfig.getGlobalInstance()是创建了一个解析器,进行初始化,主要就是初始化一些配置,可以跟进一下看:

可以看到这个parserConfig类中的构造器:

放进去了一下默认的反序列化器;比如:derializers.put(Map.class, MapDeserializer.instance);如果是Map类型的话,就使用MapDeserializer反序列化器

同时整个过程中还设置了一些黑名单,也就是当时在这个漏洞出现之前,禁止指定的类:

如这里的java.lang.Thread,线程类。

接下来JSON扫描器,就会从头开始扫描我们传递进来的string

首先就会判断第一个字符是不是一个" { ",如果是的话,也就是代表着一个JSON格式的字符串,进入到if条件里面:

或者是不是一个"[",如果是的话,其实就是一个数组。由于我们传递的就是一个符合JSON格式的字符串,所以if直接就进入了,其他的都不会进去,此时就会创建好默认的JSON解析器了,下一步就是解析的过程:

跟进到这个parse方法中:

parse方法中通过switch来匹配第一个字符到底是什么,因为我们是一个左大括号,所以继续往下走:

这里便会创建一个JSONObject,他其实是一个Map

这里相当于是创建了一个新的Mapsize0

这里我们继续跟进到parseObject方法中!先是经过一系列的判断:

因为这里我们是"{",所以这里所有的if条件都不满足,直接过:

接下来经过了一个死循环!必须存在break 或者return才能结束。ifwhile主要是进行了重复的事情,寻找",",如果存在的话,就跳过。

然后继续判断是不是双引号和:、}等符号:整个过程中就是取出来了一个key!也就是@type

继续往下走的话,其实会去判断拿到这个key是不是和默认的key一样!默认的可以就是@type

之后就会通过loadClass进行类加载了!继续跟进:

先从缓存中查找是不是已经加载过了,或者有相关的记录(空间换时间),如果存在的话,就直接拿出来用了!

之后便会判断类名的首字符是不是"[",也就代表着数组,之后又判断是不是以"L"开头,同时以";"结尾。我们这里都不满足,所以就不管了,继续往下走:

这里就会获取到AppClassLoader,通过AppClassLoader进行加载!然后就会把他放到缓存中,最后return 这个类。此时整个loaderclass也就结束了。

接下来回到了默认的JSON解析器中:

这里if条件都不满足,继续往下走吧:

这里的object就是最开始创建的JSONObject,当时说他是一个Map,整个过程中还没有往里面存放东西,所以他的size就是0,这里的if条件还是不成立,继续往下走就会发现:ObjectDeserializer deserializer = config.getDeserializer(clazz);

config里面获取了一个反序列化器!将我们的类放到里面。而这个获取到的反序列化器就是前面我们看到ParserConfig构造器里面初始化的那些,当然我们这里继续跟进,看看他获取到的反序列化器是什么?

这里从构造器初始化的那些反序列化器中获取对应的,其实是没有的!继续往下走是泛型 然后调用了:getDeserializer

跟进之后,发现还是一直在找这个反序列化器,一直找不到,就各种判断是不是type为空等,是不是自己通过注解的方式写了一个反序列化器,显然我们是没有的!所以这里的if都是进不去的!

之后就开始判断是不是黑名单里面的~ 继续往下走:

然后又去判断了是不是Enum等,到最后一直找不到,就创建了一个JavaBeanDeserializer反序列化器!继续跟进到这个方法中:

这里存在一个变量asmEnable变量,初始化是true

继续走,调用了JavaBeaninfo类中的getBuilderClass方法,由于传递的jsonType是一个null值,最终方法return null。还是继续往下走,superclass复制本身,通过一个死循环,获取这个类的修饰符,判断是不是public。如果不是的话,那么asmEnable就会被设置为false

整个过程中又去判断了是不是参数为空,指定的类是不是一个接口!(反正又是各种不满足!)

最后调用了一个JavaBeaninfobuild方法,跟进看一下:

到这个build方法中,发现获取了Person类中的所有的Filed,以及Public属性修饰的方法,以及默认的构造器!然后判断这个默认的构造器是不是为空,如果不为空的话,就设置一下可访问!

接下来就是三个for循环:

首先第一个就是获取所有的set方法!第二个是获取所有的public属性的变量,第三个就是获取get方法!先看第一个方法:

这里先挨个遍历这些方法,他的方法名字长度是不是小于4,因为set就占了三个字符长度了~ 又判断了这个方法的修饰符是不是静态的,以及方法的返回值是什么,因为set方法一般就是没有返回值的!

当方法是set相关的方法的时候,经过上述的条件判断,一直往下走:

获取到set之后的第一个字母,判断是不是大写的!然后判断了一个静态变量是不是true,如果不是的话,那么就将这个大写字母转换为小写字母!然后将后面的其他字符进行拼接!

之后就是获取这个方法中的变量:

最后通过add方法,把整个获取到信息,全部放入到List里面!

在这个方法中还创建一个FieldInfo,这里我们跟进到这个方法中:

在这个方法中存在一个Feild,对整个逻辑存在相关的影响:

就是这个getOnly变量,正常来说他是一个false,往下走的时候,可以清楚的看到else{ }里面存在着这个变量的覆盖!getOnly=true

正常这个types就是参数的长度,我们这里就是1,如果不是1的话,那就会走到这个else代码里面,然后给getOnly进行赋值!(这里下面也就没有什么其他的东西了,继续往下走就回到了add方法,这里的getOnly有什么用后续再说!)

最终经过上面的for循环,整个List里面就存放了两个值,一个是age,一个是name

之后便是进入到获取类中所有的变量,因为当前的类中是没有Public修饰的变量的,所以就不用看了。

进入到第三个for循环,就是获取get方法,但是这里并不会add(像第一个for循环,寻找set方法时),

因为在这个for循环中,会去判断这个方法的返回值类型是不是Collection或者是不是Map等,如果是的话,才会进行后续的add,还有一个条件就是:

他在找get的时候,会看一下fieldList里面是不是存在这个字段,如果存储过了,那就不会add。所以这里总结一下在寻找get方法的时候,触发add方法的两个条件:

    • 返回值类型需要是Map、Collections等要求的那些
    • 同时fieldList里面没有这个字段(换句话说就是这个字段,只有get方法,而没有set方法!)

最后直接返回了一个JavaBeanInfo,其实在创建反序列化器的整个过程中就是在获取我们这个Person类中的所有的信息!

继续往下走:

下面依然会去通过一些if条件,这个asmEnable还是存在可以修改的情况!比如clazz不是一个接口,默认的构造器是null,同样会修改asmEnablefalse

接下来就是会通过一个for循环!可以遍历Field里面的getonly,如果有一个是true的话,就可以修改asmEnbalefalse了!但是这里并不满足!

最后就会判断这个asmEnable是不是false,如果是的话,就会创建一个JavaBeanDeserializer反序列化器!否则的话,会利用asmFactory去创建一个反序列化器!

所以这里创建的反序列化器并不是默认的那个反序列化器:

而是一个叫FastJsonASMDeserializer的反序列化器!他是一个临时创建的类,所以这里是没办法调试的!回顾整个创建的过程中,其实我们在之前有说到过一个field,就是getonly。当他满足的为true的时候,asmEnable也就变成了false。此时创建的反序列化器就是默认的,此时也就可以进行调试了。

也就是Fieldinfo类中的构造器中,他去获取了方法的参数,判断参数类型的长度是不是1,如果不是的话,就可以进入到else代码中,将getOnly设置为true

那么在往前去找我们从哪里进来这个构造器的:

是在JavaBeanInfo类中的两个for循环中出现的调用!第一个就是寻找set方法,第二个就是寻找get方法!然而在第一个for循环中的FieldInfo里面其实是无法将getOnly设置为true的。原因就是他的参数类型长度肯定是1,所以在第一个for循环中无法设置了!

只能在第二个for循环中进行设置!也就是寻找get方法的for循环。但是之前我们就谈到过这个for循环中的add方法是需要满足条件才能进入的!

他的返回值必须要是if条件里面的才行!所以我们这里需要创建一个返回值是如上类型的set方法!之前还说到一个条件就是 这个field只能有get方法,没有set方法,不然的话,在上面for循环中,将这个field加入到FeildList中就不会再调用get方法了!

所以这里我们定义一个map类型的field

复制代码
package org.y4y17;

import java.util.Map;

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

    public Map getMap() {
        System.out.println("调用了getMap方法");
        return map;
    }

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

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

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

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

再次调试,直接断点在第二个for循环那里,因为没有给map变量设置set方法,所以前面的寻找set方法的时候,往FieldList里面加入的还是agename两个field

此时是getMap!我们往下跟进:

这里的if条件满足,所以就会进入到这个if条件里面了!

而且在fieldList里面寻找也没找到,原因就是他根本就没有set方法,所以没找到!

到这里的话就继续跟进到Fieldinfo方法里面:

到这里的时候,getMap方法的参数是空的,所以这里就不满足了,成功的进入到了else代码里面,成功的将getOnly设置成了true

最后return。回到上层,继续看:

此时已经是返回了beaninfo,里面的field就是三个了,分别就是agenamemap;继续代码往下走就会经过for循环,去获取fieldgetonly变量,如果是true的话,就会将asmEnable设置为false!然而上面的mapgetonly就已经被我们设置成了true!所以能进入if条件:

这里可以看到mapgetOnly确实就是true。因此可以设置asmEnablefalse

然后往下走成功进入到if条件,创建了一个默认的JavaBeanDeserializer对象!

然后将成功的创建了一个反序列化器,这个是可以调试的。(上述的目的仅仅是为了调试~ )接下来就是利用反序列化器进行反序列化操作了,继续跟进到deserialze方法中:

发现调用了createInstance方法,其中parser就是默认的JSONparsertype便是指定的类,继续跟进:

最终通过构造器进行了newInstance,也就执行了构造器方法!

接着继续往下走便是setvalue

赋值操作无非就是通过反射或者是通过调用set方法!跟进到这个setValue方法中!

然后就是通过invoke进行调用赋值!这里其实有一个if条件,并没有满足,直接跳到了invoke这里执行了。

这里的getOnly变量的值是false。因为他是一个set方法。最后也就返回了整个调用过程和对象:

可以看到这里并没有执行get方法,最开始的时候,就已经提到了上面是调用set方法,而在toJSON的时候才会进行调用get方法!

继续跟进到toJSON里面:

上面一直都在判断这个clazz是个什么?都不满足,因为他是个Person类!

这里的getobjectWriter就是序列化的方法!

接下来就是创建了一个JSONObject对象(和之前的一样就是一个Map

这里就是获取到了三个键值对!然后往JSONObject里面存放!整个过程中也是通过调用invoke方法来实现的get方法执行:

Map<String, Object> values = javaBeanSerializer.getFieldValuesMap(javaObject);

javaBeanSerializer.getFieldValuesMap(javaObject)方法中调用了invoke方法:

继续跟进到这个getPropertyValue方法中:

该方法中又调用了get方法!继续跟进到这个get方法中:

get方法中,先是判断了method是不是为空,如果不为空的话,就通过invoke方法来调用get方法。

此时便成功的完成了整个的调用。整个过程中,我们传递的参数,并没有传递map。如果我们传递map的话,在利用反序列化器进行反序列化的时候,也是会调用getMap的(原因是,寻找set方法的时候,mapfield并没有set方法,仅仅有一个get方法!)

🌸 流程总结

整个过程分为三个阶段

    • JSON解析器解析阶段,此时还只是当作JSON字符串来解析
    • Java反序列化器解析阶段,此时是因为在JSON字符串中找到了@type字段,便开始当作是Java对象解析
    • toJSON阶段,此时会调用get方法

思考:整个流程分析完了,那么如何利用这个漏洞/缺陷来进行攻击呢?

    • 只要能找到一个类,该类中的set或者get方法中存在调用链,便可以利用
🍂 本地Demo

尝试创建一个类,实现弹计算器的操作:

复制代码
package org.y4y17;

import java.io.IOException;

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

创建一个Test类!然后我们传递这个类,通过@type,进行指定:

复制代码
package org.y4y17;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;

public class Main {
    public static void main(String[] args) {
        String s = "{\"@type\":\"org.y4y17.Test\",\"cmd\":\"open -a calculator\"}";
        JSONObject jsonObject = JSON.parseObject(s);
        System.out.println(jsonObject);
    }
}

运行后成功弹出计算器:

相关推荐
考虑考虑2 小时前
Jpa使用union all
java·spring boot·后端
用户3721574261353 小时前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊4 小时前
Java学习第22天 - 云原生与容器化
java
渣哥5 小时前
原来 Java 里线程安全集合有这么多种
java
间彧5 小时前
Spring Boot集成Spring Security完整指南
java
间彧6 小时前
Spring Secutiy基本原理及工作流程
java
Java水解7 小时前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆9 小时前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学9 小时前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole9 小时前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端