RMI(一)-基础

Chiexf Lv4

0x00 前言

最近在学习RMI的反序列化漏洞,简单的记录下RMI的实现流程和可能存在的利用点

0x01 介绍

RMI,是Remote Method Invocation(远程方法调用)的缩写,即在一个JVMjava程序调用在另一个远程JVM中运行的java程序,这个远程JVM既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。

RMI有三个对象

1
2
3
Registry : 提供服务注册和服务获取,服务端将类名称,存放地址注册到Registry中,以供客户端获取。
Server : 远程方法的提供者。
Client : 远程方法的调用者

0x02 简单使用

三层架构

从RMI设计角度来讲,基本分为三层架构模式来实现RMI,分别为RMI服务端,RMI客户端和RMI注册中心

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用(在低版本的JDK中,ServerRegistry是可以不在一台服务器上的,而在高版本的JDK中,ServerRegistry只能在一台服务器上,否则无法注册成功)

RMI服务端

远程对象

远程调用方法的对象必须继承java.rmi.Remote接口

远程对象的实现类必须继承UnicastRemoteObject类,如果没有继承UnicastRemoteObject,则需手动创建

  • IRemoteObj接口

该接口是客户端和服务端共用的接口,内部定义了我们将要远程调用的对象方法sayHello()

1
2
3
4
public interface IRemoteObj extends Remote{
//sayHello就是客户端要调用的方法,需要抛出RemoteException
public String sayHello(String keywords) throws RemoteException;
}
  • RemoteObjImpl实现类

RemoteObjImpl是一个服务端远程对象,提供了一个sayHello方法供远程调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{
protected RemoteObjImpl() throws RemoteException {
super();
}
// public RemoteObjImpl() throws RemoteException {
//// 如果不继承UnicastRemoteObject就需要手工导入
//// UnicastRemoteObject.exportObject(this,0);
// }

@Override
public String sayHello(String keywords) throws RemoteException {
//将小写转成大写
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
  • RMIServer

主类RMIServer主要用来创建registry

1
2
3
4
5
6
7
8
9
10
11
12
public class RMIServer {
public void register() throws Exception {
//绑定注册中心
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/remoteObj",remoteObj);
}

public static void main(String[] args) throws Exception {
new RMIServer().register();
}
}

Registry端

先将被远程调用的实现类RemoteObjImpl实例化,并且在本地某个端口创建一个registry

再使用Naming.bind将实例化的对象和地址对象绑定在一起

1
2
3
4
5
6
public void register() throws Exception {
//绑定注册中心
RemoteObjImpl remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/remoteObj",remoteObj);
}

RMI客户端

  • IRemoteObj接口

该接口与服务端接口要一致,不然无法调用对应的方法

1
2
3
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
  • RMIClient
1
2
3
4
5
6
7
8
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException, NamingException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
String hello = remoteObj.sayHello("hello");
System.out.println(hello);
}
}

0x03 流程实现

创建远程对象

在初始化的时候,会先创建一个UnicastRemoteObject对象,在调用其exportObject来将远程对象导出

此时的port赋值为0,代表会把一个远程对象发布到一个随机端口上

它跟注册中心的1099是不一样的

1
2
3
4
5
protected UnicastRemoteObject(int port) throws RemoteException
{
this.port = port;
exportObject((Remote) this, port);
}

再来看一下exportObject方法

可以看到有两个参数,第一个参数是obj,主要是来实现逻辑的

第二个参数是**new UnicastServerRef(port)**,主要是来实现网络请求的逻辑实现

1
2
3
4
5
public static Remote exportObject(Remote obj, int port)
throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}

那就跟进UnicastServerRef中,可以看到实现了个LiveRef对象

1
2
3
public UnicastServerRef(int port) {
super(new LiveRef(port));
}

那我们在看看LiveRef又做了什么

1
2
3
public LiveRef(int port) {
this((new ObjID()), port);
}

很明显第一个参数是个ID,所以直接去看一下它的构造函数

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

