Java反射&(反)序列化入门

写在前面

参考资料

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原生(反)序列化

基本使用

让需要被(反)序列化的类实现一下Serializable接口就行了。

class Person implements Serializable{}

输出的话,需要实例化一个”对象输出流“对象,调用它的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对象在被序列化时自动调用__sleep方法,在被反序列化时自动调用__wakeup方法,Java对象在被序列化时会自动调用writeObject方法,在被反序列化时自动会调用readObject方法。而这些方法都是可以在 需要进行序列化相关操作的类里 被“重写”的。

//“重写”打上引号的原因,就是它并不需要加Override
private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
    //我原先以为重写writeObject能让我们改变输出的Java序列化字节码的格式,甚至可以输出人话;但实际上并不是这样(至少我不会)。
    //我们只是可以进行一些操作来改变对象属性的值,最后还是得调用defaultWriteObject或WriteObject。
    //这里的defaultWriteObject就相当于我们重写前的WriteObject。
    this.age=-1;
    s.defaultWriteObject();
    //此外,我们还可以干一些和序列化不相干的事,比如命令执行。
    Runtime.getRuntime().exec("calc");
}
//这里跟上面差不多,就不多赘述了
private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException,ClassNotFoundException{
    s.defaultReadObject();
    //注意在default之后再修改属性,否则会被覆盖
    this.age=100;
    //也可以命令执行。
    Runtime.getRuntime().exec("calc");
}

重写了上面两个方法后,如果再对这个类的对象进行序列化相关的操作,就会使计算器被打开。这就是最原始的命令执行。

调用链:基本的类嵌套&同名方法调用

