- vừa được xem lúc

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

0 0 11

Người đăng: Real Alpha Man

Theo Viblo Asia

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.

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 csfssf, sau đó gọi exportObject. Trong đó csfssf

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ó.

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);

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.

Tài liệu tham khảo

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tổng hợp các bài hướng dẫn về Design Pattern - 23 mẫu cơ bản của GoF

Link bài viết gốc: https://gpcoder.com/4164-gioi-thieu-design-patterns/. Design Patterns là gì. Design Patterns không phải là ngôn ngữ cụ thể nào cả.

0 0 277

- vừa được xem lúc

Học Spring Boot bắt đầu từ đâu?

1. Giới thiệu Spring Boot. 1.1.

0 0 257

- vừa được xem lúc

Cần chuẩn bị gì để bắt đầu học Java

Cần chuẩn bị những gì để bắt đầu lập trình Java. 1.1. Cài JDK hay JRE.

0 0 37

- vừa được xem lúc

Sử dụng ModelMapper trong Spring Boot

Bài hôm nay sẽ là cách sử dụng thư viện ModelMapper để mapping qua lại giữa các object trong Spring nhé. Trang chủ của ModelMapper đây http://modelmapper.org/, đọc rất dễ hiểu dành cho các bạn muốn tìm hiểu sâu hơn. 1.

0 0 180

- vừa được xem lúc

[Java] 1 vài tip nhỏ khi sử dụng String hoặc Collection part 1

. Hello các bạn, hôm nay mình sẽ chia sẻ về mẹo check String null hay full space một cách tiện lợi. Mình sẽ sử dụng thư viện Lớp StringUtils download file jar để import vào thư viện tại (link).

0 0 55

- vừa được xem lúc

Deep Learning với Java - Tại sao không?

Muốn tìm hiểu về Machine Learning / Deep Learning nhưng với background là Java thì sẽ như thế nào và bắt đầu từ đâu? Để tìm được câu trả lời, hãy đọc bài viết này - có thể kỹ năng Java vốn có sẽ giúp bạn có những chuyến phiêu lưu thú vị. DJL là tên viết tắt của Deep Java Library - một thư viện mã ng

0 0 124