當前位置:首頁 > IT技術 > 編程語言 > 正文

Java反射&(反)序列化入門
2022-04-29 14:06:01

寫在前面

參考資料

https://www.bilibili.com/video/BV16h411z7o9?spm_id_from=333.1007.top_right_bar_window_custom_collection.content.click

https://blog.csdn.net/mocas_wang/article/details/107621010

https://juejin.cn/post/6844904025607897096#heading-15

https://zhuanlan.zhihu.com/p/72644638

https://segmentfault.com/a/1190000023876273

https://juejin.cn/post/6844903838927814669

IDEA快捷鍵使用

跟進類、方法:Ctrl+B

彈出structure框框:Alt+7

Java原生(反)序列化

基本使用

讓需要被(反)序列化的類實現(xiàn)一下Serializable接口就行了。

class Person implements Serializable{}

輸出的話,需要實例化一個”對象輸出流“對象,調(diào)用它的writeObject方法。

ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("D://demo.txt"));
out.writeObject(wkz);
//out是“對象輸出流”對象,wkz是需要被序列化的對象。

讀入類似,換成“對象讀入流”和readObject就行了。

ObjectInputStream in=new ObjectInputStream(new FileInputStream("D://demo.txt"));
Person who=(Person) in.readObject();
//注意要一個強轉

有transient標識的對象不參與序列化。

方法重寫

我們當然不能滿足于上述的基本使用,而是稍微探尋一下它的原理和個性化功能。

事實上,類似PHP對象在被序列化時自動調(diào)用__sleep方法,在被反序列化時自動調(diào)用__wakeup方法,Java對象在被序列化時會自動調(diào)用writeObject方法,在被反序列化時自動會調(diào)用readObject方法。而這些方法都是可以在 需要進行序列化相關操作的類里 被“重寫”的。

//“重寫”打上引號的原因,就是它并不需要加Override
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    //我原先以為重寫writeObject能讓我們改變輸出的Java序列化字節(jié)碼的格式,甚至可以輸出人話;但實際上并不是這樣(至少我不會)。
    //我們只是可以進行一些操作來改變對象屬性的值,最后還是得調(diào)用defaultWriteObject或WriteObject。
    //這里的defaultWriteObject就相當于我們重寫前的WriteObject。
    this.age=-1;
    s.defaultWriteObject();
    //此外,我們還可以干一些和序列化不相干的事,比如命令執(zhí)行。
    Runtime.getRuntime().exec("calc");
}
//這里跟上面差不多,就不多贅述了
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException,ClassNotFoundException{
    s.defaultReadObject();
    //注意在default之后再修改屬性,否則會被覆蓋
    this.age=100;
    //也可以命令執(zhí)行。
    Runtime.getRuntime().exec("calc");
}

重寫了上面兩個方法后,如果再對這個類的對象進行序列化相關的操作,就會使計算器被打開。這就是最原始的命令執(zhí)行。

調(diào)用鏈:基本的類嵌套&同名方法調(diào)用

