Notes: RMI linh tinh
Last updated
Was this helpful?
Last updated
Was this helpful?
OVERVIEW
RMI (Remote Method Invocation). Một RMI application thì sẽ có server và client, hiểu nôm na RMI cho phép client gọi/thực thi một method của một object từ xa (object đã được bind vào registry).
Về bản chất, RMI runtime phía client mở 1 socket connection đến 1 port đã được mở lắng nghe bởi RMI runtime phía server.
Một bức ảnh minh họa khác chi tiết hơn về kiến trúc của RMI:
Ngoài Registry ra thì còn có DGC (Distributed Garbage Collection).
Registry sẽ nhận bind, rebind các name dẫn đến các method cần dùng hoặc lookup để gọi đến các method đã được bind từ trước.
Stub và Skel được xem như là 2 proxy của client-side và server-side.
RMI không hẳn là protocol, chính xác hơn nếu gọi RMI là interface để dev thuận tiện remote object trong một hệ thống phân tán. Thông thường RMI sử dụng giao thức JRMP kết hợp Java Serialization.
Client:
getRegistry()
tạo một TCP connection đến server
loopkup()
sẽ khởi tạo một newCall()
đến server và trả về StreamRemoteCall
(stream này là object đã được bind từ phía server) sau đó được write lại vào stream và cuối cùng là được readObject()
.
Nhìn chung, làm việc với byte stream và readObject()
sẽ là một sink mà các kiểu tấn công registry sẽ sử dụng được đề cập phía sau.
Server:
Dựa vào stacktrace sau đặt breakpoint để đón được request của client:
Từ Thread -> Transport sẽ đảm nhận việc bóc tách request ví dụ như protocol để get dispatch tương ứng hay call Service tương ứng.
Tại UnicastServerRef.dispatch()
sẽ đọc từ stream lên các method và unmarshallParameters()
sẽ deserialize những args kèm theo của method.
Dựa vào các rmi attack vector thì có thể phân ra làm 3 nhóm chính:
Known RMI interface: Dạng này đòi hỏi phải biết trước các services (methods + args) được bound ra ngoài bằng những cách brute force hoặc có source code trước. Và 1 attack vector đặc trưng của dạng này là khi method có arg truyền vào là object (hoặc là String nhưng cần sửa lại vài hexcode trong gói tin trước khi gửi đi).
RMI Registry/DGC: Tận dụng việc method bind() sẽ deser mọi object từ client mà không validate hoặc chèn malicious object trong lúc thực hiện dirty/clean thông qua JRMP đến DGC layer. Dạng này đã bị filter từ jdk 8u121 - JEP 290 có whitelist filter được đặt ở Registry layer hoặc DGC layer.
JRMI: Khởi tạo một JRMP listener đã được đặt payload của mình, chuyển server thành client rồi connect tới listener trước đó và gửi lại payload cho server deserialize.
Đi vào chi tiết để tìm hiểu và phân tích lại một số attack vector nổi bật.
Việc đầu tiên cần làm ở vector này là mình phải xác định được các endpoints (các object được bind, chỉ cần dùng list() để liệt kê) và xác định được các services (bao gồm method name, args, args type) bằng cách brute force hoặc bằng cách nào đấy đọc được source code.
Ví dụ như với souce code bsides client dưới đây:
Với việc dùng tool brute-force thì chỉ detect được register(String dummy). Nhưng service mà mình hướng tới là poke(Object dummy). Vì sao lại hướng tới các service chứa param type là object?
UnicastServerRef.dispatch()
thực hiện unmarshallParameters()
với var1 là remote, var8 là method đã được confirm map với method phía server, var38 là args đã được marshall phía trên.
Từng arg sẽ được unmarshall như sau:
var1.readObject sẽ deserialize malicious object của mình gửi lên.
Stack trace:
Attack vector này không phụ thuộc vào interface, service mà chỉ quan tâm đến jdk version đang dùng có install JEP290 hay chưa. Tóm tắt vector này là client thực hiện bind malicious object (Remote type) đến RMI server và rồi server deserialize object đó mà không validate.
Chain exploit:
Payload của mình sẽ được "ép kiểu" thành Remote rồi bind đến server.
Cụ thể cách để ép kiểu sang Remote là ysoserial đã dùng dynamic proxy, đặt payload vào attribute của một remote interface class. Thế nhưng có 1 cách trông đơn giản hơn nhưng hiệu quả tương đương:
Stack trace:
DGC layer sẽ luôn xuất hiện mỗi khiình khởi tạo 1 RMI service bởi vì y sẽ đảm nhiệm vai trò gửi "tín hiệu" duy trì việc sử dụng remote service. DGC exploit khác với Registry một số điều cơ bản sau:
DGC không có bind/rebind object mà là clean/dirty
Registry có method để client tương tác trực tiếp đến server còn DGC thì không.
Chain Registry có thể nhúng payload vào bind/rebind để gửi lên server, với DGC thì phía client phải can thiệp ở low-level để nhúng được payload và gửi đi.
Cả branch clean hay dirty đều có thể là sink vì đều deser object của mình
Chain Exploit:
Lượm lặt được 1 bức hình giải thích rất chi tiết từng đoạn split và write value của chain trên:
Cuối cùng, DGC Exploit này tương tự với Registry Exploit đều bị filter từ jdk 8u121.
Những JDK 6u141, JDK 7u131, JDK 8u121 đã có sự xuất hiện của JEP290 và filtering policy.
Whitelist:
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
Hạn chế vẫn còn tồn tại ở JEP290: Filter đặt ở các Registry hoặc DGC, mà deserialize xuất hiện gần như mọi nơi trong quá trình giao tiếp giữa RMI client và RMI server. Attack vector Object param vẫn chưa được khắc phục. Hơn thế là có thể tận dụng whitelist trên để tạo chain bypass.
Ý tưởng cơ bản của chain này:
Tạo JRMP Listener (JRMP server) được nhúng payload trigger command
Phía client bind/rebind chứa payload turn server thành JRMP client đến RMI registry
JRMP client gửi request đến JRMP Listener
JRMP Listener gửi lại server payload trigger command
Như hình trên thì có thể thấy giao tiếp JRMP sẽ không bị filter nên payload của mình thành công được trigger.
Để khởi tạo 1 JRMP listener thì mình có thể dùng chain trên ysoserial:
Câu hỏi đặt ra là làm sao để turn RMI registry thành JRMP client gửi request đến JRMP listener?
Nói đơn giản, sử dụng UnicastRef có trong whitelist để server không bị filter khi deser. UnicastRef wrap LiveRef object connect tới một TCP Endpoint (ip:port của JRMP listener). Sau đó đóng gói kết quả trên vào Remote object để có thể bind đến server. Khi server nhận được sẽ deser mà thực hiện connect đến endpoint mình chỉ định trên.
Có thể sử dụng dynamic proxy để change type Remote sang Registry (tương đương).
Với điều kiện môi trường không có restricted network, không outbound mà mình muốn đọc output của command thì đây là lúc chain của anh @tint0 thể hiện mức impact của mình.
Chain này khác với kiểu bypass phía trên là việc đọc return output bằng error, phía trên sẽ bị catch lại tại DGCClient.makeDirtyCall()
mà không tiếp tục throw.
Phía ngoài dispatch catch được exception và serialize và gửi lại cho client
...continue...
<custom payload to eval code>
Dựng lại source code và debug để xem một tí về cách client (lookup) vào server (nhận data) hoạt động như nào. (JDK 8u112)
Từ JDK 8u121, có với sự xuất hiện registryFilter()
khi readObject() nhầm kiểm tra 1 số ràng buộc và whitelist nên phương thức tấn công trên không còn khả thi.
Giải thích có trong:
Tại bài viết có giải thích rất kỹ về chain này, tóm tắt là sử dụng UnicastRemoteObject (extends Remote nên vẫn trong whitelist). Server sẽ deser object này và gọi reexport()
rồi đến exportObject()
. Vì dyamic proxy sang RemoteObjectInvocationHandler nên method cũng được chuyển qua invokeRemoteMethod()
, tại đây exception được catch rồi throw ra ngoài.