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
跟进类、方法:Ctrl+B
弹出structure框框:Alt+7
让需要被(反)序列化的类实现一下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逻辑以及一些框架 里尝试找链。
所以,在这里,就不继续深入了。
与“正射”相对;不使用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类创建对象,引入不能序列化的类。
代理模式是一种设计模式。(类似“工厂模式”这种)
其主要意图是为其他对象提供一种代理以控制对这个对象的访问。
先有一个类。
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方法不会引起类初始化。
原则:loadClass和loadClassOrNull进,其余跳。
过程:
它先跳到了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加载。加载成功,返回。
把之前Test类生成的.class文件放在了项目根目录。
进行复现操作:
URLClassLoader urlclassloader= new URLClassLoader(new URL[]{new URL("http://localhost:9999/")}); Class<?> c=urlclassloader.loadClass("Test"); c.newInstance();
能够执行。
(这个过程建议也调一下,比上面稍复杂一点;它在BuiltinClassLoader里没找到,catch了一个exception,之后再URLClassLoader类里找到的。)
先告一段落吧。