(這塊涉及的內(nèi)容比較淺,可以說是我在PHP中最先學到的反序列化漏洞姿勢的 Java實現(xiàn)

前面所述的代碼屬于“入口類的readObject直接調(diào)用系統(tǒng)方法”;這種情況在真實環(huán)境中是很少出現(xiàn)的。更多的情況是“入口類參數(shù)中包含可控類對象,該類對象又調(diào)用別的類對象,別的類對象又.....幾層之后,才出現(xiàn)系統(tǒng)方法。

在類對象的調(diào)用過程中,如果讀入類對象的內(nèi)容可控,則用戶可以通過同名方法調(diào)用,將調(diào)用鏈引向開發(fā)者不曾設想的地方。

為了講述原理方便,這里只舉一個簡單的例子。

import java.io.Serializable;
import java.io.*;

/*
work類 和Person類,animal類,plant類(后面兩個沒寫代碼,就意思意思)屬于一塊邏輯,
開發(fā)者的想法是,讓用戶傳入一個屬于Person、animal、plant等類的對象,然后根據(jù)不同的類,進行不同的自我介紹。
但在每個類里都寫一個readObject方法太麻煩了,于是開發(fā)者用了個大的work類做包裹,直接調(diào)用其對象元素的toString方法。
但是work類的參數(shù)類型是Object且沒有額外過濾,所以可以干一些別的事情。

sys類是這個程序中,與上面那塊邏輯完全不相干的東西。
但是它的toString方法中有個系統(tǒng)調(diào)用。

于是,我們用sys對象作為屬性生成一個work對象(注釋的那三行)
并將其送入開發(fā)者提供的反序列化服務。
便可以成功進行syscall。
*/
class work implements Serializable{
    private Object thing;
    public work(Object thing) {
        this.thing = thing;
    }
    private void readObject(java.io.ObjectInputStream s)
         throws java.io.IOException,ClassNotFoundException{
     s.defaultReadObject();
     System.out.println(this.thing);
 }
}

class Person implements Serializable{
    private String name;
    private int age;
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    @Override
    public String toString(){
        return "introduce:Person{name='"+this.name+"',age='"+this.age+"}";
    }
}

class sys implements Serializable{
    @Override
    public String toString(){
        return "This is an syscall";
    }
}

public class one2022 {
    public static void main(String[] args) throws Exception{
        //work syscall=new work(new sys());
        //ObjectOutputStream out=new ObjectOutputStream(new FileOutputStream("D://demo.txt"));
        //out.writeObject(syscall);

        ObjectInputStream in=new ObjectInputStream(new FileInputStream("D://demo.txt"));
        in.readObject();
    }
}

代碼看起來很簡單,甚至有點傻;主要是看代碼對應的邏輯。

繼續(xù)深入?

基本的調(diào)用鏈邏輯,上面那個例子就夠了。

由于我Java知識的缺乏,這里如果接著上面的思路繼續(xù)寫反序列化鏈利用的話,就變成PHP那套__call,__invoke之類的東西了。

在PHP里,我不少很多出題人自己構造的反序列化鏈的題,也自己出過題,但主要的問題就是沒有找過框架層面的反序列化鏈,在比較真實的環(huán)境里找鏈的能力很弱。

所以在Java里,根據(jù)魔術方法構建反序列化鏈 這條老路我就不再走一遍了,而是學一些Java相關的知識和技巧后,開始嘗試在 正經(jīng)Java-web邏輯以及一些框架 里嘗試找鏈。

所以,在這里,就不繼續(xù)深入了。

Java反射

理解

與“正射”相對;不使用new來創(chuàng)建對象。

反射的作用:讓Java具有動態(tài)性。

PHP是一個動態(tài)性很強的語言;eval("字符串");可以直接將(用戶輸入的)字符串當作代碼執(zhí)行。但正常的Java就沒有這種功能。運用反射,可以讓java實現(xiàn)類似的功能。

基礎使用

以Person類為例。

class Person implements Serializable{
    public String name;
    private int age;
    public Person(){}
    public Person(String name,int age){
        this.name=name;
        this.age=age;
    }
    @Override
    public String toString(){
        return "Person{name='"+this.name+"',age='"+this.age+"}";
    }

    public void action(String s){
        System.out.println(s);
    }
}

反射的關鍵在于操作“類的原型”,即Class對象。

Person person=new Person();
Class c=person.getClass();//Class相當于類的原型

動態(tài)生成對象


//c.newInstance();
//可以直接調(diào)class對象的newInstance方法生成對象,但它只會調(diào)用person的無參構造方法,不能滿足我們的需求。
Constructor personcon=c.getConstructor(String.class,int.class);
//獲取以string和int作為類型的構造函數(shù);注意傳參是.class形式。
Person p=(Person) personcon.newInstance("pzc",19);
//用獲取的構造函數(shù)生成對象。
System.out.println(p);

獲取&修改對象屬性

//使用getField獲取 類原型 的公共屬性,并使用set作用于一個類對象,修改該屬性。
Field namefield0=c.getField("name");
namefield0.set(p,"hiddener");
System.out.println(p);

//使用getDeclaredfield獲取 類原型 的私有屬性,并使用setAccessible使其可修改。
//注意setAccessible沒有對象參數(shù),即,它是作用于屬性對象的(Field)
Field namefield1=c.getDeclaredField("age");
namefield1.setAccessible(true);
namefield1.set(p,20);
System.out.println(p);
//打印Person類的所有屬性(結果都是private int Person.age這種形式,和具體的實例化對象無關)
        Field[] personfields=c.getDeclaredFields();
        for (Field f:personfields){
            System.out.println(f);
        }

獲取&調(diào)用對象方法

//獲取方法與獲取屬性基本相同
//需要額外注意的是,這里的getMethod可以獲取繼承自父類的屬性,而getDeclaredMethod好像不行。
Method[] personmethods=c.getMethods();
    for(Method m:personmethods){
        System.out.println(m);
    }

//生成Method方法對象,并通過invoke調(diào)用Person類對象的方法。也是要注意參數(shù)。
    Method action=c.getMethod("action", String.class);
    action.invoke(p,"wawawa");
}

漏洞利用

(在反序列化漏洞中的應用)

定制需要的對象;

通過invoke調(diào)用除了同名函數(shù)以外的函數(shù);

通過Class類創(chuàng)建對象,引入不能序列化的類。

JDK動態(tài)代理