主要来看看这个getLocalEndpoint是干嘛的

可以看到这边返回值是一个TCPEndpoint类型的对象

1
2
3
public static TCPEndpoint getLocalEndpoint(int port) {
return getLocalEndpoint(port, null, null);
}

TCPEndpoint是一个网络请求的类,我们可以去看一下它的构造函数,传参进去一个IP与一个端口,也就是说传进去一个IP和一个端口,就可以进行网络请求。

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

接着继续去看一下LiveRef的构造函数做了什么

发现hostport是赋值到了endpoint里面,而endpoint又是被封装在LiveRef里面的

1
2
3
4
5
public LiveRef(ObjID objID, Endpoint endpoint, boolean isLocal) {
ep = endpoint;
id = objID;
this.isLocal = isLocal;
}

一路返回,在UnicastServerRef的父类UnicastRef

又将LiveRef赋值给ref,也就是说我们只创建了一个LiveRef,对应远程服务的端口

1
2
3
public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

接着我们回到最初,去看看exportObject做了什么

1
2
3
4
5
6
7
8
9
private static Remote exportObject(Remote obj, UnicastServerRef sref)
throws RemoteException
{
// if obj extends UnicastRemoteObject, set its ref.
if (obj instanceof UnicastRemoteObject) {
((UnicastRemoteObject) obj).ref = sref;
}
return sref.exportObject(obj, null, false);
}

这里的sref其实就是包含了刚创建的LiveRefUnicastServerRef

也就是说,现在调用的是UnicastServerRefexportObject方法,来看看做了什么

可以看到在这边创建了stub,原因是

  • RMI会先在服务端创建一个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
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

可以看到在createProxy中有个判断

1
2
3
4
5
6
7
8
9
10
11
12
13
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
//省略部分代码
if (forceStubUse ||
!(ignoreStubClasses || !stubClassExists(remoteClass)))
{
return createStub(remoteClass, clientRef);
}
//省略部分代码
}

看看那个判断中的stubClassExists做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
private static boolean stubClassExists(Class<?> remoteClass) {
if (!withoutStubs.containsKey(remoteClass)) {
try {
Class.forName(remoteClass.getName() + "_Stub",
false,
remoteClass.getClassLoader());
return true;
} catch (ClassNotFoundException cnfe) {
withoutStubs.put(remoteClass, null);
}
}
return false;
}

可以看出主要是用于通过加载远程类名称加上**”_Stub”后缀的类,来检查给定的远程类是否存在对应的stub**类

RMI中,常用于在执行远程调用之前,检查远程接口和stub类的正确性

目前我们是没有这个类的,所以也不会去执行判断里的createStub方法

判断后就是创建动态代理的流程了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static Remote createProxy(Class<?> implClass,
RemoteRef clientRef,
boolean forceStubUse)
throws StubNotFoundException
{
//省略部分代码......

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);
}
}

第一个参数是AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面只有一个 ref,还是原本那个封装的LiveRef

这边也就把动态代理创建好了

返回exportObject中,往下走会出现一个判断,不过会跳过这个判断

因为在上面创建Stub的时候没有走进createStub(),返回的对象没有继承RemoteStub

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
//省略部分代码......
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

接着会走到target这里,看一下在Target对象里都封装了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Target(Remote impl, Dispatcher disp, Remote stub, ObjID id,
boolean permanent)
{
this.weakImpl = new WeakRef(impl, ObjectTable.reapQueue);
this.disp = disp;
this.stub = stub;
this.id = id;
this.acc = AccessController.getContext();
/*
* ......
*/
ClassLoader threadContextLoader =
Thread.currentThread().getContextClassLoader();
ClassLoader serverLoader = impl.getClass().getClassLoader();
if (checkLoaderAncestry(threadContextLoader, serverLoader)) {
this.ccl = threadContextLoader;
} else {
this.ccl = serverLoader;
}
this.permanent = permanent;
if (permanent) {
pinImpl();
}
}

这里的ref还是LiveRef,接着调用refexportObject方法将target发布出去

1
2
3
4
5
6
7
public void exportObject(Target target) throws RemoteException {
ep.exportObject(target);
}

public void exportObject(Target target) throws RemoteException {
transport.exportObject(target);
}

一路跟到这里sun.rmi.transport.tcp下的TCPTransport类的exportObject方法

首先走到了listen里面,开始了网络监听,也就是真正的网络请求部分

1
2
3
4
5
6
7
8
9
10
11
12
public void exportObject(Target target) throws RemoteException {
/*
* Ensure that a server socket is listening, and count this
* export while synchronized to prevent the server socket from
* being closed due to concurrent unexports.
*/
synchronized (this) {
listen();
exportCount++;
}
//省略部分代码......
}

看一下listen中做了什么

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private void listen() throws RemoteException {
assert Thread.holdsLock(this);
TCPEndpoint ep = getEndpoint();
int port = ep.getPort();
if (server == null) {
if (tcpLog.isLoggable(Log.BRIEF)) {
tcpLog.log(Log.BRIEF,
"(port " + port + ") create server socket");
}
try {
server = ep.newServerSocket();
/*
* Don't retry ServerSocket if creation fails since
* "port in use" will cause export to hang if an
* RMIFailureHandler is not installed.
*/
Thread t = AccessController.doPrivileged(
new NewThreadAction(new AcceptLoop(server),
"TCP Accept-" + port, true));
t.start();
}
//省略部分代码......
}
}

先获取TCPEndpoint,然后会开启一个新的ServerSocket,并且创建一个新的线程Thread

newServerSocket中,如果port==0还会给port随机赋值

1
2
3
4
5
6
ServerSocket newServerSocket() throws IOException {
//省略部分代码......
if (listenPort == 0)
setDefaultPort(server.getLocalPort(), csf, ssf);
return server;
}

这部分都是一些网络方面的东西,不太懂就先跳过了。。。

然后到这里的化,远程对象已经在服务端上的随机端口发布出去了

后面这部分就是记录一下远程对象发布到哪里去了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void exportObject(Target target) throws RemoteException {
//省略部分代码......
boolean ok = false;
try {
super.exportObject(target);
ok = true;
} finally {
if (!ok) {
synchronized (this) {
decrementExportCount();
}
}
}
}

看一下这个**super.exportObject(target)**做了什么

主要是**ObjectTable.putTarget(target)**这部分

1
2
3
4
public void exportObject(Target target) throws RemoteException {
target.setExportedTransport(this);
ObjectTable.putTarget(target);
}

来看一下putTarget,主要是在put部分

1
2
3
4
5
6
7
8
9
10
11
12
13
static void putTarget(Target target) throws ExportException {
//省略部分代码......
synchronized (tableLock) {
if (target.getImpl() != null) {
//省略部分代码
objTable.put(oe, target);
implTable.put(weakImpl, target);
if (!target.isPermanent()) {
incrementKeepAliveCount();
}
}
}
}

也就是说,将信息存储在ObjectTable类的两个静态Map中

相当于给服务端做了个备份吧

1
2
3
4
private static final Map<ObjectEndpoint,Target> objTable =
new HashMap<>();
private static final Map<WeakRef,Target> implTable =
new HashMap<>();

创建注册中心

首先会进入到一个静态方法createRegistry中,传一个端口,默认是1099

1
2
3
public static Registry createRegistry(int port) throws RemoteException {
return new RegistryImpl(port);
}

创建的这个RegistryImpl对象,首先会做个安全检查

后面创建了个LiveRef,以及创建了一个新的UnicastServerRef,这里跟前面的创建的远程对象是很像的

也就说这里又创建一个服务端引用,作为参数交给了setup方法,跟服务端创建远程对象类似

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// grant permission for default port only.
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref));
}
}

我们可以先看一下UnicastServerRef做了什么,主要是将liveRef赋值给ref

1
2
3
4
5
6
7
8
9
10
11
public UnicastServerRef(LiveRef ref) {
super(ref);
}

public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

public UnicastRef(LiveRef liveRef) {
ref = liveRef;
}

现在我们去看一下setup方法

1
2
3
4
5
6
7
8
9
private void setup(UnicastServerRef uref)
throws RemoteException
{
/* Server ref must be created and assigned before remote
* object 'this' can be exported.
*/
ref = uref;
uref.exportObject(this, null, true);
}

一样,也是调了UnicastServerRef对象的exportObject方法

第一个参数代表远程对象,创建远程对象就是自己实现的Impl,创建注册中心就是RegistryImpl

第三个参数代表时效选项,上次是false,这次变成了true

接着来看一下exportObject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public Remote exportObject(Remote impl, Object data,
boolean permanent)
throws RemoteException
{
Class<?> implClass = impl.getClass();
Remote stub;
try {
stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
} catch (IllegalArgumentException e) {
throw new ExportException(
"remote object implements illegal remote interface", e);
}
if (stub instanceof RemoteStub) {
setSkeleton(impl);
}
Target target =
new Target(impl, this, stub, ref.getObjID(), permanent);
ref.exportObject(target);
hashToMethod_Map = hashToMethod_Maps.get(implClass);
return stub;
}

首先也是创建stub,直接看一下createProxy方法

对比创建远程对象,创建注册中心到这里是直接走进去判断里面的createStub方法

原因是因为,在stubClassExists检测的时候

会发现现在是系统自带的类(在rt.jar->sun->rmi可以找到RegistryImpl_Stub),符合条件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
//省略部分代码......
}

继续往下,调用exportObject的**setSkeleton()**方法,

这时的RegistryImpl_Stub对象确实是RemoteStub的子类,所以满足条件,进入setSkeleton方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setSkeleton(Remote impl) throws RemoteException {
if (!withoutSkeletons.containsKey(impl.getClass())) {
try {
skel = Util.createSkeleton(impl);
} catch (SkeletonNotFoundException e) {
/*
* Ignore exception for skeleton class not found, because a
* skeleton class is not necessary with the 1.2 stub protocol.
* Remember that this impl's class does not have a skeleton
* class so we don't waste time searching for it again.
*/
withoutSkeletons.put(impl.getClass(), null);
}
}
}

然后这里有一个createSkeleton()方法,一看名字就知道是用来创建Skeleton

可以看到Skeleton是用**forName()**的方式创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private static RemoteStub createStub(Class<?> remoteClass, RemoteRef ref)
throws StubNotFoundException
{
String stubname = remoteClass.getName() + "_Stub";
/* Make sure to use the local stub loader for the stub classes.
* When loaded by the local loader the load path can be
* propagated to remote clients, by the MarshalOutputStream/InStream
* pickle methods
*/
try {
Class<?> stubcl =
Class.forName(stubname, false, remoteClass.getClassLoader());
Constructor<?> cons = stubcl.getConstructor(stubConsParamTypes);
return (RemoteStub) cons.newInstance(new Object[] { ref });
}
//省略部分代码......
}

这里的skelUnicastServerRef的内部属性

也就是说创建好的skeleton其实会存储在UnicastServerRefskel属性中

接着就是走到Target里了,也就是储存封装的数据,跟之前的一样

可以看到在objTable表中,有三个对象

第一个对象中的valuestub值为DGCImpl_Stub,是分布式垃圾回收的一个对象,它并不是我们刚才创建的,且dispskelDGCImpl_Skel

第二个对象里的valuestub值为**$Proxy对象,是远程对象的,且dispskel**为空

第三个对象中的valuestub值为RegistryImpl_Stub,是我们刚创建的对象,且dispskelRegistryImpl_Skel

绑定

  • Registry+Name方式

bindings就是一个Hashtable

如果当前的keySet中找不到已经绑定的远程对象名,那么就put进去远程对象名和远程对象(动态代理)

