SpringKill
- 关注
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9

Java反序列化深度讲解(一)
前言
之所以写这一系列文章,是因为想到自己当时在学习反序列化的时候手头根本没有多少资料,网上的各种资料也是比较七零八碎,很难找到体系化学习的文章,同时最近工作中又遇到了反序列化相关的问题,所以向通过写文章的方式帮助各位想学习和了解Java反序列化的师傅们体系学习,文中可能有不对的地方还希望各位大佬及时指出。
**P.S.阅读本文需要有一定的Java基础 **
反射
在反序列化中我们经常要使用反射,比如通过实例化某个类来执行构造函数,又或者是绕过沙箱检测等等,所以说想要学习反序列化要先了解反射,在这里我只做简单的介绍,要想详细了解反射可以去看相关文章。
什么是反射
反射为很多代码提供了动态特性,Java中也有反射机制,通过反射可以拿到特定的类以及其方法。
正式一点的说法就是:反射指程序可以访问、检测和修改它本身状态或行为的一种能力
反射API
使用Java反射API,你可以在运行时执行以下操作:
获取类的信息:你可以获取类的名称、父类、实现的接口、字段和方法等信息。
实例化对象:通过反射,你可以实例化一个类的对象,即使你在编译时并不知道该类的存在。
访问和修改字段:你可以使用反射获取类的字段,并读取或修改其值。
调用方法:反射允许你调用一个类的方法,包括公共方法、私有方法和静态方法。
创建和操作数组:你可以使用反射创建和操作数组对象。
……
要使用Java反射API,你需要使用java.lang.reflect
包中的类和接口,常见的反射API包含以下五个类:
Field 类:表示类的字段,可以用于获取和修改字段的值。
Constructor 类:表示类的构造函数,可以用于实例化对象。
Method 类:表示类的方法,可以用于调用方法。
Class 类:表示一个类或接口,在运行时可以获取和操作类的信息。
Object 类:Object 是所有 Java 类的父类。所有对象都默认实现了 Object 类的方法。
** 使用反射API可以通过反射快速获得这个类的全部信息,通过obj.getCalss()``obj.class
或Class.forName()
方法获得类,通过Field类中的getDeclaredFields
和getFields
获取所有成员变量然后获取构造函数、方法……具体怎么实现后面在说单例方法的时候会详细介绍。**
反射、初始化和构造函数
我们拿forName举例,forName方法有两个重载分别是:
Class<?> forName(String name)
Class<?> forName(String name, **boolean** initialize, ClassLoader loader)
我们在使用第一个重载的时候实际上调用的是forName0函数:
只是在调用forName0的时候自动获取了ClassLoader以及默认设置initialize为true。
关于ClassLoader要提到JVM的加载机制,这些我在后面会做说明。
回到这两个重载,我们可以看到另一个重载同样调用了forName0,并且参数都是可控的,那么不如就把第一个重载看为第二个的简单实现。,二者只是向forName0中传参的方式不同。
forName0方法中有一个有意思的参数initialize
,这个参数直接字面上理解就是初始化,那么什么是初始化呢?
先来看个例子:
public class Test {
static {
System.out.println("静态代码块");
}
{
System.out.println("构造代码块");
}
Test(){
System.out.println("构造函数");
}
public static void main(String[] args){
new Test();
new Test();
}
}
这段代码执行后发生了什么呢?
其中每部分是什么已经写得很清楚了,这是两次new的结果,但是却只出现了一个静态代码块,那么就说明在类初始话的时候只执行一次静态代码块。
那么为什么要说这些呢,可以看下列的例子:
public class TestForName {
public static void main(String[] args) throws ClassNotFoundException {
// new Test();
Class a = Class.forName("Test");
}
}
我们先注释掉new然后运行一下发现程序调用了静态代码块:
那么我们将new取消注释,再次运行:
结果是new函数先执行,静态代码块被执行,后续的forName并没有再次运行静态代码块。
我们再修改一下并debug:
public class TestForName {
public static void main(String[] args) throws ClassNotFoundException {
Class a = Class.forName("Test");
new Test();
}
}
可以看到在forName调用后就运行了静态代码块的代码,再往后继续执行,程序结束,也只调用了一次静态代码块:
说这么多其实只是总结一句话:只有在类最初被加载到内存中的时候才会执行其中的静态代码块,然后再次加载只是生成一个副本,并不会再次执行静态代码块。
调用规则:只有初次将类加载进内存时才会调用静态代码块static{},也就是类初始化的时候。
前面说的反射的时候initialize
参数设置为true的时候,如果该类没有被加载到内存中过,则会执行其static{}中的内容,这就是类初始化,但是这个类初始化和new的类实例化又有一些区别,大家可以根据debug过程理解一下
单例模式
在说单例模式钱我简单介绍一下几个方法。class.newInstance()
这个方法的是用来调用class类的无参构造函数。class.getMethod()
这个方法用来获取类中的public方法。class.getDeclaredMethods()
获取类中的所有方法(包括私有方法,不包括父类继承)。class.newInstance()
调用该类的无参构造函数,但前提是无参构造函数不能是私有的。class.getConstructor()
调用有参构造函数,但前提是有参构造函数不能是私有的。class.getDeclaredConstructor()
类比上面的Declared。Method.invoke()
执行方法。
好了说完这些我们接着说单例模式。
单例模式下,类的构造方法是私有的,也就是说只有再new的时候才能实例化这个对象,那么这个时候如果还想调用一次它的构造方法怎么办?使用newInstance
方法肯定是不行了,好在单例模式下类中一般都会提供一个public方法来实例化一个当前类。
我们用Java安全中一个重要且常见的类作为例子:Runtime
类。
可以看到在Java中Runtime
类的无参构造函数是私有的,这就导致了无法通过newInstance
调用,但是明明还有那么多RCE的POC啊,这是怎么回事?
那是因为Runtime
类中还为我们提供了一个public类getRuntime()
和一个声明private static Runtime currentRuntime = new Runtime();
所以说一般我们在调用Runtime
来执行命令的时候是这样写的:
Runtime runtime = Runtime.getRuntime();
为了能使用反射调用一个方法,这里说一下invoke
方法。
其实也没什么特别多好说的,需要注意的就是传参,这点和字节码比较像,静态方法不依赖实例,普通方法要带本身的类(实例化后的),这么说可能不太明了,直接看下代码吧。
需要注意的就是第一个参数obj,因为非静态方法要将类实例化,比如我实例化了一个对象,那么invoke调用的时候就是:
Class<?> clazz = MyClass.class;
Object object = clazz.newInstance();
Method method = clazz.getMethod("nonStaticMethod", parameterTypes);
method.invoke(object, args);
但是静态方法就不一样了,如果向调用一个静态方法那么就是:
Class<?> clazz = MyClass.class;
Method method = clazz.getMethod("staticMethod", parameterTypes);
method.invoke(null, args);
区别就在于null,如果你使用的是静态方法,那么就应该传入null,因为静态方法不依赖于对象的实例,而是与类本身相关联。
那么上面的Runtime换成反射调用就是如下代码,当然你也可以写成更简单的格式,这里是为了方便大家理解:
Class myClass = Class.forName("java.lang.Runtime");
Method myMethod = myClass.getMethod("exec", String.class);
Method getRuntimeMethod = myClass.getMethod("getRunt~~~~ime");
Object doRuntime = myMethod.invoke(myClass);
execMethod.invoke(doRuntime, "calc.exe");
除了这些还有一个特例,那就是既不是单例模式,也没有无参构造方法,并且有参构造方法可能是私有的。
那么对于有参非私有构造函数只需要使用getConstructor()
,有参私有构造函数使用getDeclaredConstructor()
。
但是切记使用getDeclaredConstructor()
方法后,一定要将获取到的Constructor类进行constructor.setAccessible(true)
处理,同样的对于class.getDeclaredMethods()
也需要做同样的处理,然后再使用getConstructor().newInstance()
调用newInstance()
进行实例化对象。
比如如下代码:
Class myClass = Class.forName("java.lang.Runtime");
Constructor constructor = myClass.getDeclaredConstructor();
constructor.setAccessible(true);
myClass.getMethod("exec", String.class).invoke(constructor.newInstance(), "calc.exe");
到此反序列化中的反射基本就说的差不多了。
结语
本文对反射做了一些讲解,目的是帮助师傅们回忆起可能很久没用到过的反射或者帮助有一些Java基础但是没有接触过反射的师傅们了解反射,个人认为在反序列化中对于反射的了解这么多就足够应对绝大部分场景了,如果想了解反射的更多详细细节以及底层原理可以看网上Java开发相关的文章。
对于反序列化漏洞其实很多时候都要用反射来调用一些链,所以不论是对老漏洞的理解(CC链等)还是对发掘新的链来说,反射都是必不可少的前置知识,同样的还有历史的CC链,远程调用(RMI),亦或是Java字节码等等,我会尽量将我所了解的知识以通俗易懂的方式讲出来,如有错误还请大佬们批评指正。
Thanks~
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
