【JAVA反序列化_一】反射基础
须知:
这个图相信不陌生,是shiro反序列化利用工具,其中这利用链说实话当我第一次见的时候满脸问号,于是就想探究一番,说实话我之前学的就是java,不过其实跟研究这个东西还是有差别的,所以说接下来我打算研究JAVA反序列化利用链,虽然网上一大堆文章了,但是我感觉他们并不友好,并且有一些我认为他们根本就似懂非懂跟着debug一下就完事了,就认为自己会了,我认为学一个东西就是从无到有的过程并且这个过程中肯定会产生很多疑问,最后解决这些疑问,我认为这样的过程才是真正学会了,所以我接下来要写的文章会做到相对友好细致(当然我不可能从最基础的JAVA开始),并且在这其中会有很多问题并且会有解答,这些文章我会分多次发布,会先发一些基础然后再研究,如果基础看好了后边的文章看着不会非常吃力。
文章更新会优先于公众号:小惜渗透,建议用电脑观看
1. 反射基础
1.1 简单举例
学过java面向对象的都知道,在修饰变量、方法、类的时候,有四种修饰:私有的、受保护的、默认的、公共的,java里面有个特牛的东西叫反射
,利用好它你甚至可以调用任意类的私有方法,而我们的反序列化利用也是跟它所离不开的,下面的代码我展示了一个非常简单的反射的例子。
//Person类
class Person{
String like = "苹果";
public void eat(){
System.out.println("吃"+like);
}
}
public class test {
public static void main(String[] args) throws Exception {
Class p = Class.forName("Person");
p.getMethod("eat").invoke(p.newInstance());
}
}
先简单解释一下下面的几个在反射中常用的方法:
forName:
通过该函数可以获得一个类
getMethod:
获取类的方法
newInstance:
用类实例化出一个对象
invoke:
执行函数的方法
1.2 forName方法
此函数有两个函数重载(函数名相同,参数列表不同)
//第一种
public static Class<?> forName(String className)
//第二种
public static Class<?> forName(String name, boolean initialize,ClassLoader loader)
第一种也就是实例代码中的了,同时也是较为常用的一种,但其实它也是第二种的一个变形而已,不过是后边两个参数赋予了默认值
//它们两个事相等的
Class.forName(className)
Class.forName(className, true, currentLoader)
该函数第一种方式最为常用,但是第二种也要了解一下
其中第一个参数是类名这个就不用说了,第二个参数表示是否初始化,第三个参数表示ClassLoader
什么是初始化,要分清它和实例化是两回事,初始化是完成程序执行前的准备工作,初始化相关静态代码块和赋值,并分配空间,而且初始化只在类加载的时候执行一次
ClassLoader
顾名思义就是一个加载器,它告诉虚拟机如何加载这个类
1.3 newInstance方法
class.newInstance() 的作用对该类进行实例化并等到实例化后的对象,这里要先扩展一下JAVA中的实例化,看如下代码,我对Person类实例化得到p对象,调用p对象的eat方法
class Person{
String like = "苹果";
public void eat(){
System.out.println("吃"+like);
}
}
public class test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.eat();
}
}
可能这是个很简单的一些代码,但是其实我们要知道这个实例化它是根据什么来的,它其实是根据构造器(构造函数)来的,在Java中就算一个类没有手动写一个构造器(就像上方的演示代码一样),系统会分配给它一个默认的空参构造器,但是当我们手动写了构造器的时候就会把这个空餐构造器给顶掉了,就算你写的是有参构造也会顶掉(如下方代码,运行失败),所以这就是为什么上面的演示代码就算我们没写构造器也可以进行new
。
class Person{
String like = "苹果";
public Person(String like) {
this.like = like;
}
public void eat(){
System.out.println("吃"+like);
}
}
public class test {
public static void main(String[] args) throws Exception {
Person p = new Person();
p.eat();
}
}
因为我们手写了构造器,所以相当于把系统给的无参构造器给扔了,所以我们再使用无参构造new的话就会报错
这个时候再想实例化,就必须使用我们写的那个有参构造了
知道了上述这些我们再来重新看一下newInstance()函数,之前说了它就是实例化用的,再严谨一点,当用class对象调用它时,其实它实例化调用的是这个类的无参构造函数,那这时候问题来了,当一个类的没有无参构造器或者无参构造器是私有的我们怎么获取实例化的对象呢?当然有办法做到
- 首先当一个类没有无参构造,也没有返回对象的静态方法的时候,我们如何获取实例化对象
我们需要用到getConstructor
,它的作用就是根据传递的参数,获取对应的构造器,然后再用newInstance
函数实例化,我举个例子,看下方代码一目了然
import java.lang.reflect.Constructor;
class Person{
String like = "苹果";
//构造方法也是可以重载(函数名相同,参数列表不同)的
public Person(String g1) {
System.out.println("我是第一个构造器");
}
public Person(String s2,int i2){
System.out.println("我是第二个构造器");
};
public void eat(){
System.out.println("吃"+like);
}
}
public class test {
public static void main(String[] args) throws Exception {
Class<?> person = Class.forName("Person");
//根据传递getConstructor不同的参数来定位并获取指定的构造器
//获取第一个构造器
Constructor<?> g1 = person.getConstructor(String.class);
//获取第二个构造器
Constructor<?> g2 = person.getConstructor(String.class,int.class);
person.getMethod("eat").invoke(g1.newInstance("第一个"));
System.out.println("---------------------------------------------");
person.getMethod("eat").invoke(g2.newInstance("第二个",2));
}
}
然后可以成功执行
- 那我们怎么运行私有化的方法的呢?
这点也很简单,反射是很牛的,这点问题也难不倒它,它还给我们提供了一个函数getDeclaredMethod
:
- 正常的
getMethod
函数,只能获取类的公有方法,包括从父类继承过来的 getDeclaredMethod
方法获取的就是在当前类里实实在在写了的方法,包括私有的方法,不包含从父类继承来的。
它的使用方法如下所示:
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
class Person{
String like = "苹果";
private void eat(){
System.out.println("吃"+like);
}
private void eat(String food){
System.out.println("吃"+food);
}
}
public class test {
public static void main(String[] args) throws Exception {
Class<?> person = Class.forName("Person");
Method method = person.getDeclaredMethod("eat");
//这里使用了一个方法setAccessible,它的作用就是修改私有方法的作用域,不用深究记住就行
method.setAccessible(true);
method.invoke(person.newInstance());
System.out.println("--------------------");
Method method1 = person.getDeclaredMethod("eat",String.class);
method1.setAccessible(true);
method1.invoke(person.newInstance(),"西瓜");
}
}
执行结果如下图
1.4 invoke方法
可以看到invoke
函数的参数由两部分组成,第二部分其实是一个可变长参数,我们先不做讨论,先说一下它的第一个参数
- 如果这个方法是一个普通方法,那么第一个参数是类对象
- 如果这个方法是一个静态方法,那么第一个参数是类
简单理解就是把使用顺序反过来了,这个需要知道我们正常使用类的方法时都是先实例化对象,在通过对象调用方法。当方法是静态方法的时候,使用就可以直接类名调用方法,这种情况通常用在工具类上(包含大量重复使用的函数的类)。
对象.方法名
方法.invoke(对象)
//静态方法时
类名.方法名
方法名.invoke(类)
所以我们这里是一个普通的方法,它的参数应该是类对象,所以在示例代码中,invoke
的参数就是用newInstance
实例化出的一个对象
既然newInstance
对类进行实例化并得到一个对象,so,我们要调用这个公有的eat
函数,还可以这样,直接通过反射实例化出一个对象并调用这个对象的eat
,其实虽然两种方式都可以执行eat
class Person{
String like = "苹果";
public void eat(){
System.out.println("吃"+like);
}
}
public class test {
public static void main(String[] args) throws Exception {
Class p = Class.forName("Person");
// p.getMethod("eat").invoke(p.newInstance());
Person person = (Person) p.newInstance();
person.eat();
}
}