+1

Java RMI Phần 3 - Bypass JEP 290 để khai thác Deserialization

Trong phần 1phần 2 mình đã đi qua cách RMI hoạt động và một số phương pháp khai thác nó, bạn đọc quan tâm có thể tìm hiểu thêm.

Chi tiết về JEP 290 cũng đã được mình phân tích ở bài 1. Nói thêm về JEP 290, nó không chặn được cách tấn công khi có 1 method có parameter Object, chỉ whitelist filter class của quá trình deserialization diễn ra trong RMI/DGC.

Trong phần 3, mình sẽ trình bày phương pháp bypass whitelist nói trên. Phương pháp này của tác giả An Trịnh. Bạn đọc có thể tham khảo slide tại đây.

1. Concept về cách bypass

Khi attacker kết nối tới RMI Server, nó sẽ đi qua một lớp Deserlization filter, nhưng nếu attacker đóng vai trò là server và biến RMI Server thành client, filter đó sẽ không được áp dụng. Phải nói thêm là filter này có áp dụng với RMI và DGC nhưng không áp dụng với JRMP.

Ý tưởng chính của cách bypass này: Chuyển server-side call thành client-side call.

image.png

Quá trình JRMP Server (JRMPListener) khai thác JRMP Client (RMI Registry) như sau:

  • Phần cuối RMI Server phải hoạt động như một JRMP Client để chủ động kết nối tới JRMP Listener (Filter chỉ áp dụng cho quá trình deserialization chứ không áp dụng cho serialization)
  • JRMP Listener tạo 1 gadget, serialize sau đó gửi lại cho JRMP Client (RMI Registry)
  • Vì không có JEP 290 trong quá trình deserialize của JRMP, payload được tải và RCE

Điều còn thiếu duy nhất là làm thế nào để biến một RMI Server thành JRMP Client. Điều này tương đương với việc tìm 1 gadget với kết quả cuối cùng là tạo kết nối JRMP tới JRMP Listener, mà các class trong gadget đó đều trong whitlist.

Nếu muốn tìm gadget này, trước tiên ta nên tìm hiểu tất cả các phương thức bắt đầu JRMP request tới máy chủ, sau đó tìm kiếm nơi phương thức này được gọi để thực hiện đảo ngược từng lớp cho đến khi chúng ta tìm thấy nơi thực hiện deserialize.

2. Gadget chain sử dụng

An Trịnh đã tổng hợp gadget này như sau:

image.png

Điểm bắt đầu nằm ở dòng cuối java.rmi.server.UnicastRemoteObject#readObject

image.png

java.rmi.server.UnicastRemoteObject#reexport

image.png

Phương thức này thực hiện check csfssf, sau đó gọi exportObject. Trong đó csfssf

image.png

Cả RMIClientSocketFactoryRMIServerSocketFactory đều là 2 interface, cho phép developer tạo kết nối RMI/JRMP. Oracle có 1 ví dụ khá chi tiết sử dụng 2 interface này tại đây.

Trong đó RMIServerSocketFactory có 1 phương thức là createServerSocket, ghi nhớ phương thức này vì lát nữa sẽ đi qua nó.

image.png

Chúng ta cần chuyển một UnicastRemoteObject cho RMI Registry. Object này phải chứa một số instance của RMIServerSockerFactory (trong thuộc tính ssf). Tuy nhiên constructor của UnicastRemoteObject là protected và ssf là private nên ta phải làm điều đó thông qua reflection.

// 1. Tạo constructor và set nó thành public
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);

// 2. Tạo 1 instance của UnicastRemoteObject
UnicastRemoteObject myRemoteObject = (UnicastRemoteObject) constructor.newInstance(null);
        
// 3. Lấy reference của ssf và biến nó thành public
Field privateSsfField = UnicastRemoteObject.class.getDeclaredField("ssf");
privateSsfField.setAccessible(true);

// 4. Tạo ssf object với kiểu là UnicastRemoteObject
privateSsfField.set(myRemoteObject, handcraftedSSF);

Tiếp theo là sun.rmi.transport.tcp.TCPTransport#listen

image.png

Gọi tới sun.rmi.transport.tcp.TCPEndpoint#newServerSocket

image.png

Và gọi tới createServerSocket đã nói ở trên

Trong gadget, An Trịnh có sử dụng Proxy tới RemoteObjectInvocationHandler. Vậy cái proxy này là gì.

Một Proxy cho phép chúng ta tạo một trung gian hoạt động như một interface với tài nguyên khác. Hãy nhớ rằng interface RMIServerSocketFactory có duy nhất 1 phương thức createServerSocket nên để implement nó chỉ cần viết lại phương thức này. Giả sử chúng ta đã có 1 class implement nó hoạt động hiệu quả, được gọi là EncryptedRMIServerSocketFactory, sử dụng secure connection. Tuy nhiên trong quá trình hoạt động, chúng ta nhận thấy nó sử dụng default key. Ta cần 1 cách thức để check xem default key này thay đổi hay chưa, đó là tác dụng của proxy. Vì proxy cũng là implementation của RMIServerSocketFactory nên ta có thể viết lại hàm createServerSocket như sau:

public class EncryptedRMIServerSocketProxy implements RMIServerSocketFactory {

    private EncryptedRMIServerSocket myServerSocket;

    // Constructor lấy EncryptedRMIServerSocket làm argument
    public EncryptedRMIServerSocketProxy(EncryptedRMIServerSocket serverSocket) {
        this.myServerSocket = serverSocket;
    }

    public ServerSocket createServerSocket(int port) throws IOException {

        // Check default key
        if (myServerSocket.key == myServerSocket.defaultKey) {
            throw new IOException("Usage of default key is not allowed");
        }

        // gọi tới method của EncryptedRMIServerSocketProxy
        return myServerSocket.createServerSocket(port);
    }
}

Tạo những proxy như vậy yêu cầu viết lại code rất nhiều, tuy nhiên trong Java Reflection đã cung cấp sẵn các Dynamic Proxy class. Dynamic Proxy là class implement các interface cụ thể trong runtime sao cho một method call thông qua một trong các interface trên một implementation của class sẽ được mã hóa và gửi đến một object khác thông qua một interface thống nhất.

Một dynamic proxy chuyển tiếp toàn bộ incoming call tới method invoke của Invocation handler, chuyển tên và toàn bộ argument của phương thức được gọi ấy. Invocation handler sau đó chuyển nó tới shieled object. Điều này được mô tả khá chi tiết trong document.

Đối với Java RMI, điều này cũng diễn ra tương tự. Khi client query tới RMI Registry để tìm kiếm 1 remote object, thực tế client đó sẽ nhận được một dynamic proxy class mà implement interface của object đó. RMI Invocation handler sau đó sẽ forwall message call tới object trên remote server.

Dynamic proxy cũng rất hữu hiệu khi tạo gadget vì nó cho phép chúng ta redirect cuộc gọi từ một interface bất kỳ tới invoke method của invocation handler.

Để tạo proxy, chúng ta sẽ sử dụng method Proxy.newProxyInstance().

image.png

Method này cần 3 argument: 1 ClassLoader để load dynamic proxy, 1 mảng các interface (trong trường hợp này chúng ta chỉ cần RMIServerSocketFactory) và một InvocationHandler để forward các method call từ proxy tới.

RMIServerSocketFactory handcraftedSSF = (RMIServerSocketFactory) Proxy.newProxyInstance( RMIServerSocketFactory.class.getClassLoader(), new Class[] { RMIServerSocketFactory.class }, myInvocationHandler);

InvocationHandler này là RemoteObjectInvocationHandler vì nó vừa là một invocation handler, vừa extends từ RemoteObject nằm trong whitelist.

image.png

Method invoke của nó thực hiện forward method call từ client tới object thực trên server

image.png

Nó gọi tới java.rmi.server.RemoteObjectInvocationHandler#invokeObjectMethod

image.png

Trong hàm này nó tạo 1 JRMP connection thông qua hàm ref.invoke. Object ref bao gồm IP, port của server (lấy từ class TCPEndpoint) và là một instance của RemoteRef interface. Và chúng ta sẽ sử dụng UnicastRef là instance của RemoteRef. Nó cũng là class thực hiện RMI/JRMP, nên cuối cùng một JRMP connection sẽ được tạo ra.

Lưu ý rằng method kiểm tra xem proxy có phải là một instance của lớp Remote hay không. Do đó ta phải extend object proxy của mình để đảm bảo rằng điều kiện này được đáp ứng.

Tạo object RemoteRef này bằng 1 đoạn code đơn giản sau (lấy từ gadget JRMP Client)

image.png

3. Chuyển gadget vào RMI

Chúng ta đã có gadget, nhưng không thể truyền trực tiếp thông qua method bind vì nó (hay chính xác hơn là object ObjectOutput trong đó) sẽ thay thế object của chúng ta thành proxy object như đã đề cập ở trên, do vậy gadget thực sự sẽ không được gửi tới server. Trong java.io.ObjectOutputStream#writeObject0 ta thấy enableReplace= true (mặc định) sẽ thực hiẹn replace object. Hành vi này được quyết định bởi property enableReplace và phải được set thành false.

image.png

Chúng ta thực hiện điều đó bằng reflection. Mã như sau:

StreamRemoteCall call = (StreamRemoteCall) ref.newCall(this, operations, 0, interfaceHash);
java.io.ObjectOutput out = call.getOutputStream();
ReflectionHelper.setFieldValue(out, "enableReplace", false);
out.writeObject(ourObject);

Phải nói thêm là gadget này có thể giúp attacker nhận được return output command. Bởi vì trong quá trình thực hiện deserialize, nó đã nhảy vào hàm java.rmi.server.RemoteObjectInvocationHandler#invokeRemoteMethod nói trên và trong method này nó có throw ra exception, và ở trong sun.rmi.server.UnicastServerRef#dispatch

image.png

Exeption đã được serialize và gửi về phía server. Vì thế ta hoàn toàn có thể lợi dụng một số phương pháp như ScriptEngineManager để catch được output command.

Tài liệu tham khảo


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí