<menu id="guoca"></menu>
<nav id="guoca"></nav><xmp id="guoca">
  • <xmp id="guoca">
  • <nav id="guoca"><code id="guoca"></code></nav>
  • <nav id="guoca"><code id="guoca"></code></nav>

    java序列化與反序列化

    VSole2022-04-13 16:35:35

    最近抽空回顧了下java的序列化和反序列化,覺得之前了解的很淺顯,索性對底層代碼做了些分析。看了些師傅的文章,把筆記整理了下。個人覺得還可以。

    序列化與反序列化

    java序列化指的是將java對象轉化為字節序列的過程。

    java反序列化指字節序列恢復到java對象。

    基礎知識

    計算機內存最小單位為一個二進制位,即 0或1。

    我們把這個二進制位稱為一個bit(比特)位。

    一個字節(byte)有八個比特位,即 byte = 8*bit。

    如果八個bit位都為1,即這個字節最大為 FF = 1111 1111。

    一個字(word)是兩個byte,即 word = 2 byte = 16 bit,

    則一個字最大為 FFFF。

    doubleword 雙字,是兩個word ,即四個byte,32*bit,

    一個doubleword為FFFF FFFF。

    一般情況下使用最多的是字節,字節相當于人民幣的元一樣,雖然不是最低的,但卻是最常用的。

    一串字符在內存中一般是以ascii編碼形式存在,不同編碼占用子節長度不同

    一個ascii碼的占用一個字節。

    unicode碼占用一個字(兩個字節)。

    utf-8 是我們國內常用的是針對unicode碼的一種可變編碼方式。

    ascii

    字節序

    當一串數據太大的時候,一個字節放不下,就需要使用多個字節。

    比如0x12345678就需要四個字節。

    而現在就有了兩種存放方式,

    我們稱這兩種為 小端序和大端序。

    小端序從屁股開始,大端序從頭開始。

    小端序

    大端序

    各家架構不同,使用的大小端序不同,無需糾結。

    但是后來計算機網絡通信出來了,大家如果有不同的話會導致混亂。

    tcp/ip協議出來之后就規定網絡通信必須使用大端序。

    以上就是字節序的基本知識。

    序列化與反序列化

    序列化:

    對象序列化的最主要的用處就是在傳遞和保存對象的時候,保證對象的完整性和可傳遞性。序列化是把對象轉換成有序字節流,以便在網絡上傳輸或者保存在本地文件中。序列化后的字節流保存了Java對象的狀態以及相關的描述信息。序列化機制的核心作用就是對象狀態的保存與重建。

    反序列化:

    客戶端從文件中或網絡上獲得序列化后的對象字節流后,根據字節流中所保存的對象狀態及描述信息,通過反序列化重建對象。

    序列化就是把實體對象狀態按照一定的格式寫入到有序字節流,反序列化就是從有序字節流重建對象,恢復對象狀態。

    上面的簡單點說,進程間通信可以將圖片,視頻,音頻等信息用二進制方式傳輸。但是進程間的對象卻不能這么搞。

    比如我創建了一個User u1 = new User(1,”a”,100);

    我要將它傳給另一個軟件(進程),

    進程間的對象想要傳輸就需要序列化和反序列化。

    序列化為二進制數據,可以永久存在硬盤里,也可以進行網絡傳輸。

    實現java序列化和反序列化

    下面嫌太長了可以直接看例子。

    JDK類庫中序列化和反序列化API

    java.io.ObjectOutputStream:

    表示對象輸出流;

    它的writeObject(Object obj)方法可以對參數指定的obj對象進行序列化,把得到的字節序列寫到一個目標輸出流中;

    java.io.ObjectInputStream:

    表示對象輸入流;它的readObject()方法源輸入流中讀取字節序列,再把它們反序列化成為一個對象,并將其返回;

    實現序列化的要求

    只有實現了Serializable或Externalizable接口的類的對象才能被序列化,否則拋出異常!

    實現Java對象序列化與反序列化的方法

    若User類僅僅實現了Serializable接口,則可以按照以下方式進行序列化和反序列化:

    ObjectOutputStream采用默認的序列化方式,對User對象的非transient的實例變量進行序列化。

    ObjcetInputStream采用默認的反序列化方式,對對User對象的非transient的實例變量進行反序列化。

    若User類僅僅實現了Serializable接口,并且還定義了readObject(ObjectInputStream in)和writeObject(ObjectOutputSteam out),則采用以下方式進行序列化與反序列化:

    ObjectOutputStream調用User對象的writeObject(ObjectOutputStream out)的方法進行序列化。

    ObjectInputStream會調用User對象的readObject(ObjectInputStream in)的方法進行反序列化。

    若User類實現了Externalnalizable接口,且User類必須實現readExternal(ObjectInput in)和writeExternal(ObjectOutput out)方法,則按照以下方式進行序列化與反序列化:

    ObjectOutputStream調用User對象的writeExternal(ObjectOutput out))的方法進行序列化。

    ObjectInputStream會調用User對象的readExternal(ObjectInput in)的方法進行反序列化。

    實例

    user對象,使用的是上述第一種方式,所以User要實現Serializable。

    import java.io.Serializable; public class User implements Serializable {    int id;    String name;    String phone;      #一些get set 構造參數,這里就不列舉了}
    

    序列化與反序列化

    import java.io.*; public class userDemo {    public static void main(String[] args) throws IOException, ClassNotFoundException {        //創建對象        User u1 = new User(1,"AAAAAAA","110"); //被序列化的對象        User u2; //反序列化的對象         //序列化        getSerial(u1);         //反序列化        u2 = backSerial();        System.out.println(u2.getName());    }     //序列化    static void getSerial(User u1) throws IOException {        FileOutputStream fos = new FileOutputStream("obj.out");        ObjectOutputStream oos = new ObjectOutputStream(fos);        oos.writeObject(u1);        oos.flush();        oos.close();    }     //反序列化    static User backSerial() throws IOException, ClassNotFoundException {        FileInputStream fis = new FileInputStream("obj.out");        ObjectInputStream ois = new ObjectInputStream(fis);        User u1 = (User) ois.readObject();        return u1;    }}
    

    序列化底層分析

    ObjdectOutputStream對象的初始化

    bout是數據輸出流的底層

    writeStreamHeader將文件頭寫入文件

    這里根據序列化的文件分析

    所以這里是寫入文件頭,表示聲明使用序列化協議以及說明序列化版本

    初始化完畢,文件存在且寫入了文件頭。

    開始序列化寫入文件

    writeObject(u1);

    向下調用write0ject0();這個方法的內容比較長。重要點在意思是按照不同類型的方法去寫入序列化數據,可以看上面實現Java對象序列化與反序列化的方法。

    我們實例中實現了Serializable,所以執行writeOrdinaryObject方法。

    bout.writeByte(TC_OBJECT);

    寫入了0x73

    調用 writeClassDesc(desc, false);跟進:

    這里isProxy是判斷類是否是動態代理模式。

    具體可以自行了解,我也不清楚。因為我們實例的類不是動態代理,所以跟進writeNonProxyDesc();

    先寫入了描述符號0x72

    下面判斷跟進兩個參數一個為1,一個為2。跟進writeClassDescriptor(desc);

    和true執行同一個方法:

    在開發中,我們經常會遇到要經過for循環來判斷該循環體中是否包含或不包含某一元素,這個時候我們也常用一個boolean值來介入判斷。而“|=”可以輕松的讓我們完成實現。

    boolean flag = false; 在一個循環體中,flag |= (c==e);如果一直不相等,則flag一直為false,一旦有一個相等則為true;

    out.writeUTF(name);

    寫入類名

    out.writeLong(getSerialVersionUID());

    寫入序列化uid

     

    再往下一堆if判斷接口的實現方式,將標志位寫入

    out.writeByte(flags);

    我們使用serializable,所以應該寫入0x02

    所以從0x000B - 0x0013 都是序列化uid

    然后調用writeShort寫入兩個字節的域長度(比如說有3個變量,就寫入 00 03 )。

    實例中有三個參數

    接下來就是循環寫入變量名和變量類型。

    每輪循環:

    writeByte寫入一個字節的變量類型,writeUTF()寫入變量名,判斷是不是原始類型,即是不是對象。不是原始類型(基本類型)的話,就調用writeTypeString()。

    這個writeTypeString(),如果是字符串,就會調用writeString()。而這個writeString()往往是這樣寫的,字符串長度(不是大小)小于兩個字節,就先寫入一個字節的TC_STRING(16進制 74),然后調用writeUTF(),寫入一個signature,這好像跟jvm有關。

    最后一般寫的是類似下面這串74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b“翻譯”過來就是,字符串類型,占18個字節長度,變量名是 Ljava/lang/string;

    紅色 id參數 int 類型

    綠色 name 參數 string 因為 String是引用數據類型所以調用了writeTypeString() 寫入了Ljava/lang/string;

    黃色 phone 參數 string

    這里第一次看有個疑問,phone參數也是string,但是他卻沒Ljava/lang/string;這一串,后邊又增加一個string的參數,確定同一種引用數據類型只寫入一次。

    循環執行完,返回到writeNonProxyDesc方法,寫入結束標志位0x78

    bout.writeByte(TC_ENDBLOCKDATA);

    準備開始寫入序列化數據,回到writeOrdinaryObject()方法,writeSerialData(obj, desc);方法來寫入序列化數據

    這里根據使用方式來判斷,所以調用了 defaultWriteFields();

    第二個if是判斷是否為基本數據類型,是的話就會直接寫入序列化數據,不是的話向下到for循環附近。獲取變量數,然后循環調用writeObject0();寫入

    循環結束,直到所有運行完成,回到主函數。

    反序列化就不寫了,反反過來推一遍就成。

    java反射機制

    反射機制允許程序在運行期借助于Reflection API取得任何類的內部信息,并能直接操作任意類和對象的所有屬性及方法。

    要使用一個類,就要先把它加載到虛擬機中,在加載完類之后,堆內存的方法區中就產生了一個Class類型的對象(一個類只有一個class對象),這個對象就包含了完整的類的結構信息,我們可以通過這個對象看到類的結構,這個對象就像一面鏡子,透過鏡子可以看到類的結構,所以形象的稱之為:反射。

    實例:

    import java.lang.reflect.Method; public class test {    public static void main(String[] args) throws Exception {        a1Class a1 = new a1Class();        //通過運行時的對象調用getClass();        Class c = a1.getClass();        try {            //getMethod(方法名,參數類型)            //getMethod第一個參數是方法名,第二個參數是該方法的參數類型            //因為存在同方法名不同參數這種情況,所以只有同時指定方法名和參數類型才能唯一確定一個方法            Method m1 = c.getMethod("print", int.class, int.class);            //相當于r1.print(1, 2);方法的反射操作是用m1對象來進行方法調用 和r1.print調用的效果完全相同            //使用r1調用m1獲得的對象所聲明的公開方法即print,并將int類型的1,2作為參數傳入            Object i = m1.invoke(a1,1,1);        }catch (Exception e){            e.printStackTrace();        }    }    static class a1Class {        public void print(int a, int b) {            System.out.println(a + b);        }    }}
    

    嘗試簡化上面的代碼

    創建另一個文件

    public class testMiao {    public static void maio(){        System.out.println("miao!");    }}
    

    使用反射來執行miao();

    public class test {    public static void main(String[] args) throws Exception {        try {            Object s = Class.forName("testMiao").getMethod("maio").invoke(null);         }catch (Exception e){            e.printStackTrace();        }    }}
    

    嘗試添加參數簡化

    public class testMiao {    public static void maio(String s){        System.out.println("miao!"+s);    }}
    

    反射

    public class test {    public static void main(String[] args) throws Exception {        try {            Class.forName("testMiao").getMethod("maio", String.class).invoke(Class.forName("testMiao"),"aaa");        }catch (Exception e){            e.printStackTrace();        }    }}
    

    java執行命令

    java中可以使用Runtime.getRuntime.exec();來執行系統命令

    如:

    嘗試使用反射來執行

    Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke("open /System/Applications/Calculator.app");
    

    這樣會報錯,報錯的信息:是對象不是聲明類的實例

    說明exec只能是通過getRuntime來執行

    import java.lang.reflect.Method; public class test {    public static void main(String[] args) throws Exception {        Object o = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);        Class.forName("java.lang.Runtime").getMethod("exec", String.class).invoke(o,"open /System/Applications/Calculator.app");    }}
    

    這樣會成功,原理跟隨反射實例第一個實例來理解。

    現在可以打開計算器,明白什么是序列與反序列化了。

    關于cc1的鏈,之后再寫,可以看bilibili 白日夢組長分析思路,我個人覺得他的思路是真的超級棒。

    序列化string
    本作品采用《CC 協議》,轉載必須注明作者和本文鏈接
    Java安全中Groovy組件從反序列化到命令注入及繞過和在白盒中的排查方法
    STATEMENT聲明由于傳播、利用此文所提供的信息而造成的任何直接或者間接的后果及損失,均由使用者本人負責,雷神眾測及文章作者不為此承擔任何責任。雷神眾測擁有對此文章的修改和解釋權。如欲轉載或傳播此文章,必須保證此文章的完整性,包括版權聲明等全部內容。未經雷神眾測允許,不得任意修改或者增減此文章內容,不得以任何方式將其用于商業目的。
    序列化的核心思維旨在,將A變成B,最后再從B還原回A。 總之,在一些條件苛刻或者變化無常的環境與需求中,產生了這種靈活的可逆性的B的中間體。 理解不安全反序列化的最好方法是了解不同的編程語言如何實現序列化和反序列化。這里的序列化與反序列化指的是程序語言中自帶的實施與實現。而非自創或者自定義的序列化與反序列化機制(比如:N進制形式hashmap樹型等其他數據結構里的序列化中間體)。
    目前的Log4j2檢測都需要借助dnslog平臺,是否存在不借助dnslog的檢測方式呢
    最近兩個月我一直在做拒絕服務漏洞相關的時間,并收獲了Spring和Weblogic的兩個CVE但DoS漏洞終歸是雞肋洞,并沒有太大的意義,比如之前有人說我只會水垃圾洞而已,所以在以后可能打算做其他方向早上和pyn3rd師傅聊天
    最近寫了點反序列化的題,才疏學淺,希望對CTF新手有所幫助,有啥錯誤還請大師傅們批評指正。php反序列化簡單理解首先我們需要理解什么是序列化,什么是反序列化?本質上反序列化是沒有危害的。但是如果用戶對數據可控那就可以利用反序列化構造payload攻擊。
    前置知識分析Transformer接口及其實現類。transform()傳入對象,進行反射調用。構造調用鏈調用鏈構造原則:找調用關系要找不同名的方法,如果找到同名,再通過find usages得到的還是一樣的結果。找到InvokerTransformer類中的transform(),右鍵,點 Find Usages,找函數調用關系,最好找不同名的方法,調用了transform()。因為transform()調用transform()不能換到別的方法里,沒有意義。如果有一個類的readObject()調用了get(),那我們就可能找到了調用鏈。最終選擇TransformedMap這個類,因為TransformedMap類中有好幾處都調用了transform()。
    初識Java反序列化
    2022-06-10 08:49:49
    研究某產品反序列化EXP時,搜集到的POC只有一段16進制字節序列難以利用,遂有下文對Java序列化和反序列化的學習。 大致內容如下: 序列化和反序列化示例 序列化數據組成解構 反序列化漏洞形成原理
    淺談Java反序列化漏洞
    2022-05-17 17:48:01
    Java序列化與反序列化Java 提供了一種對象序列化的機制,該機制中,一個對象可以被表示為一個字節序列,該字節序列包括該對象的數據、有關對象的類型的信息和存儲在對象中數據的類型。反序列化就是打開字節流并重構對象。對象序列化不僅要將基本數據類型轉換成字節表示,有時還要恢復數據。
    正常來說一個合法的反序列化字符串,在二次序列化也即反序列化序列化之后所得到的結果是一致的。
    VSole
    網絡安全專家
      亚洲 欧美 自拍 唯美 另类