當(dāng)前位置:首頁 > IT技術(shù) > 編程語言 > 正文

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快捷鍵使用

跟進(jìn)類、方法:Ctrl+B

彈出structure框框:Alt+7

Java原生(反)序列化

基本使用

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

class Person implements Serializable{}

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

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

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

ObjectInputStream in=new ObjectInputStream(new FileInputStream("D://demo.txt"));
Person who=(Person) in.readObject();
//注意要一個(gè)強(qiáng)轉(zhuǎn)

有transient標(biāo)識(shí)的對(duì)象不參與序列化。

方法重寫

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

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

//“重寫”打上引號(hào)的原因,就是它并不需要加Override
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    //我原先以為重寫writeObject能讓我們改變輸出的Java序列化字節(jié)碼的格式,甚至可以輸出人話;但實(shí)際上并不是這樣(至少我不會(huì))。
    //我們只是可以進(jìn)行一些操作來改變對(duì)象屬性的值,最后還是得調(diào)用defaultWriteObject或WriteObject。
    //這里的defaultWriteObject就相當(dāng)于我們重寫前的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之后再修改屬性,否則會(huì)被覆蓋
    this.age=100;
    //也可以命令執(zhí)行。
    Runtime.getRuntime().exec("calc");
}

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

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

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

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

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

為了講述原理方便,這里只舉一個(gè)簡(jiǎn)單的例子。

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

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

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

于是,我們用sys對(duì)象作為屬性生成一個(gè)work對(duì)象(注釋的那三行)
并將其送入開發(fā)者提供的反序列化服務(wù)。
便可以成功進(jìn)行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();
    }
}

代碼看起來很簡(jiǎn)單,甚至有點(diǎn)傻;主要是看代碼對(duì)應(yīng)的邏輯。

繼續(xù)深入?

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

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

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

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

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

Java反射

理解

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

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

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

基礎(chǔ)使用

以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);
    }
}

反射的關(guān)鍵在于操作“類的原型”,即Class對(duì)象。

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

動(dòng)態(tài)生成對(duì)象


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

獲取&修改對(duì)象屬性

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

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

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

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

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

漏洞利用

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

定制需要的對(duì)象;

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

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

JDK動(dòng)態(tài)代理

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

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

靜態(tài)代理

先有一個(gè)類。

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

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

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

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

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

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");
    }
}

最后進(jìn)行調(diào)用測(cè)試。

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方法,同時(shí)userProxy生成了“調(diào)用了show”調(diào)用日志。調(diào)用日志記錄這個(gè)功能是不需要show本身實(shí)現(xiàn)的,這樣會(huì)顯得邏輯很混亂。加一個(gè)代理類負(fù)責(zé)記錄各種日志,同時(shí)也達(dá)到了代理模式中“控制對(duì)這個(gè)對(duì)象的訪問”的意圖。

動(dòng)態(tài)代理

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

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

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

所以,需要使用Java自帶的動(dòng)態(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)用測(cè)試:

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();
    }
}

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

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

漏洞利用

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

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

類的動(dòng)態(tài)加載

感覺這東西難度挺大的。

類加載流程

基礎(chǔ)知識(shí)

其中,加載和連接不是嚴(yán)格的先后關(guān)系,而是并列的。

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

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

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

基礎(chǔ)測(cè)試

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

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

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

以下,被注釋分割的都是一個(gè)個(gè)獨(dú)立的測(cè)試。

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

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

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

打開Structure,找其他forName:

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

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

類加載調(diào)試

先補(bǔ)充一句;ClassLoader的loadClass方法不會(huì)引起類初始化。

操作

原則:loadClassloadClassOrNull進(jìn),其余跳。

過程:

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

BuiltinClassLoader類是重頭戲;后面基本就在這個(gè)類里來回跳了。它在里面調(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”。

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

解釋

這種類加載過程與Java的雙親委派模型有關(guān)。

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

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

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

一些利用

URLClassLoader加載任意類

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

進(jìn)行復(fù)現(xiàn)操作:

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

能夠執(zhí)行。

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

先告一段落吧。

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

開通會(huì)員,享受整站包年服務(wù)立即開通 >