1
2
3
4
5
6
7
8
9
10
11
public void bind(String name, Remote obj)
throws RemoteException, AlreadyBoundException, AccessException
{
checkAccess("Registry.bind");
synchronized (bindings) {
Remote curr = bindings.get(name);
if (curr != null)
throw new AlreadyBoundException(name);
bindings.put(name, obj);
}
}

客户端请求注册中心

会先跳转到这

1
2
3
4
5
public static Registry getRegistry(String host, int port)
throws RemoteException
{
return getRegistry(host, port, null);
}

跟进去getRegistry方法中

可以看到先接收对应的参数,然后在本地创建了个LiveRef对象,UnicastRef封装,客户端引用

  • ObjID.REGISTRY_ID对接的id==00代表注册中心
  • TCPEndpoint中的是注册中心的IPport=1099
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
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 = "";
}
}
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);
}

后面又调用了createProxy方法

在实现上跟注册中心相差不大,都是使用反射创建代理,也就是RegistryImpl_Stub类,这里需要UnicastRef参与构造函数

也就是说,注册中心创建的RegistryImpl_Stub其实并没有传递给客户端

而是客户端利用了注册中心的ipport在本地自己创建了个RegistryImpl_Stub

客户端lookup远程对象

首先跟进去RegistryImpl_Stublookup方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
}
//省略部分代码......
return var23;
}
//省略部分代码......
}

可以看到首先会调用一个newCall方法

主要功能就是创建一个连接

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
public RemoteCall newCall(RemoteObject obj, Operation[] ops, int opnum,
long hash)
throws RemoteException
{
clientRefLog.log(Log.BRIEF, "get connection");
Connection conn = ref.getChannel().newConnection();
try {
clientRefLog.log(Log.VERBOSE, "create call context");
/* log information about the outgoing call */
if (clientCallLog.isLoggable(Log.VERBOSE)) {
logClientCall(obj, ops[opnum]);
}
RemoteCall call =
new StreamRemoteCall(conn, ref.getObjID(), opnum, hash);
try {
marshalCustomCallData(call.getOutputStream());
} catch (IOException e) {
throw new MarshalException("error marshaling " +
"custom call data");
}
return call;
} catch (RemoteException e) {
ref.getChannel().free(conn, false);
throw e;
}
}

接着返回lookup,先获取字符串,将其写进一个输出流里面,序列化进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);

//省略部分代码......

}
//省略部分代码......
}

当前类对象为RegistryImpl_Stubref属性为UnicastRef

所以之后调用 UnicastRefinvoke方法

1
2
3
4
5
6
7
public void invoke(RemoteCall call) throws Exception {
try {
clientRefLog.log(Log.VERBOSE, "execute call");
call.executeCall();
}
//省略部分代码......
}

接着会去调用call的**executeCall()**方法,是一个真正处理网络请求的方法

在字节流的层面负责传输:

  • 将客户端想寻找远程对象名字接收,传给注册中心

  • 接收注册中心传递回来的对象的字节流

如果注册中心返回的远程对象的字节流出现异常,readObject会执行,而且in就是数据流里面的东西

如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞

这个地方更隐蔽,危险也更广

