起因是学习java序列回显学习,不知道怎么就跑到Java RCE中类的反射获取&动态加载去了。因为对于反射的理解过于👎🐓,花了足足一天才搞清楚。
对于Class的获取方式有很多,常见的比如
getClass()
方法。Student s=new Student();
Class clazz=s.getClass();
Class clazz=Student.class;
forName()
静态方法,此种方法最安全、性能最好。Class clazz=Class.forName("类的全路径"); //推荐使用
ClassLoader classLoaDer = Sting.class.getClassLoader();
Class c4 = classLoaDer.loadClass("java.lang.String");
而在Java RCE中类的反射获取&动态加载中说重点关注的是 loadClass()
、forName()
和 defineClass()
。
而提到这几个函数往往便会顺带提及JVM的类加载机制。
类的加载有3个步骤
int=0
接下来就要说道说道loadClass()
、forName()
的区别。
loadClass(String name)
内部调用了Classloder.loadClass(String name, boolean resolve)
/**
Params:
name – The binary name of the class
Returns:
The resulting Class object
*/
public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
}
forName(String className)
内部则调用了Class.forName0(className, true, ClassLoader.getClassLoader(caller), caller)
/**
* @param className the fully qualified name of the desired class.
* @return the {@code Class} object for the class with the specified name.
*/
@CallerSensitive
public static Class<?> forName(String className)
throws ClassNotFoundException {
Class<?> caller = Reflection.getCallerClass();
return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}
2者最大的区别就是获取的class处于类加载中不同的阶段:
Class.forName
得到的class是已经初始化完成的,也就是说会执行static代码块
Classloder.loaderClass
得到的class是还没有链接的
ClassLoder.defineClass()
则很少提及,可却是ClassLoader的核心方法之一。
defineClass()
方法可以通过使用 .class文件的字节码数组得到class实例。
因为没有搞清楚class文件的字节码数组,在这里就卡了我几小时。
首先通过defineClass method throws java.lang.ClassFormatError: Incompatible magic value 1885430635 in class file和高赞回答提到的How do you dynamically compile and load external java classes? [duplicate]搞明白,是要读取.class文件
而不是最开始我理解的class对象
先写一个序列化测试对象SerObj
import java.io.Serializable;
public class SerObj implements Serializable {
private int one;
private int two;
public SerObj(int one, int two) {
this.one = one;
this.two = two;
}
@Override
public String toString() {
return "SerObj{" +
"one=" + one +
", two=" + two +
'}';
}
}
然后编译得到SerObj.class文件
得到后就是defineClass()
获取class实例的示例代码了
/*获取SerObj.class字节流*/
File dir = new File("./out/production/reflect/SerObj.class");
InputStream is = new FileInputStream(dir);
/*将字节流读取到classBytes字节数组中*/
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] classBytes = new byte[Integer.parseInt(new Long(dir.length()).toString())];
int bytesNumRead = 0;
while ((bytesNumRead = is.read(classBytes)) != -1){
baos.write(classBytes,0,bytesNumRead);
}
baos.close();
is.close();
/*反射获取defineClass方法*/
Method defineClassMethod =
ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
/*1.直接反射获得*/
Class cc = (Class) defineClassMethod.invoke(new ClassLoader(){}, classBytes, 0, classBytes.length);
/* 获取SerObj的(int,int)构造器*/
Constructor constructor = cc.getDeclaredConstructor(int.class,int.class);
Object obj = constructor.newInstance(1,2);
System.out.println(obj.toString());
到此ClassLoder.defineClass()
是结束了。
在学习的同时有额外的收获就顺便一起写在这篇博客了吧。
在查找如何获得 .class的字节数组时,看到Get bytecode from loaded class里提到Class.getResource()是最好的实现,顺便学习了一下使用。
根据文档,该方法的参数可以是一个目录也可以是一个URL链接,而他的返回值便是一个URL对象。而URL.openStream()返回一个读取了数据的InputStream
,这正是我想要的
URL url = Resources.class.getResource("/SerObj.class");
InputStream is = url.openStream();
剩下的部分就跟上面的一样了。
上面的东西都是因为此文Java RCE中类的反射获取&动态加载引发出来的,结果这篇文章的内容其实并不多。
主要是讲了几个使用defineClass()
获取class实例的方法。
Method defineClassMethod =
ClassLoader.class.getDeclaredMethod("defineClass",new Class[]{byte[].class, int.class, int.class});
defineClassMethod.setAccessible(true);
/*1.直接反射获得*/
Class cc =
(Class) defineClassMethod.invoke(new ClassLoader(){}, classBytes, 0, classBytes.length);
/*2.通过当前线程*/
Class cc =
(Class) defineClassMethod.invoke(Thread.currentThread().getContextClassLoader(),classBytes, 0, classBytes.length);
上述2种方法一个有趣的地方是在调用Method.invoke()
时ClassLoader总是重新new一个。这是因为类class的唯一性取决于定义该类的classloader,从浅谈Java中DefineClass方法以及ClassLoader可见。一个ClassLoder只会加载同一个class一次!
在Java中类的唯一性由类加载器和类本身决定,如果沿用当前上下文中的类加载器实例,而POC中使用同一个类名多次攻击,可能出现类重复定义异常,而每次new一个新的classloader就可以避免这个问题。
第三种方法提到的使用Unsafe类也非常的有趣。
Unsafe.defineClass()
是一个native方法。
/* 3.利用Unsafe获取*/
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe unsafe = (Unsafe) field.get(Unsafe.class);
Class cc = unsafe.defineClass("SerObj", classBytes, 0, classBytes.length, null, null);