记关于RMI机制基础的学习

前言

实际解一道题,找到链子也只是基础,具体的攻击手段也许才是核心

RMI

认识

对于这个概念,我们需要建立基本的认识
RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。
具体到漏洞利用阶段,我们可以远程开一个vps,准备一个恶意类
然后利用靶机进行远程调用,实现RCE
RMI整个交互过程依赖于什么协议呢?
JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。

1
2
3
4
Server ———— 服务端:服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket
Client ———— 客户端:客户端调用服务端的方法
因为有了 C/S 的交互,而且 Socket 是对应端口的,这个端口是动态的,所以这里引进了第三个 RMI 的部分 ———— Registry 部分。
Registry ———— 注册端;提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用。

这么理解即可,接下来,我们本地搭一下全过程

本地搭建

服务端概要

明确三个部分

  • 一个继承了java.rmi.Remote 的接⼝,其中定义我们要远程调⽤用的函数,⽐如这里的 hello()
  • 一个实现接口的类
  • 一个主类,用于创建Rigistry,并将上述类实例化绑定到一个地址中

服务端代码

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
package org.vulhub;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements
IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
package org.vulhub;

import java.rmi.Naming;

public class Client {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}

运行

成功调用

分析

wireshark

抓流量包

可以看到流量的移动
总结如下

1
2
3
实际建⽴了两次 TCP 连接,第一次是去连 1099 端口的;第二次是由服务端发送给客户端的。
在第一次连接当中,是客户端连 Registry 的,在其中寻找 Name 为 hello 的对象,这个对应数据流中的 Call 消息;然后 Registry 返回⼀个序列化的数据,这个就是找到的 Name=Hello 的对象,这个对应数据流中的ReturnData消息。
到了第二次连接,服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 172.17.88.209:24429,于是再与这个地址建⽴ TCP 连接;在这个新的连接中,才执⾏真正远程⽅法调⽤,也就是 sayHello()

就这些了()
看了看流量包是这么个意思

调试跟进

学rmi总得跟一个吧
关注三块

1
2
3
Server
Client
Registry

打断点、分析流量理解三者间的逻辑即可,便于我们后续针对细节的攻击

创建

开始
我们看到调试到UnicastRemoteObject的构造函数
port被传入了0
exportObject() 是一个静态函数,它就是主要负责将远程服务发布到网络上
我们利用继承,即不用手动导入了
继续跟进构造方法
调用了UnicastServerRef构造方法
继续调用

1
2
3
public LiveRef(ObjID objID, int port) {  
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
}

第一个参数 ID,第三个参数为 true,所以我们重点关注一下第二个参数。
跟进看一下这个方法实现什么功能

1
2
3
public TCPEndpoint(String host, int port) {
this(host, port, null, null);
}

一个发请求的类,继续跟进this
查看具体创建过程

进入LiveRef 创建的过程
完毕之后跟进exportObject()方法
直到stub的出现

1
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);

而这个stub
RMI 先在 Service 的地方,也就是服务端创建一个 Stub,再把 Stub 传到 RMI Registry 中,最后让 RMI Client 去获取 Stub。
可想而知它的重要性,我们跟进它的创建过程

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
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
Class<?> remoteClass;

try {
remoteClass = getRemoteClass(implClass);
} catch (ClassNotFoundException ex ) {
throw new StubNotFoundException(
"object does not implement a remote interface: " +
implClass.getName());
}

if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}

final ClassLoader loader = implClass.getClassLoader();
final Class<?>[] interfaces = getRemoteInterfaces(implClass);
final InvocationHandler handler =
new RemoteObjectInvocationHandler(clientRef);

/* REMIND: private remote interfaces? */

try {
return AccessController.doPrivileged(new PrivilegedAction<Remote>() {
public Remote run() {
return (Remote) Proxy.newProxyInstance(loader,
interfaces,
handler);
}});
} catch (IllegalArgumentException e) {
throw new StubNotFoundException("unable to create proxy", e);
}
}

这里在创建动态代理
继续打断点,到了

1
2
3
4
5
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;

这里在进行一个封装
在这段过程里打断点仔细分析一下
这里就不讲发生啥了,感兴趣自己调一下
跟进到TCPTransport.java#exportObject
这里关键调用了一个listen方法,只讲结果
多出了一个随机化的端口


大佬是这么说的
发布之后的记录就没跟进了()

1
2
3
用 exportObject() 指定到发布的 IP 与端口,端口的话是一个随机值。至始至终复杂的地方其实都是在赋值,创建类,进行各种各样的封装,实际上并不复杂。
还有一个过程就是发布完成之后的记录,理解的话,类似于日志就可以了,这些记录是保存到静态的 HashMap 当中。
这一块是服务端自己创建远程服务的这么一个操作,所以这一块是不存在漏洞的。

