获取类:forName
用法:Class.forName(String className)
返回 Class<?>
类型
从对象获取类:getClass
用法:Object.getClass()
返回 Class<?>
类型
Object
应为上下文中已经实例化的类的对象。
实例化类对象:newInstance
用法:Class<?>.newInstance()
实质上这个方法的作用是调用这个类的无参构造函数,当这个类没有无参构造方法或者无参构造方法为私有属性的时候,无法通过newInstance
来获得这个类的对象,java.lang.Runtime
就是一个典型案例,因为Runtime
类的构造方法是私有的。
返回Object
类型,即返回Class<?>
的一个对象
获取函数:getMethod
用法:Object.getMethod(String name, class<?>...parameterType)
第一个参数代表方法名,第二个参数表示该方法参数类型(可缺省),不过Java中支持类的重载,一个同名函数往往会由于参数列表的不同而具有不同的效果,因而无法仅通过一个函数名来确定一个函数,因此第二个参数也会用来获取指定的一个方法。
返回Method
类型
执行函数:invoke
用法:Method.invoke(Object obj, Object ... args)
这个方法一般要与getMethod
结合使用,一般是先用前者获取到一个Method
类,再用invoke
调用。
当调用的是一个普通方法时,第一个参数代表类的一个对象;当调用的是一个静态方法时,第一个参数代表类,第二个参数代表方法的参数。
可以这么理解:正常执行一个方法时,我们通过A.method(B,C,D,...)
来执行;在反射中,我们则是通过method.invoke(A,B,C,D,...)
去执行,普通方法的A需要时该类的一个对象,而静态方法的A则是类。
Runtime
类是我们在Java安全中较为常见用于实现RCE的一个类,那么如何通过这个类来执行命令呢?刚才提到过java.lang.Runtime
类的无参构造方法是私有的,我们无法通过newInstance
来获取Runtime
类的对象。
之所以要将类的某个构造方法设置为私有的,这是由于单例模式的开发模式,通俗地来解释就是在某个类的构造函数只需要实例化调用一次时会如此设计,后续如果需要调用这个类的对象则需要通过调用这个类的某个特定静态方法来获得,这样的设计可以避免某个类的构造函数被多次重复的调用,最典型的例子就是创建数据库连接。
那么Runtime
所提供的静态方法就是getRuntime
,通过调用getRuntime
我们可以获取到Runtime
类的一个对象。
例如:
package org.example;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
// 等价于 Runtime myRuntime = Runtime.getRuntime();
Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke(null);
// 等价于 myRuntime.exec("open /System/Applications/Calculator.app");
// Runtime的exec方法有6个重载,这里的getMethod方法的第二个参数指定了获取参数为String的exec方法
Method exec = Class.forName("java.lang.Runtime").getMethod("exec", String.class);
exec.invoke(myRuntime,"open /System/Applications/Calculator.app");
}
}
序列化:使用ObjectOutputStream
类的writeObject
函数
public final void writeObject(Object x) throws IOException
反序列化:使用ObjectInputStream
类的readObject
函数
public final Object readObject() throws IOException, ClassNotFoundException
支持序列化的对象必须满足:
java.io.Serializable
接口transient
,使得该属性将不会被序列化DOME:
package org.example;
import java.io.*;
class Employee implements Serializable{
private String name;
private String address;
// number成员声明了transient属性无法反序列化
private transient int number;
public Employee(String name, String address, int number){
this.name = name;
this.address = address;
this.number = number;
} public void info(){
System.out.println("I am " + name);
System.out.println("Number is " + number);
}
// Override readObject
private void readObject(ObjectInputStream in) throws Exception {
// 调用默认的readObject方法,使其能正常反序列化
in.defaultReadObject();
// 重载的readObject方法多了一行输出
System.out.println("Employee call readObject Function");
}
}
public class Main {
public static void main(String[] args) {
Employee e = new Employee("Reyan Ali","Phokka Kuan, Ambehta Peer",123);
try {
// Serialize
// 创建一个文件输出流 -> 用于写入文件
FileOutputStream fileOut = new FileOutputStream("./employee.ser");
// 创建一个对象输出流 并且输出定向到文件中
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// writeObject 序列化
out.writeObject(e);
out.close();
fileOut.close();
System.out.println("Serialized");
// Deserialize
// 创建文件输入流 -> 用于读取文件
FileInputStream fileIn = new FileInputStream("./employee.ser");
// 创建一个对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
// readObject 反序列化
Employee obj = (Employee) in.readObject();
in.close();
fileIn.close();
obj.info();
System.out.println("Deserialized");
} catch(IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
}
}
}
运行结果:
Number`成员由于声明了`transient`属性没有反序列化,所以这里输出的值为`0
除了常见的ObjectInputStream.readObject
可以触发反序列化操作外,还有以下几种触发方式:
ObjectInputStream.readObject // 流转化为Object
ObjectInputStream.readUnshared // 流转化为Object
XMLDecoder.readObject // 读取xml转化为Object -> XML反序列化
Yaml.load // yaml字符串转Object -> yaml反序列化
XStream.fromXML // XStream用于Java Object与xml相互转化
ObjectMapper.readValue // jackson中的api -> jackson反序列化漏洞
JSON.parseObject // fastjson中的api -> fastjson反序列化漏洞
readUnshared
方法读取对象,不允许后续的readObject
和readUnshared
调用引用这次调用反序列化得到的对象,而readObject
读取的对象可以。
反序列化的过程主要分为两步:
跟进ObjectInputStream.readObject
方法:
跟进readObject方法:
主要的反序列化代码是调用的readObject0
方法:
Object obj = readObject0(type, false);
在readObject0
里有一个switch
判断:
这里的115
所指向的是TC_OBJECT
,代表反序列化的对象是一个Object
:
跟进readOrdinaryObject
:
这里的readClassDesc(false)
作用是从序列化流中提取出相关的类信息:
跟进这里的readNonProxyDesc()
方法:
这里的关键点在于resolveClass()
方法,这个方法实现了利用反射机制获取到类的Class对象:
注意这里利用反射中的getName和
forName`最终获取到了类的Class对象:
此处整一个反射就是先通过Class.forName
获取到当前描述器所指代的类的Class对象,后续会在initNonProxy
或initProxy
函数中复制该Class对象的相关信息(包括相关函数),最后在2044行处ObjectStreamClass.newInstance
实例化该对象:
获得对象之后会在2213行的readSerialData()
函数将序列化流中的相关数据填充进实例化后的对象中或调用当前类描述器的readObject函数:
readSerialData()
具体实现如下:
这里的hasReadObjectMethod
用于判断该类是否有自己的readObject
方法。
然后可以跟进invokeReadObject
方法,更深入的看到是如何设置对象的字段值的,这里忽略掉中间调用,最终是在defaultReadFields
中的setObjFieldValues
方法实现的:
Java程序中类ObjectInputStream
的readObject
方法被用来将数据流反序列化为对象,如果流中的对象是class
,则它的ObjectStreamClass
描述符会被读取,并返回相应的class对象,ObjectStreamClass
包含了类的名称及serialVersionUID
。
如果类描述符是动态代理类,则调用resolveProxyClass
方法来获取本地类。如果不是动态代理类则调用resolveClass
方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException
异常。
如果反序列化对象不是String、array、enum类型,ObjectStreamClass
包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable
或者externalizable
接口,则抛出InvalidClassException
异常。因为只有实现了Serializable
或Externalizable
接口的类的对象才能被序列化。
前面分析中提到最后会调用resolveClass
获取类的Class对象,这是反序列化过程中一个重要的地方,也是必经之路,所以有研究人员提出通过重载ObjectInputStream
的resolveClass
来限制可以被反序列化的类。