因为invoke–>executeCall不只是lookup存在,还有bindlist方法也是会调用invoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void executeCall() throws Exception {
byte returnType;
// read result header
DGCAckHandler ackHandler = null;
//省略部分代码......
// read return value
switch (returnType) {
case TransportConstants.NormalReturn:
break;
case TransportConstants.ExceptionalReturn:
Object ex;
try {
ex = in.readObject();
}
//省略部分代码......
}

返回lookup继续,如果invoke完成请求之后

接着又获取一个输入流var6,也就是说返回值获取到了

也会通过readObject方式执行,var23也就是远程对象的动态代理

远程对象会以动态代理的形式返回,里面包含了Liveref,需要连接的ip:port等等信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public Remote lookup(String var1) throws AccessException, NotBoundException, RemoteException {
try {
RemoteCall var2 = super.ref.newCall(this, operations, 2, 4905912898345647071L);
try {
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(var1);
} catch (IOException var18) {
throw new MarshalException("error marshalling arguments", var18);
}
super.ref.invoke(var2);
Remote var23;
try {
ObjectInput var6 = var2.getInputStream();
var23 = (Remote)var6.readObject();
}
//省略部分代码......
return var23;
}
//省略部分代码......
}

所以说,客户端向注册中心获取远程对象的过程是通过反序列化实现的,最后的远程对象是通过readObject读出来的

如果有个恶意的注册中心,就可以通过这个来攻击客户端

客户端请求服务端

动态代理的核心就是handlerinvoke方法

当调用远程对象的方法时,会走RemoteObjectInvocationHandlerinvoke方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
if (! Proxy.isProxyClass(proxy.getClass())) {
throw new IllegalArgumentException("not a proxy");
}
if (Proxy.getInvocationHandler(proxy) != this) {
throw new IllegalArgumentException("handler mismatch");
}
if (method.getDeclaringClass() == Object.class) {
return invokeObjectMethod(proxy, method, args);
} else if ("finalize".equals(method.getName()) && method.getParameterCount() == 0 &&
!allowFinalizeInvocation) {
return null; // ignore
} else {
return invokeRemoteMethod(proxy, method, args);
}
}

这里会进去invokeRemoteMethod方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private Object invokeRemoteMethod(Object proxy,
Method method,
Object[] args)
throws Exception
{
try {
if (!(proxy instanceof Remote)) {
throw new IllegalArgumentException(
"proxy not Remote instance");
}
return ref.invoke((Remote) proxy, method, args,
getMethodHash(method));
}
//省略部分代码......
}

首先会去调用invoke方法,这是一个重载的方法

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
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
//省略部分代码......
try {
//省略部分代码......
try {
ObjectOutput out = call.getOutputStream();
marshalCustomCallData(out);
Class<?>[] types = method.getParameterTypes();
for (int i = 0; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
} catch (IOException e) {
clientRefLog.log(Log.BRIEF,
"IOException marshalling arguments: ", e);
throw new MarshalException("error marshalling arguments", e);
}
// unmarshal return
call.executeCall();
}
}

然后会去调用marshalValue方法

判断一堆类型,之后再进行序列化,这里的参数指的是传进去的hello

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
protected static void marshalValue(Class<?> type, Object value,
ObjectOutput out)
throws IOException
{
if (type.isPrimitive()) {
if (type == int.class) {
out.writeInt(((Integer) value).intValue());
} else if (type == boolean.class) {
out.writeBoolean(((Boolean) value).booleanValue());
} else if (type == byte.class) {
out.writeByte(((Byte) value).byteValue());
} else if (type == char.class) {
out.writeChar(((Character) value).charValue());
} else if (type == short.class) {
out.writeShort(((Short) value).shortValue());
} else if (type == long.class) {
out.writeLong(((Long) value).longValue());
} else if (type == float.class) {
out.writeFloat(((Float) value).floatValue());
} else if (type == double.class) {
out.writeDouble(((Double) value).doubleValue());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
out.writeObject(value);
}
}

序列化之后会调用call.executeCall方法,这个危险点在前面有提到

所有的客户端的网络请求都会调用这个方法

接着继续往下走

如果返回值不为null,调用unmarshalValue方法

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
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum)
throws Exception
{
//省略部分代码......
try {
//省略部分代码......
// unmarshal return
call.executeCall();
try {
Class<?> rtype = method.getReturnType();
if (rtype == void.class)
return null;
ObjectInput in = call.getInputStream();

Object returnValue = unmarshalValue(rtype, in);

alreadyFreed = true;
ref.getChannel().free(conn, true);
return returnValue;
}
//省略部分代码.......
}
}

看一下unmarshalValue这个方法逻辑