代理模式是一種設計模式。(類似“工廠模式”這種)

其主要意圖是為其他對象提供一種代理以控制對這個對象的訪問。

靜態(tài)代理

先有一個類。

public class User0 implements IUser{
    public User0(){
    }

    @Override
    public void show(){
        System.out.println("展示");
    }
    @Override
    public void update(){
        System.out.println("更新");
    }
}

該類實現(xiàn)了一個IUser接口,它是代理必然需要的東西。在這個靜態(tài)代理的樣例里,它是這樣寫的:

public interface IUser {
    void show();
    void update();
}

我們還需要用一個代理類實現(xiàn)這個接口。

public class UserProxy implements IUser{
    IUser user;
    public UserProxy(IUser user){this.user=user;}
    @Override
    public void show(){
        user.show();
        System.out.println("調(diào)用了show");
    }
    @Override
    public void update(){
        user.update();
        System.out.println("調(diào)用了update");
    }
}

最后進行調(diào)用測試。

public class ProxyTest {
    public static void main(String[] args){
        IUser user=new User0();
        IUser userProxy=new UserProxy(user);
        userProxy.show();
        //使用userProxy調(diào)用user的show方法
    }
}

可以看到,我們使用userProxy調(diào)用了user的show方法,同時userProxy生成了“調(diào)用了show”調(diào)用日志。調(diào)用日志記錄這個功能是不需要show本身實現(xiàn)的,這樣會顯得邏輯很混亂。加一個代理類負責記錄各種日志,同時也達到了代理模式中“控制對這個對象的訪問”的意圖。

動態(tài)代理

但是,前面靜態(tài)代理的缺點是顯而易見的。對于接口里聲明的每一個方法,我們都要在UserProxy代理類里寫一個對應的方法來實現(xiàn)它,這樣非常麻煩,而且容易產(chǎn)生大量重復代碼。

我們的想法是,最好,無論接口聲明了多少方法,代理類都用同一個方法實現(xiàn)代理,且實現(xiàn)對需要代理的不同方法的不同處理。

然而,正常情況,在寫代理類方法時,我們無法從內(nèi)部獲知外面調(diào)用了代理接口的哪一種方法。

所以,需要使用Java自帶的動態(tài)代理科技。

還是原來的User0類和接口:

public class User0 implements IUser{
    public User0(){
    }

    @Override
    public void show(){
        System.out.println("展示");
    }
    @Override
    public void update(){
        System.out.println("更新");
    }
}

public interface IUser {
    void show();
    void update();
}

但是,代理類和之前相比,有了很大的不同:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class UserInvocationHandler implements InvocationHandler {
    IUser user;
    public UserInvocationHandler(IUser user){
        this.user=user;
    }
    @Override
    public Object invoke(Object proxy, Method method,Object[] args) throws Throwable{
        String name=method.getName();
        System.out.println("調(diào)用了"+name);

        method.invoke(user,args);
        return null;
    }
}

調(diào)用測試:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;

public class ProxyTest {
    public static void main(String[] args){
        IUser user=new User0();
        InvocationHandler userinvocationhandler=new UserInvocationHandler(user);
        //classloader,要代理的接口,要做的事情
        IUser userProxy=(IUser) Proxy.newProxyInstance(user.getClass().getClassLoader(),user.getClass().getInterfaces(),userinvocationhandler);
        userProxy.update();
    }
}

這套東西能實現(xiàn)剛才那個需求的原因是,我們自己寫的代理管理器類(動態(tài)代理類;實現(xiàn)了InvocationHandler接口的UserInvocationHandler)有Method參數(shù)。這里面的invoke是個重寫,參數(shù)是固定的;即,能有這個參數(shù),是Java本身想好了的。

關于動態(tài)代理里涉及到的各種新類、新方法,這里就不贅述了。以后有機會的話再慢慢研究。大體的研究思路是跟進去看源碼,看傳參類型,不懂的就查資料問人,在這個過程中多學一些java相關的知識。

漏洞利用

在動態(tài)代理類存在時,前面不管調(diào)了什么,都會經(jīng)過它的invoke,而invoke后的調(diào)用和前面的調(diào)用就沒啥關系了。有時可以起到鏈拼接的效果。

動態(tài)代理類的invoke在有函數(shù)調(diào)用時自動執(zhí)行;這和前面 readObject在反序列化時自動執(zhí)行有異曲同工之妙。

類的動態(tài)加載

感覺這東西難度挺大的。

類加載流程

基礎知識

其中,加載和連接不是嚴格的先后關系,而是并列的。

Java類除了我們熟知的方法(構造方法,靜態(tài)方法等),還有“代碼塊”這種東西。其分為靜態(tài)代碼塊和構造代碼塊;

