Trong phần 1 và phầ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.
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:
Điểm bắt đầu nằm ở dòng cuối java.rmi.server.UnicastRemoteObject#readObject
java.rmi.server.UnicastRemoteObject#reexport
Phương thức này thực hiện check csf
và ssf
, sau đó gọi exportObject
. Trong đó csf
và ssf
là
Cả RMIClientSocketFactory
và RMIServerSocketFactory
đề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ó.
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
Gọi tới sun.rmi.transport.tcp.TCPEndpoint#newServerSocket
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()
.
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);
Và InvocationHandler
này là RemoteObjectInvocationHandler
vì nó vừa là một invocation handler, vừa extends từ RemoteObject
nằm trong whitelist.
Method invoke
của nó thực hiện forward method call từ client tới object thực trên server
Nó gọi tới java.rmi.server.RemoteObjectInvocationHandler#invokeObjectMethod
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)
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
.
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
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.