也就是说如果想要获取远程函数调用的结果,是通过反序列化获取的

这里的unmarshalValue与前面的marshalValue是对称的

我们传进去的是String类型的,不符合判断里的条件,所以会读出返回值

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
protected static Object unmarshalValue(Class<?> type, ObjectInput in)
throws IOException, ClassNotFoundException
{
if (type.isPrimitive()) {
if (type == int.class) {
return Integer.valueOf(in.readInt());
} else if (type == boolean.class) {
return Boolean.valueOf(in.readBoolean());
} else if (type == byte.class) {
return Byte.valueOf(in.readByte());
} else if (type == char.class) {
return Character.valueOf(in.readChar());
} else if (type == short.class) {
return Short.valueOf(in.readShort());
} else if (type == long.class) {
return Long.valueOf(in.readLong());
} else if (type == float.class) {
return Float.valueOf(in.readFloat());
} else if (type == double.class) {
return Double.valueOf(in.readDouble());
} else {
throw new Error("Unrecognized primitive type: " + type);
}
} else {
return in.readObject();
}
}

注册中心响应客户端

Registry端,由sun.rmi.transport.tcp.TCPTransport#handleMessages来处理请求,调用serviceCall方法处理

serviceCall方法中从ObjectTable中获取封装的Target对象,并获取其中的封装的UnicastServerRef以及RegistryImpl对象。然后调用UnicastServerRefdispatch方法

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 boolean serviceCall(final RemoteCall call) {
try {
/* read object id */
final Remote impl;
ObjID id;
try {
id = ObjID.read(call.getInputStream());
} catch (java.io.IOException e) {
throw new MarshalException("unable to read objID", e);
}
/* get the remote object */
Transport transport = id.equals(dgcID) ? null : this;
Target target =
ObjectTable.getTarget(new ObjectEndpoint(id, transport));
if (target == null || (impl = target.getImpl()) == null) {
throw new NoSuchObjectException("no such object in table");
}
final Dispatcher disp = target.getDispatcher();
target.incrementCallCount();
try {
/* call the dispatcher */
//省略部分代码......
try {
setContextClassLoader(ccl);
currentTransport.set(this);
try {
java.security.AccessController.doPrivileged(
new java.security.PrivilegedExceptionAction<Void>() {
public Void run() throws IOException {
checkAcceptPermission(acc);
disp.dispatch(impl, call);
return null;
}
}, acc);
}
//省略部分代码......
}
//省略部分代码......
}

看一下UnicastServerRefdispatch方法

这里先判断了this.skel是否为空,用来区别自己是Registry还是Server

然后调用oldDispatch方法,

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
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// positive operation number in 1.1 stubs;
// negative version number in 1.2 stubs and beyond...
int num;
long op;
try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}
op = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}
//省略部分代码......
}
//省略部分代码......
}

跟进oldDispatch方法,在最后调用了this.skel也就是RegistryImpl_Skel类的dispatch方法

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
public void oldDispatch(Remote obj, RemoteCall call, int op)
throws IOException
{
long hash; // hash for matching stub with skeleton
try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
try {
Class<?> clazz = Class.forName("sun.rmi.transport.DGCImpl_Skel");
if (clazz.isAssignableFrom(skel.getClass())) {
((MarshalInputStream)in).useCodebaseOnly();
}
} catch (ClassNotFoundException ignore) { }
hash = in.readLong();
} catch (Exception readEx) {
throw new UnmarshalException("error unmarshalling call header",
readEx);
}
// if calls are being logged, write out object id and operation
logCall(obj, skel.getOperations()[op]);
unmarshalCustomCallData(in);
// dispatch to skeleton for remote object
skel.dispatch(obj, call, op, hash);
}
//省略部分代码......
}

跟进RegistryImpl_Skel类的dispatch方法,这里才是重点

主要作用是根据流中写入的不同的操作类型分发给不同的方法处理

与注册中心进行交互的几种方式在dispatch中的对应关系

