RMI简单学习
RMI
RMI全称是Remote Method Invocation,远程⽅法调⽤。从这个名字就可以看出,他的⽬标和RPC其实 是类似的,是让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的⽅法,只不过RMI是Java独 有的⼀种机制。
我们直接从⼀个例⼦开始演示RMI的流程吧。 ⾸先编写⼀个RMI Server:
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements
IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
⼀个RMI Server分为三部分:
- ⼀个继承了 java.rmi.Remote 的接⼝,其中定义我们要远程调⽤的函数,⽐如这⾥的 hello()
- ⼀个实现了此接⼝的类
- ⼀个主类,⽤来创建Registry,并将上⾯的类实例化后绑定到⼀个地址。这就是我们所谓的Server 了。
然后我们写一个RMI Client:
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://10.0.27.142:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}
执行,就达到了远程调用RMI server上的方法。
一个RMI有三个参与者
- RMI Registry
- RMI Server
- RMI Client
但是为什么我给的示例代码只有两个部分呢?原因是,通常我们在新建一个RMI Registry的时候,都会 直接绑定一个对象在上面,也就是说我们示例代码中的Server其实包含了Registry和Server两部分:
LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());
第一行创建并运行RMI Registry,第二行将RemoteHelloWorld对象绑定到Hello这个名字上。 Naming.bind
的第一个参数是一个URL,形如: rmi://host:port/name
。其中,host和port就是 RMI Registry的地址和端口,name是远程对象的名字。 如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是 localhost ,port默认 是 1099 :
Naming.bind("Hello", new RemoteHelloWorld());
以上就是RMI整个的原理与流程。接下来,我们很自然地想到,RMI会给我们带来哪些安全问题?
1.如果我们能访问RMI Registry服务,如何对其攻击?
2.如果我们控制了目标RMI客户端中 Naming.lookup
的第一个参数(也就是RMI Registry的地址),能不能进行攻击?
RMI利用codebase执行任意代码
codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。
如果我们指定 codebase=http://example.com/
,然后加载 org.vulhub.example.Example
类,则 Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为 Example类的字节码。
RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。
这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?
对,在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去 CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。
不过满足下列条件才能被攻击:
- 安装并配置了
SecurityManager
- Java版本低于7u21、6u45,或者设置了
java.rmi.server.useCodebaseOnly=false
官方将 java.rmi.server.useCodebaseOnly
的默认值由 false 改为了 true 。在 java.rmi.server.useCodebaseOnly
配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从RMI请求中获取。
因为利用条件过于苛刻,没有复现该漏洞