目录
1.概述
RPC:
RPC(Remote Procedure Call),一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议,RPC可以用HTTP协议实现,并且用HTTP是建立在 TCP 之上最广泛使用的 RPC,但是互联网公司往往用自己的私有协议,比如鹅厂的JCE协议,私有协议不具备通用性但是相比于HTTP协议,RPC采用二进制字节码传输,更加高效也更加安全。
用一个比较形象的例子来形容,你老婆出去打麻将,然后想起家里洗衣机里的衣服还没洗,于是打电话给在家里的你,叫你把洗衣机里的衣服洗了,这就是远程过程调用。微服务中服务的调用底层就是使用的RPC机制。
RMI:
RMI(Remote Method Invocation),在JDK1.2中推出,RPC的Java版本,官方的说法是RMI 支持存储于不同地址空间的程序级对象之间彼此进行通信,实现远程对象之间的无缝远程调用。直白的说法RMI其实就是支持一个JVM去调用另一个JVM中的对象中的方法。其底层的其实就是靠socket以及序列化和反序列化支撑起来的,使用分布式垃圾收集器(DGC)进行GC。
RMI是一个分布式的架构,由三部分组成:
-
客户端
远程对象的调用者
-
服务器
定义、发布远程对象
-
注册中心
JDK提供的一个可以独立运行的程序,在bin目录下,其运行在服务器端的一个固定端口上。
2.详述
2.1.流程分析
2.1.1.手写实现
为了方便理解RMI的整个流程,我们首先基于网络通信和序列化、反序列化来手写一个远程方法调用的demo:
核心为两个代理:
-
Skeleton
骨架,服务器端的远程对象的代理,封装远程对象及网络通信能力。
-
stub
存根,客户端的远程对象的代理,搭建一个远程对象的骨架,具体的方法调用通过网络通信来访问远程对象的对应方法。
接口:
public interface Person {
int getAge() throws Throwable;
String getName() throws Throwable;
}
服务端:
//实现类
public class PersonServer implements Person{
private int age;
private String name;
public PersonServer(String name,int age){
this.age=age;
this.name=name;
}
public int getAge() throws Throwable {
return age;
}
public String getName() throws Throwable {
return name;
}
}
//骨架
public class Person_Skeleton extends Thread{
private PersonServer myServer;
public Person_Skeleton(PersonServer server) {
// get reference of object server
this.myServer = server;
}
public void run() {
try {
// new socket at port 9000
ServerSocket serverSocket = new ServerSocket(9000);
// accept stub's request
Socket socket = serverSocket.accept();
while (socket != null) {
// get stub's request
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
String method = (String)inStream.readObject();
// check method name
if (method.equals("age")) {
// execute object server's business method
int age = myServer.getAge();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeInt(age);
outStream.flush();
}
if(method.equals("name")) {
// execute object server's business method
String name = myServer.getName();
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
// return result to stub
outStream.writeObject(name);
outStream.flush();
}
}
} catch(Throwable t) {
t.printStackTrace();
System.exit(0);
}
}
}
客户端:
//存根
public class Person_Stub implements Person{
private Socket socket;
public Person_Stub() throws Throwable {
// connect to skeleton
socket = new Socket("127.0.0.1", 9000);
}
public int getAge() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("age");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return inStream.readInt();
}
public String getName() throws Throwable {
// pass method name to skeleton
ObjectOutputStream outStream =
new ObjectOutputStream(socket.getOutputStream());
outStream.writeObject("name");
outStream.flush();
ObjectInputStream inStream =
new ObjectInputStream(socket.getInputStream());
return (String)inStream.readObject();
}
}
2.1.2.JDK实现
结合前文的手写实现来看JDK给出的实现整个流程一目了然:
服务器端生产远程对象和骨架代理,将远程对象注册到注册中心中,客户端找注册中心拿远程对象的时候会获取到远程对象的存根代理,通过存根代理和骨架代理之间的网络通信就实现了远程方法调用。
代码示例:
1.服务器
//继承远程调用接口,定义方法模板
public interface IRemoteObj extends Remote {
String sayHello(String keyWords) throws RemoteException;
}
//实现业务方法
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
protected RemoteObjImpl() throws RemoteException {
//如果不继承UnicastRemoteObject就需要手动导出
//UnicastRemoteObject.exportObject(this,0);
}
public String sayHello(String keyWords) throws RemoteException {
System.out.println(keyWords);
return "hello "+keyWords;
}
}
//启动入口
public class RMIServer {
public static void main(String[] args) throws Exception{
IRemoteObj remoteObj=new RemoteObjImpl();
//创建注册中心
Registry registry= LocateRegistry.createRegistry(1099);
//向注册中心中注册对象
registry.bind("remoteObj",remoteObj);
}
}
2.客户端
public interface IRemoteObj extends Remote {
String sayHello(String keyWords) throws RemoteException;
}
public class RMIClient {
public static void main(String[] args) throws Exception {
//连接注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
//调用对象
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("world!");
}
}
2.2.安全漏洞
2.2.1.漏洞成因
由于RMI底层使用了序列化和反序列化,因此存在序列化相关的漏洞。
Serializable接口其实隐式的提供了两个方法writeObject、readObject用来自定义序列化时的读写动作,这两个方法在重写快捷键中是看不到的,但是只要定义了这两个方法就会生效。
反序列化由于需要从外部去加载类,这样给一些恶意代码的注入提供了机会,以下为一个反序列化注入攻击的例子:
public class MyObject implements Serializable {
private static final long serialVersionUID = -6554051283690579548L;
public String name;
//重写readObject()方法
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
//执行默认的readObject()方法
in.defaultReadObject();
//执行指定程序
Runtime.getRuntime().exec("calc.exe");
}
}
public class test {
public static void main(String[] args) throws Exception {
//定义myObj对象
MyObject myObj = new MyObject();
myObj.name = "hello world";
//创建一个包含对象进行反序列化信息的”object”数据文件
FileOutputStream fos = new FileOutputStream("object");
ObjectOutputStream os = new ObjectOutputStream(fos);
//writeObject()方法将myObj对象写入object文件
os.writeObject(myObj);
os.close();
//从文件中反序列化obj对象
FileInputStream fis = new FileInputStream("object");
ObjectInputStream ois = new ObjectInputStream(fis);
//恢复对象
MyObject objectFromDisk = (MyObject)ois.readObject();
System.out.println(objectFromDisk.name);
ois.close();
}
}
2.2.2.防御方法
-
认知和签名
-
禁止JVM执行外部命令Runtime.exec
-
RASP监测
序列化、反序列化漏洞攻击与防御是一个很大的话题,历年来JAVA开源社区中爆出的各类组件的安全漏洞中大多数与其相关,此处暂不展开做详细论述,后面会写专门的文章来讨论相关内容。