概念

Java Remote Method Protocol,Java远程方法协议。

RMI依赖的通信协议为JRMP,该协议用于查找和引用远程对象,是运行在RMI之下、TCP/IP之上的线路层协议。

一个RMI的过程,需要用到JRMP这个协议去组织数据格式然后通过TCP进行传输、从而达到调用远程方法的目的。

当服务端与客户端之间通过socket建立连接,其中通信的协议就是通过JRMP协议格式来进行通信的

JRMP接口的两种常见实现方法:

  • JRMP协议(Java Remote Message Protocol):RMI专用的Java远程消息交换协议
  • IIOP协议(Internet Inter-ORB Protocol),基于CORBA实现的对象请求代理协议

DGCImpl_Stub和DGCImpl_Skel

前面我们在利用RMI攻击时都是围绕着RegistryImpl_Stub和RegistryImpl_Skel之间来讲的

而其中还有一种JRMP的攻击没有进行讲解,前面我们在将DGC分布式垃圾回收的时候也讲过,在执行RegistryImpl_Stub.lookup方法中,在接受服务端的返回值后会通过done的后续调用创建DGCImpl_Stub,并调用DGCImpl_Stub.dirty方法,该方法中同样会调用invoke进行传输然后将返回内容进行反序列化。

而处理请求对应于DGCImpl_Skel.dispatch方法,当DGC调用完DGCImpl_Stub.dirty方法,DGCImpl_Skel.dispatch会处理这个方法的请求,其中同样存在反序列化,还原lease对象

image-20250714103238259

image-20250714103250423

0对应处理clean请求,1对应处理dirty请求

ysoserial程序分析

在ysoserial中有exploit模块和payload模块,每个模块下都有JRMPClient和JRMPListener的脚本,对应着两种攻击方式

payloads/JRMPListener+exploit/JRMPClient

第一种方法是基于RMI反序列化的客户端打服务器类型。通过将一个payload(JRMPListener)发送到存在漏洞的服务器,存在漏洞的服务器反序列化该payload(JRMPListener)后在指定端口开启RMI监听,然后再通过exploit(JRMPClient)去发送利用链载荷,最终在存在漏洞的服务器上进行反序列化操作实现攻击。

我们从代码的角度上来分析,先看ysoserial.payloads.JRMPListener的利用链:

image-20250714112159717

跟过远程对象创建的师傅们应该都比较熟悉这个链子,链子后面不就是创建远程对象的部分过程吗,只不过是通过UnicastRemoteObject.readObject这个反序列化入口来进行的:

image-20250714113032025

调用UnicastRemoteObject.reexport()方法:

image-20250714113252896

接着调用UnicastRemoteObject.exportObject()方法,后面就是创建远程对象时的流程了,后面将封装好的target对象通过exportObject发布出去,其中会调用listen()方法创建socket并开启监听等待连接

看一下ysoserial的操作,先看一下继承链:

img

在ysoserial中用到了UnicastRemoteObject的子类ActivationGroupImpl

image-20250714170826255

调用了Reflections.createWithConstructor方法,这是自定义的方法,有四个参数,跟进下:
image-20250714171049034

逻辑大概就是:

1
2
3
4
5
6
7
8
9
10
int port = 1099;
Constructor uro = RemoteObject.class.getDeclaredConstructor(RemoteRef.class);
uro.setAccessible(true);
Constructor sc= ReflectionFactory.getReflectionFactory().newConstructorForSerialization(ActivationGroupImpl.class,uro);
sc.setAccessible(true);
UnicastRemoteObject u = (UnicastRemoteObject) sc.newInstance(new UnicastServerRef(port));

Field field=UnicastRemoteObject.class.getDeclaredField("port");
field.setAccessible(true);
field.set(u,port);

先获取RemoteObject的私有构造器,参数为RemoteRef对象:

image-20250714173353292

然后调用ReflectionFactory.getReflectionFactory()获取ReflectionFactory对象,再调用newConstructorForSerialization方法来获取构造方法,这里将ActivationGroupImpl的序列化构造行为劫持到RemoteObject的构造器,这样能绕过JVM对构造器的安全检测,同时获取到ActivationGroupImpl对象后,向上转型就能获取到UnicastRemoteObject,避免了直接实例化UnicastRemoteObject对象直接触发监听。

之所以选择ActivationGroupImpl是因为在继承链中,ActivationGroupImpl 是 RMI 中唯一同时满足

  • 非抽象类
  • 继承自 UnicastRemoteObject
  • 在标准JDK中预加载(避免 ClassNotFoundException)
  • 实现 Serializable 接口

image-20250714173647058

image-20250714174016127