1
2
3
4
5
6
7
switch(opnum){
case 0: // bind(String, Remote)
case 1: // list()
case 2: // lookup(String)
case 3: // rebind(String, Remote)
case 4: // unbind(String)
}

我们当前主要是2–>lookup

在服务端,我们将remoteObj名称序列化传到注册中心

在注册中心,会将其反序列读出来

var10其实就是我们lookup寻找远程对象的方法名

不只是在lookup中,只要存在readObject都有机会利用

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
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
//省略部分代码......
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
//省略部分代码......
}
}
}

服务端响应客户端

当前的Target是动态代理Proxy,所以skel==null

就不会调用oldDispatch方法了

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
public void dispatch(Remote obj, RemoteCall call) throws IOException {
// positive operation number in 1.1 stubs;
// negative version number in 1.2 stubs and beyond...
int num;
long op;
try {
// read remote call header
ObjectInput in;
try {
in = call.getInputStream();
num = in.readInt();
if (num >= 0) {
if (skel != null) {
oldDispatch(obj, call, num);
return;
} else {
throw new UnmarshalException(
"skeleton class not found but required " +
"for client version");
}
}
op = in.readLong();
}
//省略部分代码......
}

继续往下走,获取到输入流,以及MethodMethod就是我们之前写的**sayHello()**方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public void dispatch(Remote obj, RemoteCall call) throws IOException {
//省略部分代码......
MarshalInputStream marshalStream = (MarshalInputStream) in;
marshalStream.skipDefaultResolveClass();
Method method = hashToMethod_Map.get(op);
if (method == null) {
throw new UnmarshalException("unrecognized method hash: " +
"method not supported by remote object");
}
// if calls are being logged, write out object id and operation
logCall(obj, method);
//省略部分代码......
}

继续往下走,循环当中的**unmarshalValue()**方法

这个之前有提到,会将客户端传过来的序列化参数反序列化出来,是存在漏洞的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void dispatch(Remote obj, RemoteCall call) throws IOException {
//省略部分代码......
// unmarshal parameters
Class<?>[] types = method.getParameterTypes();
Object[] params = new Object[types.length];
try {
unmarshalCustomCallData(in);
for (int i = 0; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
} catch (java.io.IOException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} catch (ClassNotFoundException e) {
throw new UnmarshalException(
"error unmarshalling arguments", e);
} finally {
call.releaseInputStream();
}
//省略部分代码......
}

继续往下走,最后利用marshalValue方法,将结果序列化,写到字节流,返回给客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void dispatch(Remote obj, RemoteCall call) throws IOException {
//省略部分代码......
// make upcall on remote object
Object result;
try {
result = method.invoke(obj, params);
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
// marshal return value
try {
ObjectOutput out = call.getResultStream(true);
Class<?> rtype = method.getReturnType();
if (rtype != void.class) {
marshalValue(rtype, result, out);
}
}
//省略部分代码......
}

0x03 总结

引用一下素十八 师傅的总结

RMI底层通讯采用了 Stub(运行在客户端)Skeleton(运行在服务端) 机制,**RMI **调用远程方法的大致如下:

  1. **RMI 客户端在调用远程方法时会先创建Stub (sun.rmi.registry.RegistryImpl_Stub)**。
  2. **Stub **会将 Remote 对象传递给远程引用层 **( java.rmi.server.RemoteRef ) **并创建 **java.rmi.server.RemoteCall( 远程调用 )**对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI 服务端的远程引用层**( sun.rmi.server.UnicastServerRef )**收到请求会请求传递给 **Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )**。
  6. **Skeleton **调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bindlistlookuprebindunbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI 服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. **RMI **客户端反序列化 RMI 远程方法调用结果

0x04 参考资料

https://su18.org/post/rmi-attack/

https://www.bilibili.com/video/BV1L3411a7ax?p=1&vd_source=19d2e433219440bcf5304fbe8a00b7ff

https://wx.zsxq.com/dweb2/index/group/2212251881