(这块涉及的内容比较浅,可以说是我在PHP中最先学到的反序列化漏洞姿势的 Java实现

前面所述的代码属于“入口类的readObject直接调用系统方法”;这种情况在真实环境中是很少出现的。更多的情况是“入口类参数中包含可控类对象,该类对象又调用别的类对象,别的类对象又.....几层之后,才出现系统方法。

在类对象的调用过程中,如果读入类对象的内容可控,则用户可以通过同名方法调用,将调用链引向开发者不曾设想的地方。

为了讲述原理方便,这里只举一个简单的例子。

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

/*
work类 和Person类,animal类,plant类(后面两个没写代码,就意思意思)属于一块逻辑,
开发者的想法是,让用户传入一个属于Person、animal、plant等类的对象,然后根据不同的类,进行不同的自我介绍。
但在每个类里都写一个readObject方法太麻烦了,于是开发者用了个大的work类做包裹,直接调用其对象元素的toString方法。
但是work类的参数类型是Object且没有额外过滤,所以可以干一些别的事情。

sys类是这个程序中,与上面那块逻辑完全不相干的东西。
但是它的toString方法中有个系统调用。

于是,我们用sys对象作为属性生成一个work对象(注释的那三行)
并将其送入开发者提供的反序列化服务。
便可以成功进行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();
    }
}

代码看起来很简单,甚至有点傻;主要是看代码对应的逻辑。

继续深入?

基本的调用链逻辑,上面那个例子就够了。

由于我Java知识的缺乏,这里如果接着上面的思路继续写反序列化链利用的话,就变成PHP那套__call,__invoke之类的东西了。

在PHP里,我不少很多出题人自己构造的反序列化链的题,也自己出过题,但主要的问题就是没有找过框架层面的反序列化链,在比较真实的环境里找链的能力很弱

所以在Java里,根据魔术方法构建反序列化链 这条老路我就不再走一遍了,而是学一些Java相关的知识和技巧后,开始尝试在 正经Java-web逻辑以及一些框架 里尝试找链。

所以,在这里,就不继续深入了。

Java反射

理解

与“正射”相对;不使用new来创建对象。

反射的作用:让Java具有动态性。

PHP是一个动态性很强的语言;eval("字符串");可以直接将(用户输入的)字符串当作代码执行。但正常的Java就没有这种功能。运用反射,可以让java实现类似的功能。

基础使用

以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相当于类的原型

动态生成对象


//c.newInstance();
//可以直接调class对象的newInstance方法生成对象,但它只会调用person的无参构造方法,不能满足我们的需求。
Constructor personcon=c.getConstructor(String.class,int.class);
//获取以string和int作为类型的构造函数;注意传参是.class形式。
Person p=(Person) personcon.newInstance("pzc",19);
//用获取的构造函数生成对象。
System.out.println(p);

获取&修改对象属性

//使用getField获取 类原型 的公共属性,并使用set作用于一个类对象,修改该属性。
Field namefield0=c.getField("name");
namefield0.set(p,"hiddener");
System.out.println(p);

//使用getDeclaredfield获取 类原型 的私有属性,并使用setAccessible使其可修改。
//注意setAccessible没有对象参数,即,它是作用于属性对象的(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);
        }

获取&调用对象方法

//获取方法与获取属性基本相同
//需要额外注意的是,这里的getMethod可以获取继承自父类的属性,而getDeclaredMethod好像不行。
Method[] personmethods=c.getMethods();
    for(Method m:personmethods){
        System.out.println(m);
    }

//生成Method方法对象,并通过invoke调用Person类对象的方法。也是要注意参数。
    Method action=c.getMethod("action", String.class);
    action.invoke(p,"wawawa");
}

漏洞利用

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

定制需要的对象;

通过invoke调用除了同名函数以外的函数;

通过Class类创建对象,引入不能序列化的类。

JDK动态代理

代理模式是一种设计模式。(类似“工厂模式”这种)

其主要意图是为其他对象提供一种代理以控制对这个对象的访问。

静态代理

先有一个类。

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

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

该类实现了一个IUser接口,它是代理必然需要的东西。在这个静态代理的样例里,它是这样写的:

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

我们还需要用一个代理类实现这个接口。

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

最后进行调用测试。

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

可以看到,我们使用userProxy调用了user的show方法,同时userProxy生成了“调用了show”调用日志。调用日志记录这个功能是不需要show本身实现的,这样会显得逻辑很混乱。加一个代理类负责记录各种日志,同时也达到了代理模式中“控制对这个对象的访问”的意图。

动态代理

但是,前面静态代理的缺点是显而易见的。对于接口里声明的每一个方法,我们都要在UserProxy代理类里写一个对应的方法来实现它,这样非常麻烦,而且容易产生大量重复代码。

我们的想法是,最好,无论接口声明了多少方法,代理类都用同一个方法实现代理,且实现对需要代理的不同方法的不同处理

然而,正常情况,在写代理类方法时,我们无法从内部获知外面调用了代理接口的哪一种方法。

所以,需要使用Java自带的动态代理科技。

还是原来的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("调用了"+name);

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

调用测试:

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

这套东西能实现刚才那个需求的原因是,我们自己写的代理管理器类(动态代理类;实现了InvocationHandler接口的UserInvocationHandler)有Method参数。这里面的invoke是个重写,参数是固定的;即,能有这个参数,是Java本身想好了的。

关于动态代理里涉及到的各种新类、新方法,这里就不赘述了。以后有机会的话再慢慢研究。大体的研究思路是跟进去看源码,看传参类型,不懂的就查资料问人,在这个过程中多学一些java相关的知识。

漏洞利用

在动态代理类存在时,前面不管调了什么,都会经过它的invoke,而invoke后的调用和前面的调用就没啥关系了。有时可以起到链拼接的效果。

动态代理类的invoke在有函数调用时自动执行;这和前面 readObject在反序列化时自动执行有异曲同工之妙。

类的动态加载

感觉这东西难度挺大的。

类加载流程

基础知识

其中,加载和连接不是严格的先后关系,而是并列的。

Java类除了我们熟知的方法(构造方法,静态方法等),还有“代码块”这种东西。其分为静态代码块和构造代码块

除了我们熟知的类实例化(生成对象),还有“类初始化”阶段。

先摆出结论:上述内容中,静态代码块属于初始化范畴,其他都属于使用范畴;初始化中内容只执行一次,而“使用”中的内容可以执行多次。除了构造方法和(其他)魔术方法,一般情况下方法都需要显式调用才会执行,静态方法也不例外。

基础测试

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

    public static int id;
    static {
        System.out.print("静态代码块 ");
    }
    {
        System.out.print("构造代码块 ");
    }

    public static void staticAction(){
        System.out.print("静态方法 ");
    }
    public Test() {System.out.print("构造方法" );}
}

以下,被注释分割的都是一个个独立的测试。

new Test();
//静态代码块 构造代码块 构造方法
//用new,就一股脑全执行了,没啥好说的。
Class c=Test.class;
c.getConstructor();
//
//获取类原型,以及调用类原型的大部分方法,都不进行初始化操作。
Class c=Test.class;
c.newInstance();
//静态代码块 构造代码块 构造方法
//用反射直接实现类实例化,也是一股脑全调用
new Test();
Class c=Test.class;
c.newInstance();
//静态代码块 构造代码块 构造方法 构造代码块 构造方法
//静态代码块只执行一次。

Class.forName("Test");
//静态代码块
//调用这玩意也执行初始化,有点神奇奥
ClassLoader cl=ClassLoader.getSystemClassLoader();
Class.forName("Test",false,cl);
//
//通过改参数,让它不初始化了。

最后两个测试多说一句;我们跟到forName里,发现

打开Structure,找其他forName:

看到还有个第一个参数也是String的forName,点过去:

发现initialize参数,设置为false;最后那个ClassLoader,先别管是啥,模仿着生成个传进去不报错就行了。

类加载调试

先补充一句;ClassLoader的loadClass方法不会引起类初始化。

操作

原则:loadClassloadClassOrNull进,其余跳。

过程:

它先跳到了ClassLoader里的单参数loadClass,再到了ClassLoaders里的loadClass。在ClassLoaders.loadClass里进行一些安全检查后,直接调用父类双参数super.loadClass(cn, resolve)进入BuiltinClassLoader类

BuiltinClassLoader类是重头戏;后面基本就在这个类里来回跳了。它在里面调自己的私有loadClassOrNull方法。该方法检查parent属性,若不为空,则调它的loadClassOrNull方法。

第一轮中,该属性是PlatFormClassLoader类

继续,很快又回到了这里,发现是BootClassLoader类

继续,发现在Boot这层,最后c的返回值为null;在platform这一层,c的返回值为“class Test”。

这个Test一直被回带,最终回到测试代码里。注意测试代码中的ClassLoader cl是AppClassLoader类

解释

这种类加载过程与Java的双亲委派模型有关。

双亲委派模型其实是单亲(拳师警告);它反映的是一种调用关系:当类生成时,会先找到最顶层的加载器,从它开始加载类;若它不能加载,下一层的加载器再尝试加载,以此类推。

图中,Extension ClassLoader对应我们调试中的 Platform ClassLoader;我们没有写自定义ClassLoader,刚开始就是AppClassLoader。

所以,前面的调试过程反映的流程是:我们实例化的APPClassLoader加载器通过PlatformClassLoader找到最顶层BootClassLoader,Boot不能加载那个类;再通过PlatForm加载。加载成功,返回。

一些利用

URLClassLoader加载任意类

把之前Test类生成的.class文件放在了项目根目录。

进行复现操作:

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

能够执行。

(这个过程建议也调一下,比上面稍复杂一点;它在BuiltinClassLoader里没找到,catch了一个exception,之后再URLClassLoader类里找到的。)

先告一段落吧。

posted @ 2022-04-29 12:25  hiddener  阅读(446)  评论(0编辑  收藏  举报