Java RMI学习与解读(二)
写在前面
接上篇文章,这篇主要是跟着看下整个RMI过程中的源码并对其做简单的分析
RMI源码分析
还是先回顾下RMI流程:
- 创建远程对象接口(RemoteInterface)
- 创建远程对象类(RemoteObject)实现远程对象接口(RemoteInterface)并继承UnicastRemoteObject类
- 创建Registry&Server端,一般Registry和Server都在同一端。
- 创建注册中心(Registry)
LocateRegistry.getRegistry("ip", port);
- 创建Server端:主要是实例化远程对象
- 注册远程对象:通过
Naming.bind(rmi://ip:port/name ,RemoteObject)
将name与远程对象(RemoteObject)进行绑定
- 创建注册中心(Registry)
- 远程对象接口(RemoteInterface)应在Client/Registry/Server三个角色中都存在
- 创建Client端
- 获取注册中心
LocateRegistry.getRegistry('ip', prot)
- 通过
registry.lookup(name)
方法,依据别名查找远程对象的引用并返回存根(Stub)
- 获取注册中心
- 通过存根(Stub)实现RMI(Remote Method Invocation)
创建远程接口与远程对象
在new RemoteObject的过程中主要做了这三件事
- 创建本地存根stub,用于客户端(Client)访问。
- 启动 socket,监听本地端口。
- Target注册与查找。
先抛出一段RemoteInterface和RemoteObject的代码
RemoteInterface
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RemoteInterface extends Remote{
String doSomething(String thing) throws RemoteException;
String say() throws RemoteException;
String sayGoodbye() throws RemoteException;
String sayServerLoadClient(Object name) throws RemoteException;
Object sayClientLoadServer() throws RemoteException;
}
RemoteObject
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String doSomething(String thing) throws RemoteException {
return new String("Doing " + thing);
}
@Override
public String say() throws RemoteException {
return "This is the say Method";
}
@Override
public String sayGoodbye() throws RemoteException {
return "GoodBye RMI";
}
@Override
public String sayServerLoadClient(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public Object sayClientLoadServer() throws RemoteException {
return new ServerObject();
}
}
那么接下来看看我们之前提到的在代码中必须要写的一些内容
Remote
那么首先看创建远程对象接口(RemoteInterface)部分,这个接口在上篇文章中提到过,需要继承java.rmi.Remote
接口且该接口中声明的方法要抛出RemoteException
异常,在Remote
接口的注释中提到:
这个接口用于识别某些接口是否可以从非本地虚拟机调用方法,且远程对象必须间接或直接的实现这个接口;也提到了我们之前说的"特殊的远程接口",当一个接口继承了
java.rmi.Remote
接口后,在该接口上声明的方法才可以被远程调用。
个人感觉有点像一个类似于序列化的标记式接口,用来标记这个接口的实现类是否可以被远程调用该类中的方法。
RemoteException
异常类,注释中说明了,任何一个继承了java.rmi.Remote
的远程接口,在其接口中的方法需要throws RemoteException异常,该异常是指远程方法调用执行过程中可能发生的与通信相关的异常。
流程与代码分析
用于使用JRMP导出远程对象(export remote object)并获取存根,通过存根与远程对象进行通信
主要是构造方法和exportObject(Remote)
,这个点在Longofo师傅的文章有提到,当实现了远程接口而没有继承UnicastRemoteObject
类的话需要自己调UnicastRemoteObject.exportObject(Remote)
方法导出远程对象。
构造方法
/**
* Creates and exports a new UnicastRemoteObject object using an
* anonymous port.
* @throws RemoteException if failed to export object
* @since JDK1.1
*/
protected UnicastRemoteObject() throws RemoteException
{
this(0);
}
exportObject(Remote)
/**
* Exports the remote object to make it available to receive incoming
* calls using an anonymous port.
* @param obj the remote object to be exported
* @return remote object stub
* @exception RemoteException if export fails
* @since JDK1.1
*/
public static RemoteStub exportObject(Remote obj)
throws RemoteException
{
/*
* Use UnicastServerRef constructor passing the boolean value true
* to indicate that only a generated stub class should be used. A
* generated stub class must be used instead of a dynamic proxy
* because the return value of this method is RemoteStub which a
* dynamic proxy class cannot extend.
*/
return (RemoteStub) exportObject(obj, new UnicastServerRef(true));
}
这两个方法最终都会走向重载的exportObject(Remote obj, UnicastServerRef sref)
方法
初始化时会创建UnicastServerRef
对象并调用其exportObject
方法
在方法中会通过createProxy()
方法,创建RemoteObjectInvocationHandler处理器,给RemoteInterface接口创建动态代理
之后回到UnicastServerRef#exportObject
方法,new了一个Target对象,在该对象中封装了远程对象的相关信息,其中就包括stub属性(一个动态代理对象,代理了我们定义的远程接口)
之后调用liveRef的exportObject方法
接着调用sun.rmi.transport.tcp.TCPEndpoint#exportObject
方法(调用栈如下图),最终调用的是TCPTransport#exportObject()
方法在该方法中开启了监听本地端口,并调用了Transport#exportObject()
在该方法中调用了ObjectTable.putTarget()
方法,将 Target 实例注册到 ObjectTable 对象中。
而在ObjectTarget类中提供了两种方式(getTarget的两种重载方法)去查找注册的Target,分别是参数为ObjectEndpoint
类型对象以及参数为Remote
类型的对象
回过头看一下动态代理RemoteObjectInvocationHandler,继承 RemoteObject 实现 InvocationHandler,因此这是一个可序列化的、可使用 RMI 远程传输的动态代理类。主要是关注invoke方法,如果传入的method对象所代表的类或接口的 class对象是Object.class就走invokeObjectMethod
否则走invokeRemoteMethod
在invokeRemoteMethod
方法中最终调用的是UnicastRef.invoke
方法,UnicastRef 的 invoke 方法是一个建立连接,执行调用,并读取结果并反序列化的过程。反序列化在 unmarshalValue
调用readObject实现
如上就是在创建远程接口并实例化远程对象过程中的底层代码运行的流程(多掺杂了一点动态代理部分),这里借一张时序图。
建议各位师傅也是打个断点跟一下比较好,对于整体在实例化远程对象时的一个流程就比较清晰了。
创建注册中心
创建注册中心主要是Registry registry = LocateRegistry.createRegistry(1099);
打断点debug进去,首先是实例化了一个RegistryImpl对象
进入有参构造,先new LiveRef对象,之后new UnicastServerRef对象并作为参数调用setup方法
setup方法中依旧调用UnicastServerRef#exportObject方法,对RegistryImpl对象进行导出;与上一次不同的是这次会直接走进if中创建stub,因为if判断中调用了stubClassExists方法,该方法会判断传入的类是否在本地有xxx_stub
类。
而RegistryImpl显然是有的,所以会走进createStub方法
该方法中反射拿到构造方法然后实例化RegistryImple_Stub类来创建代理类。
调用setSkeleton创建骨架
也是反射操作,实例化RegistryImple_Skel类
最终赋值给UnicastServerRef.skel属性
在UnicastServerRef类中通过dispatch方法实现了对远程对象方法的调用并将结果进行序列化并通过网络传到Client端
public void dispatch(Remote var1, RemoteCall var2) throws IOException {
try {
long var4;
ObjectInput var40;
try {
var40 = var2.getInputStream();
int var3 = var40.readInt();
if (var3 >= 0) {
if (this.skel != null) {
this.oldDispatch(var1, var2, var3);
return;
}
throw new UnmarshalException("skeleton class not found but required for client version");
}
var4 = var40.readLong();
} catch (Exception var36) {
throw new UnmarshalException("error unmarshalling call header", var36);
}
MarshalInputStream var39 = (MarshalInputStream)var40;
var39.skipDefaultResolveClass();
Method var8 = (Method)this.hashToMethod_Map.get(var4);
if (var8 == null) {
throw new UnmarshalException("unrecognized method hash: method not supported by remote object");
}
this.logCall(var1, var8);
Class[] var9 = var8.getParameterTypes();
Object[] var10 = new Object[var9.length];
try {
this.unmarshalCustomCallData(var40);
for(int var11 = 0; var11 < var9.length; ++var11) {
var10[var11] = unmarshalValue(var9[var11], var40);
}
} catch (IOException var33) {
throw new UnmarshalException("error unmarshalling arguments", var33);
} catch (ClassNotFoundException var34) {
throw new UnmarshalException("error unmarshalling arguments", var34);
} finally {
var2.releaseInputStream();
}
Object var41;
try {
var41 = var8.invoke(var1, var10);
} catch (InvocationTargetException var32) {
throw var32.getTargetException();
}
try {
ObjectOutput var12 = var2.getResultStream(true);
Class var13 = var8.getReturnType();
if (var13 != Void.TYPE) {
marshalValue(var13, var41, var12);
}
} catch (IOException var31) {
throw new MarshalException("error marshalling return", var31);
}
} catch (Throwable var37) {
Object var6 = var37;
this.logCallException(var37);
ObjectOutput var7 = var2.getResultStream(false);
if (var37 instanceof Error) {
var6 = new ServerError("Error occurred in server thread", (Error)var37);
} else if (var37 instanceof RemoteException) {
var6 = new ServerException("RemoteException occurred in server thread", (Exception)var37);
}
if (suppressStackTraces) {
clearStackTraces((Throwable)var6);
}
var7.writeObject(var6);
} finally {
var2.releaseInputStream();
var2.releaseOutputStream();
}
}
注册中心与远程服务对象注册的大部分流程相同,差异在:
- 远程服务对象使用动态代理,invoke 方法最终调用 UnicastRef 的 invoke 方法,注册中心使用 RegistryImpl_Stub,同时还创建了 RegistryImpl_Skel
- 远程对象默认随机端口,注册中心默认是 1099(当然也可以指定)
服务注册
这部分其实就是Naming.bind("rmi://127.0.0.1:1099/Zh1z3ven", remoteObject);
的实现
依旧是打断点跟进去看下
进入 java.rmi.Naming#bind()
方法后先会解析处理我们传入的url。先调用java.rmi#parseURL(name)
方法后进入intParseURL(String str)
方法。该方法内部先会对我们传入的url(rmi://127.0.0.1:1099/Zh1z3ven)做一些诸如协议是否为rmi,是否格式存在问题等判断,之后做了字符串的处理操作,分别获取到我们传入的url中的host(127.0.0.1)、port(1099)、name(Zh1z3ven)字段并作为参数传入java.rmi.Naming
的内置类ParsedNamingURL的有参构造方法中去
也就是对该内置类中的属性进行赋值操作
之后回到Naming#bind()
方法,将实例化的ParsedNamingURL
对象赋值给parsed
并作为参数带入java.rmi.Naming#getRegistry
方法
最终进入getRegistry(String host, int port, RMIClientSocketFactory csf)
方法,调用栈如下,后续依旧是创建动态代理的操作。动态代理部分和创建远程对象时操作差不多,就不再跟了
来看一下java.rmi.Naming#bind()
中最后一步,此时会调用RegistryImpl_Stub#bind
方法进行name与远程对象的一个绑定。
方法内逻辑也比较清晰,获取输出流之后进行序列化的然后调用UnicastRef#invoke
方法
大致服务注册,也就是name与远程对象绑定就是这么一个逻辑,这里与su18师傅文章中不太一样的点就是,我跟入的是第二个invoke方法,而su18师傅进入的是第一个invoke方法,这里就有些不解了,待研究。
总结
借一张su18师傅的图。Server/Registry/Client三个角色两两之间的通信都会用到java原生的反序列化操作。也就是说我们有一端可控或可以伪造,那么传入一段恶意的序列化数据直接就可以RCE。也就是三个角色都有不通的攻击场景。
END
调试的时候深感吃力,RMI源码其实我上面提到的可能还是有很多不太对的地方。
其实只要自己打断点debug跟一下,对于RMI的一个工作流程就很清晰了,有些点如果没有刚需可以不用跟的很深入。
后面就是针对RMI的攻击手法了,下篇更。