Bài viết này phân tích về cơ chế Remote Method Invocation - RMI của Java: khái niệm, cách thức hoạt động và khả năng bị khai thác trong thực tế của các ứng dụng sử dụng RMI.
1. Khái niệm và cách thức hoạt động
Remote Method Invocation (RMI) là một cơ chế được sinh ra phục vụ cho việc xây dựng các hệ thống phân tán. RMI cho phép một đối tượng có thể gọi đến (invoke) các method của đối tượng chạy trên một JVM khác.
RMI cung cấp khả năng giao tiếp từ xa giữa các ứng dụng Client và Server sử dụng 2 đối tượng gọi là stub
và skeleton (skel)
.
-
Stub: Stub là đối tượng hoạt động như một gateway cho Client side, Toàn bộ outgoing request sẽ đi qua nó. Nó nằm phía client và đại diện cho remote object. Khi ta gọi tới phương thức của stub object, nó sẽ thực hiện các công việc sau theo thứ tự:
- Bắt đầu kết nối tới Remote Virtual Machine (JVM)
- Viết và truyền (marshal) các tham số cho JVM
- Đợi kết quả
- Đọc (unmarshal) return value hoặc exception
- Trả lại giá trị cho người gọi.
-
Skeleton: Skeleton là đối tượng hoạt động như một gateway cho Server side. Toàn bộ incoming request sẽ đi qua nó. Nó nằm phía server và khi skeleton nhận incoming request, nó sẽ thực hiện các công việc sau theo thứ tự:
- Đọc (unmarshal) các parameter cho các remote method
- Gọi đến các method thực sự của các remote method
- Viết và truyền (marshal) kết quả cho người gọi.
Mô hình hoạt động cơ bản của RMI có thể biểu thị như sau:
Trong đó Registry
là một namespace nơi Server lưu trữ các object, mỗi lần server tạo ra một object thì sẽ register object này với RMIregistry
(thông qua method bind()
hoặc reBind()
). Các objects được register sẽ có một tên riêng biệt hay còn gọi là bind name.
Để invoke một remote object, client cần một reference của object đó. Lúc đó, client sẽ lấy object từ RMI Registry sử dụng bind name của nó (thông qua method lookup()
).
Việc thực hiện viết và truyền (marshal) hay đọc và truyền (unmarshal) được mô tả:
Bất cứ khi nào một client gọi một method chấp nhận các param trên một remote object, các param đó sẽ được nhóm vào một thông báo trước khi được gửi qua mạng. Các tham số này có thể thuộc kiểu primitive hoặc object. Trong trường hợp kiểu primitive, các param được đặt cùng nhau và một header được gắn vào nó. Trong trường hợp các tham số là các object thì chúng được serialize. Quá trình này được gọi là marshalling. Ở phía server, các param đã đóng gói được tách ra và sau đó required method được gọi. Quá trình này được gọi là unmarshalling.
Điểm cần chú ý ở trên đó là nó sẽ thực hiện serialize và deserialize nếu param là object. Đây cũng là nơi diễn ra các lỗ hổng deserialization liên quan tới RMI.
Để dễ dàng hơn, ta sẽ đi vào ví dụ về khai thác RMI bằng một ví dụ cụ thể tự xây dựng.
2. Xây dựng RMI Server - Client đơn giản
Phần này mình sẽ thực hiện phân tích xây dựng một server RMI cơ bản, ai đã biết có thể bỏ qua và đi đến phần tiếp theo.
2.1. RMI Server
Để tạo ra những stub
và skel
, Java yêu cầu service cần tạo 1 interface extends từ Remote
interface, kiểu như
Interface này phải được biết tới từ cả phía client và server. Trong khi client có cơ chế autorenerated stub để hoạt động, server cần phải implement interface này, kiểu như
Để khiến implementation này có thể access thông qua internet, server phải đăng ký một service dưới 1 cái tên thông qua RMI Naming Registry, trong trường hợp này được gọi là TestRMIServer
. Hầu hết cái RMI service sử dụng port 1099. Mặc dù có thể sử dụng naming registry đã có sẵn nhưng server cũng có thể khởi tạo phiên bản riêng của nó. Việc khởi tạo hoàn tất bởi bind
hoặc rebind
method. Đây là ví dụ cho việc khởi tạo TestRMIServer
Sử dụng nmap quét port 1099, ta có thể thấy service TestRMIServer
đã khởi tạo thành công.
Theo mặc định, Java cũng sử dụng 1 random port cho RMI Service, khiến cho nó trở nên khó kiểm soát hơn đối với các admin cấu hình firewall. Nếu chúng ta thực hiện scan tới port này (như trong kết quả trên là port 51833
) thì kết quả cũng nhận được thông tin về RMI Server
2.2. RMI Client
Đây là một triển khai Client cơ bản
Như ta có thể thấy, client cũng cần 1 định nghĩa interface ServerSideRMIService
như trên để hoạt động. Trong thực tế, client cũng sẽ phải có thư viện chứa các class cần thiết để gửi lên server nếu server chấp nhận custom object.
Khi khởi chạy, server sẽ nhận được kết quả như sau:
3. Khai thác RMI Service
RMI cơ bản không có một triển khai xác thực nào theo mặc định mà nó thường dựa vào triển khai xác thực như login trong application. Điều này đã chuyển phần security cho phía client (attacker control).
Một attacker biết được interface của RMI Service có thể triển khai custom client và call trực tiếp tới các method. Như ví dụ nêu trên, không cần call tới register
mà có thể trực tiếp call sendHello
.
3.1. Khai thác RMI services sử dụng Java Deserialization (trước JEP 290)
Vì RMI service dựa trên Deserialization, nó có thể bị exploit nếu như có gadget trong classpath của service. Một researcher tên là M.Bechler đã hiện thực hóa nó vào những năm 2016 và tích hợp vào trong tool ysoserial
với 2 phương pháp khai thác:
RMIRegistryExploit
: Hoạt động bằng cách gửi một malicious serialized object thông qua param của phương thứcbind
của Naming registry.JRMPClient
: Khai thác mục tiêu DGC (Distributed Garbage Collection) - được implement bởi tất cả RMI listener. Nó có thể chạy với tất cả RMI listenr, không chỉ RMI naming service.
Cả 2 đều hoạt động nếu có gadget trong classpath. Với RMIRegistryExploit
, nó có một lợi thế là sẽ in ra exception là giá trị trả về từ server. Điều này giúp ích rất nhiều trong việc cho biết class nào tồn tại. (Tạm thời không demo được do mình đang cài bản java >= JDK 8u121, đã có JEP 290, cài lại khá lằng nhằng nên bỏ qua).
Với khai thác RMI trước JEP 290 khá đơn giản nên mình cũng không demo.
3.2. Khai thác RMI services sử dụng Java Deserialization sau JEP 290
Vì scope khai thác RMI quá lớn, để giải quyết nguy cơ của deserialization, Oracle đã thực hiện một số thay đổi trong Java core. Những cái quan trọng nhất đã được giới thiệu trong document số 290 của Java Enhancement Process (JEP), (gọi tắt là JEP 290). JEP là một phần của JDK9 nhưng đã được nhập vào các phiên bản Java cũ hơn (JDK >= 8u121, 7u131, 6u141).
JEP 290 giới thiệu cơ chế Look Ahead Deserialization bằng cách add nhiều serialization filter.
Khi ta thực hiện deserialization, bản chất là đọc 1 byte stream, class description sẽ được đọc trước giá trị thực của object. Giống như bức ảnh sau bao gồm 1 payload là object, chúng ta sẽ thấy có giá trị sun.reflect.annotation.AnnotationInvocationHandler
theo ngay sau magic bytes của object Java.
Điều này cho biết rằng đây là một object của sun.reflect.annotation.AnnotationInvocationHandler
, giả sử dạng cuối cùng của object là AnnotationInvocationHandler(something)
, sau đó, bên trong something
sẽ bao gồm các giá trị tùy chỉnh + 1 (hoặc 1 vài) Object khác, chính là object tiếp theo là java.util.Map
, và cứ như vậy đến hết.
Cấu trúc này cho phép chúng ta triển khai những thuật toán riêng để đọc class description và quyết định có đọc tiếp giá trị của Object hay không, tùy thuộc vào tên class. Điều này có thể được thực hiện dễ dàng nhờ một Java hook đã được định nghĩa sẵn là method resolveClass()
. Ta chỉ cần override lại hàm này trong một class extends từ ObjectInputStream
. Như trong richfaces
đã thực hiện nó như sau trong public class LookAheadObjectInputStream extends ObjectInputStream
:
Theo ví dụ trên kia chúng ta đã nói, giả sử class java.util.Map
bị cho vào blacklist, payload object vẫn sẽ được deserialize cho đến khi gặp java.util.Map
, tức là sun.reflect.annotation.AnnotationInvocationHandler
vẫn được phép, sau đó dần dần đến java.util.Map
thì mới throw ra exception not accepted.
Một khái niệm khác là Process-wide filters
- các bộ lọc áp dụng cho tất cả vị trí nào sử dụng ObjectInputStream
(trừ khi nó bị ghi đè trên một Stream cụ thể). Ta có thể add Process-wide filters bằng command line argument -Djdk.serialFilter=
hoặc setting như một system property trong $JAVA_HOME/conf/security/java.security
.
Dù vậy, việc setup whitelist class khá khó khăn cho dev khi không thể xác định được object nào nên được cho phép deserialize.
Custom filters
: Có thể override các process-wide filter
để đáp ứng nhu cầu khi deserialize, ví dụ
ObjectInputFilter onlyFilter = ObjectInputFilter.Config.createFilter("java.util.*;!*");
Lúc này chỉ có các class thuộc java.util.*
được phép deserialize (nếu cấu hình whitelist), ngược lại nếu là blacklist thì các class java.util.*
sẽ bị reject.
Build-in Filters
: JDK từ sau JEP 290 đã trực tiếp thêm các filter cho 2 gadget chain RMIRegistryExploit
và JRMPClient
, do vậy nó sẽ không thể khai thác được theo mặc định. Đây là exception khi thực hiện với RMIRegistryExploit
:
java -cp ysoserial-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 "calc.exe"
java.rmi.ServerException: RemoteException occurred in server thread; nested exception is: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: java.io.InvalidClassException: filter status: REJECTED at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:391) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) at java.base/java.security.AccessController.doPrivileged(Native Method) at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677) at java.base/java.security.AccessController.doPrivileged(Native Method) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834) at java.rmi/sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:303) at java.rmi/sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:279) at java.rmi/sun.rmi.server.UnicastRef.invoke(UnicastRef.java:380) at java.rmi/sun.rmi.registry.RegistryImpl_Stub.bind(RegistryImpl_Stub.java:73) at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:77) at ysoserial.exploit.RMIRegistryExploit$1.call(RMIRegistryExploit.java:71) at ysoserial.secmgr.ExecCheckingSecurityManager.callWrapped(ExecCheckingSecurityManager.java:72) at ysoserial.exploit.RMIRegistryExploit.exploit(RMIRegistryExploit.java:71) at ysoserial.exploit.RMIRegistryExploit.main(RMIRegistryExploit.java:65)
Caused by: java.rmi.UnmarshalException: error unmarshalling arguments; nested exception is: java.io.InvalidClassException: filter status: REJECTED at java.rmi/sun.rmi.registry.RegistryImpl_Skel.dispatch(RegistryImpl_Skel.java:94) at java.rmi/sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:468) at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:298) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:200) at java.rmi/sun.rmi.transport.Transport$1.run(Transport.java:197) at java.base/java.security.AccessController.doPrivileged(Native Method) at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:196) at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:562) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:796) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.lambda$run$0(TCPTransport.java:677) at java.base/java.security.AccessController.doPrivileged(Native Method) at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:676) at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128) at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628) at java.base/java.lang.Thread.run(Thread.java:834)
Mặc dù đã có CC 3.1 trong classpath, nhưng exception throw ra là java.io.InvalidClassException: filter status: REJECTED
. Lý do như đã nói, trong $JAVA_HOME/conf/security/java.security
đã triển khai 1 whitelist các class được sử dụng. Với các class này thì không thể tìm ra 1 valid gadget.
sun.rmi.registry.RegistryImpl#registryFilter
Tuy nhiên, researcher An Trịnh đã tìm ra một cách bypass JEP 290 mà mình sẽ đề cập ở phần 3 tại đây.
3.3. Khai thác RMI Service dựa trên ngữ cảnh của application
Vẫn có một cách khai thác không cần bypass JEP 290, đó là khi server cho phép attacker gửi lên một object bất kỳ trong một method được viết sẵn. Với trường hợp mình viết thì phương thức đó là ping(Object client)
.
Với object ở đây mình viết lại gadget CommonsBeanutils1
.
Lúc này JEP 290 không được áp dụng vì nó không được deserialization trong ngữ cảnh của RMI. Vậy tại sao server lại thực hiện deserialize object này? Đi sâu vào cách RMI Service thực hiện triển khai method, ta thấy khi RMI client invoke method trên server, method unmarshalValue
được gọi trong sun.rmi.server.UnicastServerRef.dispatch
, để đọc method argument. Method đó như sau:
Nếu type của argument không phải Primitive
hay nó chính là Object, thực hiện readObject
, do vậy chỉ cần method có type object, nó sẽ deserialize object đó bất kể điều kiện gì.
Viết về RMI vẫn còn rất nhiều điều nên mình sẽ break nó thành nhiều phần. Phần 1 cũng đã khá dài nên sẽ kết thúc tại đây.
Tài liệu tham khảo
https://medium.com/tradahacking/rmi-study-note-and-some-study-case-72bfd47275d9
https://book.hacktricks.xyz/network-services-pentesting/1099-pentesting-java-rmi