Java反射&(反)序列化入门
写在前面
参考资料
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方法不会引起类初始化。
操作
原则: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加载。加载成功,返回。
一些利用
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类里找到的。)
先告一段落吧。