除了我們熟知的類實例化(生成對象),還有“類初始化”階段。

先擺出結論:上述內(nèi)容中,靜態(tài)代碼塊屬于初始化范疇,其他都屬于使用范疇;初始化中內(nèi)容只執(zhí)行一次,而“使用”中的內(nèi)容可以執(zhí)行多次。除了構造方法和(其他)魔術方法,一般情況下方法都需要顯式調(diào)用才會執(zhí)行,靜態(tài)方法也不例外。

基礎測試

public class Test {
    public String name;
    private int age;

    public static int id;
    static {
        System.out.print("靜態(tài)代碼塊 ");
    }
    {
        System.out.print("構造代碼塊 ");
    }

    public static void staticAction(){
        System.out.print("靜態(tài)方法 ");
    }
    public Test() {System.out.print("構造方法" );}
}

以下,被注釋分割的都是一個個獨立的測試。

new Test();
//靜態(tài)代碼塊 構造代碼塊 構造方法
//用new,就一股腦全執(zhí)行了,沒啥好說的。
Class c=Test.class;
c.getConstructor();
//
//獲取類原型,以及調(diào)用類原型的大部分方法,都不進行初始化操作。
Class c=Test.class;
c.newInstance();
//靜態(tài)代碼塊 構造代碼塊 構造方法
//用反射直接實現(xiàn)類實例化,也是一股腦全調(diào)用
new Test();
Class c=Test.class;
c.newInstance();
//靜態(tài)代碼塊 構造代碼塊 構造方法 構造代碼塊 構造方法
//靜態(tài)代碼塊只執(zhí)行一次。

Class.forName("Test");
//靜態(tài)代碼塊
//調(diào)用這玩意也執(zhí)行初始化,有點神奇奧
ClassLoader cl=ClassLoader.getSystemClassLoader();
Class.forName("Test",false,cl);
//
//通過改參數(shù),讓它不初始化了。

最后兩個測試多說一句;我們跟到forName里,發(fā)現(xiàn)

打開Structure,找其他forName:

看到還有個第一個參數(shù)也是String的forName,點過去:

發(fā)現(xiàn)initialize參數(shù),設置為false;最后那個ClassLoader,先別管是啥,模仿著生成個傳進去不報錯就行了。

類加載調(diào)試

先補充一句;ClassLoader的loadClass方法不會引起類初始化。

操作

原則:loadClassloadClassOrNull進,其余跳。

過程:

它先跳到了ClassLoader里的單參數(shù)loadClass,再到了ClassLoaders里的loadClass。在ClassLoaders.loadClass里進行一些安全檢查后,直接調(diào)用父類雙參數(shù)super.loadClass(cn, resolve)進入BuiltinClassLoader類。

BuiltinClassLoader類是重頭戲;后面基本就在這個類里來回跳了。它在里面調(diào)自己的私有l(wèi)oadClassOrNull方法。該方法檢查parent屬性,若不為空,則調(diào)它的loadClassOrNull方法。

第一輪中,該屬性是PlatFormClassLoader類

繼續(xù),很快又回到了這里,發(fā)現(xiàn)是BootClassLoader類

繼續(xù),發(fā)現(xiàn)在Boot這層,最后c的返回值為null;在platform這一層,c的返回值為“class Test”。

這個Test一直被回帶,最終回到測試代碼里。注意測試代碼中的ClassLoader cl是AppClassLoader類。

解釋

這種類加載過程與Java的雙親委派模型有關。

雙親委派模型其實是單親(拳師警告);它反映的是一種調(diào)用關系:當類生成時,會先找到最頂層的加載器,從它開始加載類;若它不能加載,下一層的加載器再嘗試加載,以此類推。

圖中,Extension ClassLoader對應我們調(diào)試中的 Platform ClassLoader;我們沒有寫自定義ClassLoader,剛開始就是AppClassLoader。

所以,前面的調(diào)試過程反映的流程是:我們實例化的APPClassLoader加載器通過PlatformClassLoader找到最頂層BootClassLoader,Boot不能加載那個類;再通過PlatForm加載。加載成功,返回。

一些利用

URLClassLoader加載任意類

把之前Test類生成的.class文件放在了項目根目錄。

進行復現(xiàn)操作:

URLClassLoader urlclassloader=
         new URLClassLoader(new URL[]{new URL("http://localhost:9999/")});
Class<?> c=urlclassloader.loadClass("Test");
c.newInstance();

能夠執(zhí)行。

(這個過程建議也調(diào)一下,比上面稍復雜一點;它在BuiltinClassLoader里沒找到,catch了一個exception,之后再URLClassLoader類里找到的。)

先告一段落吧。

本文摘自 :https://www.cnblogs.com/

開通會員,享受整站包年服務立即開通 >