对这个构造方法调用newInstance(new UnicastServerRef(port)方法来实例化远程对象,传入了UnicastServerRef

image-20250714221933974

当反序列化UnicastRemoteObject对象字节流,就会触发它的构造方法,从而在指点端口开启监听

当正常对UnicastRemoteObject反序列化,会发现端口并不是指定的,而是一个随机端口,所以需要通过反射指定端口

最后返回UnicastRemoteObject对象

此时将payload/JRMPListener注入到服务器后就已经开启了RMI服务,我们就可以通过exploit/JRMPClient发送gadgets来进行利用了(前提对方存在可以利用的gadgets),先看ysoserial:

image-20250714222706451

意思是其攻击手法大致与 {@link RMIRegistryExploit} 相同,只不过:

  • 攻击目标 是远程 DGC(分布式垃圾回收服务——只要存在远程对象监听,该服务必然存在
  • 不执行反序列化操作(避免自身被反制 )

在RMIRegistryExploit中我们主要目标是攻击rmi的Registry模块,这里是攻击DGC,且该操作不执行反序列化操作,它Client全都是向server发送数据,没有接受过任何来自server端的数据,这样自己就不会被反制了。

前面在讲DGC时也说过,当为RMI注册端口时,TableObject就存在有DGC了:

1
LocateRegistry.createRegistry(PORT);

在ysoserial中,exploit/JRMPClient调用了makeDGCCall:

image-20250714224004009

主要是为了调用dirty方法触发反序列化,传递一个用于反序列化的对象导致命令执行

image-20250714234955660

而这种客户端打服务端的方式虽然也是二次反序列化,但比较鸡肋,因为本身就是一个反序列化的点,结果还需要再去开个rmi服务,然后再次进行攻击,这就显得没必要,且后面存在jep290的限制,但这个二次反序列化可以起到绕过黑名单的效果

payloads/JRMPClient+exploit/JRMPListener

这种就类似于服务端打客户端类型,同样也是二次反序列化,也有绕过黑名单的作用。且这种服务端打客户端的类型比客户端打服务端的类型更加常用,它一方面能外连,另一方面能绕过jep290的限制。

先看payloads/JRMPClient:

image-20250715093200802

image-20250715094225114

反序列化UnicastRef类

UnicastRef实现了RemoteRef接口,RemoteRef接口又实现了Externalizable接口,Externalizable接口又实现Serializable

Externalizable接口定义了writeExternal和readExternal方法,用于实现序列化和反序列化

image-20250715094755303

UnicastRef.readExternl方法:

image-20250715094828709

调用对序列化数据流调用LiveRef.read方法:

image-20250715095020321

useNewFormat为false,会调用TCPEndpoint.readHostPortFormat(in)方法:

image-20250715095255973

通过输入流来获取host和port,返回一个封装了host和port的TCPEndpoint对象

image-20250715095503834

然后创建一个LiveRef对象将ObjId、host、port等信息封装进去

而如果我们控制输入流不为ConnectionInputStream类,那么就会调用DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));方法:

image-20250715095838748

首先会执行一次EndpointEntry.lookup(ep)方法,返回EndpointEntry对象,然后会调用EndpointEntry.registerRefs方法:

image-20250715100226147

EndpointEntry.registerRefs最后会调用一次makeDirtyCall方法,跟进:

image-20250715100357536

可以看到会调用dirty方法,实际上是调用DGCImpl_Stub.dirty方法,这个方法下调用newCall方法建立连接,还会对remoteCall进行一次反序列化

在注册远程对象时,利用RemoteObjectInvocationHandler来为UnicastRef创建动态代理,这个过程类似于RMI创建远程对象的部分:

1
2
3
4
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);

还需要一个UnicastRef对象:
image-20250715102940937

需要LiveRef对象对host、port、ObjId等远程标识对象进行封装:

image-20250715103300367

所以:

1
2
3
ObjID objID = new ObjID(new Random().nextInt());
Endpoint endpoint = new Endpoint(host,port);
UnicastRef u=new UnicastRef(new LiveRef(objID, endpoint, false));

所以payloads/JRMPClient主要功能就是生成一个向指定攻击机IP和端口发起RMI通信。

再看exploit/JRMPListener:

就是一个通用JRMP侦听器,大致逻辑就是打开一个JRMP侦听器,该侦听器会将指定的有效负载传递给连接到它并进行调用的任何客户端。

ysoserial中,获取一个用于反序列化对象:

image-20250715104532368

这样当客户端向exploit/JRMPListener进行连接时,就会返回一个序列化对象,客户端接受对象后会进行反序列化等操作,而这个恶意对象就是这里payloadObject

所以这个攻击手法整体的流程就是通过在受害机找到反序列化注入点,然后将payloads/JRMPClient写入,其反序列化时会与攻击机建立RMI通信,攻击机开启监听后生成第二次反序列化的payload,返回恶意对象,受害机收到对象后会进行反序列化造成攻击

参考

https://www.cnblogs.com/zpchcbd/p/14934168.html

https://xz.aliyun.com/news/6860

https://xz.aliyun.com/news/6675

https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/payloads/