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开源社区中爆出的各类组件的安全漏洞中大多数与其相关,此处暂不展开做详细论述,后面会写专门的文章来讨论相关内容。