我们抓个流量包看看

是正确的

注册

我们把这个类放到这个端口上之后,就要开始注册操作,跟进
细节就不说了,总之就是不断跟进,主体还是和上面的分析是一致的,不过只是几点细微的差距
比如
创建注册中心是走进到 createStub(remoteClass, clientRef); 进去的,而发布远程对象则是直接创建动态代理的,等

1
2
总结一下比较简单,注册中心这里其实和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心。
绑定的话就更简单了,一句话形容一下就是 hashTable.put(IP, port)
客户端调用

这一块好好跟一下,看看,漏洞为什么会产生,这也是本次学习的核心

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
public static Registry getRegistry(String host, int port,
RMIClientSocketFactory csf)
throws RemoteException
{
Registry registry = null;

if (port <= 0)
port = Registry.REGISTRY_PORT;

if (host == null || host.length() == 0) {
// If host is blank (as returned by "file:" URL in 1.0.2 used in
// java.rmi.Naming), try to convert to real local host name so
// that the RegistryImpl's checkAccess will not fail.
try {
host = java.net.InetAddress.getLocalHost().getHostAddress();
} catch (Exception e) {
// If that failed, at least try "" (localhost) anyway...
host = "";
}
}

/*
* Create a proxy for the registry with the given host, port, and
* client socket factory. If the supplied client socket factory is
* null, then the ref type is a UnicastRef, otherwise the ref type
* is a UnicastRef2. If the property
* java.rmi.server.ignoreStubClasses is true, then the proxy
* returned is an instance of a dynamic proxy class that implements
* the Registry interface; otherwise the proxy returned is an
* instance of the pregenerated stub class for RegistryImpl.
**/
LiveRef liveRef =
new LiveRef(new ObjID(ObjID.REGISTRY_ID),
new TCPEndpoint(host, port, csf, null),
false);
RemoteRef ref =
(csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
}

这里同样ref,封装的是 127.0.01:1099
继续跟进,总之会进入
UnicastRef#invoke方法里面
再跟进call.executeCall()方法
再跟进getDGCAckHandler()方法

在这里存在一个反序列化

1
in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽。

当然这里还只是进行连接,到执行方法那一步呢?
先在 RemoteObjectInvocationHandler 类下的 invoke() 方法的 if 判断里面打个断点,这样才能走进去,否则是debug不进去的
跟进invokeRemoteMethod方法

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

再跟进
UnicastRef#invoke方法(之前调过)
rmi处理网络请求的漏洞就藏在里面
跟进 marshalValue()
这里out.writeObject(value);
然后再走 unmarshalValueSee方法,因为现在我们传进去的类型是 String,不符合上面的一系列类型,这里会进行反序列化的操作,把这个数据读回来,这里是存在入口类的攻击点的
也就是在执行方法的时候,走了两种反序列化入口
流程

1
分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信。
注册中心干什么

这里牵扯到客户端打注册中心的打法
这里选择打Transport.java,打在Target上
跟进dispatch方法
跟进ldDispatch方法
跟进RegistryImpl_Skel#dispatch方法

1
2
3
4
5
6
7
8
9
10
11
12
13
我们与注册中心进行交互可以使用如下几种方式:
list
bind
rebind
unbind
lookup
这几种方法位于 RegistryImpl_Skel#dispatch 中,也就是我们现在 dispatch 这个方法的地方。
如果存在对传入的对象调用 readObject 方法,则可以利用,dispatch 里面对应关系如下:
0->bind
1->list
2->lookup
3->rebind
4->unbind

看一下哪些会反序列化即可
除了 list 都可
注册中心就是处理 Target,进行 Skel 的生成与处理
漏洞点是在 dispatch 这里,存在反序列化的入口类。这里可以结合 CC 链子打的

服务端做了什么呢?

这里也藏着不少漏洞
打两个断点

和之前差不多,也是进入循环当中的 unmarshalValue() 方法,这里和我们之前说的一样,是存在漏洞的。
这里传入hello,序列化进去,反序列化出来

DGC的过程

DGC:是自动创建的一个过程,用于清理内存。
这个就不跟进了,总的反序列化漏洞和之前很类似

小结

整体慢调了下,虽然有点粗糙()
大体了解了下整个运作

作者还讲了一篇关于的攻击手段,暂且搁浅了,内容比较多,实际做题时再回头看看

参考

https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/#%E5%B0%8F%E7%BB%93%E4%B8%80%E4%B8%8B-DGC-%E7%9A%84%E8%BF%87%E7%A8%8B