前言
今天就开始具体Java安全的学习了,首先从RMI安全开始,但是由于RMI的不少安全问题都涉及了反序列化的问题,这方面的内容为目前暂未涉足,因此本文只是对RMI入门的第一篇文章啦。
什么是RMI
RMI全称为Remote Method Invocation(远程方法调用),是让某个Java虚拟机上的对象调⽤另⼀个Java虚拟机中对象上的⽅法(也即客户端如何调用服务器端的方法)。
RMI和RPC(远程进程调用,Remote procedure call)其实想实现的目标是一致的,只不过RPC更多的只是强调一种技术概念,因此可以说RMI是RPC在Java上的一种具体实现
RMI实现
RMI中主要有以下三个角色:
- Client(客户端):客户端通过查询注册表来获取对应名称的对象引用,以及该对象实现的接口
- Server(服务端):运行方法,执行方法结束后向客户端返回执行结果
- Registry(注册中心):服务实例将被注册表注册到特定的名称中,在低版本的JDK中,Server与Registry是可以不在一台机器上的,而在高版本的JDK(JDK8u141之后)中,Server与Registry只能在一台机器上,否则无法注册成功(感觉功能有点像DNS服务器,提供一个别名的中介功能)
Server端实现
声明包含可调用的方法的接口
首先Server需要声明一个接口,这个接口中声明了Client能够调用的服务端的方法:
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface RMIinterface extends Remote {
String test() throws RemoteException;
}
这个接口有以下几个需要注意的地方:
1、使用public修饰,否则Client在尝试加载实现远程接口的远程对象时会出错(Client和Server位于同一机器上时可不用)。
2、需要继承Remote类。
3、接口声明的方法需要声明RemoteException报错。
实现接口
接下来Server需要实现该接口:
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends UnicastRemoteObject implements RMIinterface{
protected Server() throws RemoteException {
}
@Override
public String test() throws RemoteException {
System.out.println("Server:Hello!");
return "Hello!";
}
}
实现类有几个需要注意的地方:
1、需要继承UnicastRemoteObject
继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法)
2、实现类的构造方法需要声明RemoteException报错,这是由于其父类UnicastRemoteObject的几个构造方法声明了RemoteException报错,因此其子类也需要显式声明RemoteException。(如果使用IDEA的话可以很方便的自动生成)
不少文章在这里都还显示的去使用了super()
去调用了父类的构造方法,但在这由于类的继承关系,即使我们不去显式调用super()
,子类也会去自动父类的无参构造方法,因此在此实现中我们直接空开构造方法。
3、实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
Registry端实现
Registry需要注册远程对象,其中又具体分为三步:
1、创建远程对象
2、创建Registry对象
3、将远程对象注册到Registry中
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Registry {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
Server server = new Server();//创建远程对象
Registry registry = LocateRegistry.createRegistry(1099);//创建registry对象
registry.bind("test",server);//将远程对象注册到注册表里面,并且设置值为test
}
}
其中最后一步设置值的指定,完整表示形式是rmi://ip:port/Objectname
,即应该是rmi://127.0.0.1:1099/test
,但rmi可以省去,即省为//127.0.0.1:1099/test
,当端口为默认的1099时,前面部分又可完全省去只剩下Object名。
而当我们不想自定义端口等操作时,我们可以省去创建registry对象的过程,换成使用java.rmi.Naming提供的静态方法:
Server server = new Server();
testNaming.bind("test",server);
由于在高版本里面Server和Registry位于同个机器上,因此我们也可以将其合并进行结合
import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class Server extends UnicastRemoteObject implements RMIinterface {
protected Server() throws RemoteException {
}
@Override
public String test() throws RemoteException {
System.out.println("Server:Hello!");
return "Hello!";
}
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException {
Server server = new Server();
Naming.bind("test",server);
}
}
Client实现
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1"); //获取Registry对象
RMIinterface test = (RMIinterface) registry.lookup("test"); //通过别名获取远程对象
String result = test.test(); //执行Server上的远程方法
System.out.println(result);
}
}
有以下几点值得注意的点:
1、通过LocateRegistry获取Registry对象的步骤中,如果端口不是默认的1099,则要调用getRegistry的另一种重载形式来指定端口:
Registry registry = LocateRegistry.getRegistry("127.0.0.1",2000);
2、通过lookup方法获取远程对象返回的是一个Remote接口,但其实我们真正想利用的是RMIinterface这种接口类型,因此需要对返回值进行一次向下转型。可以看到这也是采用接口定义的方便之处,即Client其实不关心Server上方法的具体写法只在意结果,但同时Client又需要知道有哪些方法可以调用以及参数类型,接口就很好的实现这两个目标。
3、也可以直接利用Naming的静态方法直接得到远程对象RMIinterface test = (RMIinterface) Naming.lookup("test");
其中的test完整表达形式仍为之前提过的:rmi://ip:port/Objectname
只不过在本地服务器且端口为1099的情况下可以只传参别名。
通信过程
这部分我内容我没有去实际进行Wireshark抓包进行实践,直接总结各位师傅们的经验
- 首先Client会和Registry进行TCP三次握手,建立连接
- Client向Registry发出一个Call请求(根据别名来获取远程对象),然后Registry会返回一个ReturnData,ReturnData中会包含Server的IP地址、端口(端口号就紧接着IP,两个字节)和序列化后的远程对象(\xAC\xED开始就是序列化的标志)
- Client反序列化远程对象并通过Server的IP和端口号访问Server进行远程方法调用
信息泄露
假如Client可以访问到Registry,那能想象到几种可行的攻击方式,比如说最容易被人想到的绑定恶意远程对象,但是实际上是不可行的,Registry的bind,rebind,unbind方法只能由localhost调用。外部的client只能调用registry对象的list和lookup方法。其中list方法被用来列出远程对象所拥有的方法:
String[] method = Naming.list("test");
for (String s : method) {
System.out.println(s);
}
输出:
//:1099/test
假设远程对象存在恶意方法,我们可以通过list方法列出所有可能的恶意方法然后进行远程调用
Codebase安全问题
codebase就是远程加载类的路径,当对象在发送序列化的数据的时候会带上codebase信息,当接受方在本地classpath中没有找到类的话,就会去codebase所指向的地址加载类。
如果我们指定 codebase=http://example.com/,然后加载 org.vulhub.example.Example 类,则 Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为 Example类的字节码。RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就会去寻 找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在 本地没有找到这个类,就会去远程加载codebase中的类。
那很自然的,我们想到,RMI中我们已经可控codebase,不就可以恶意加载类了么,但是要完成这一攻击对Server有两个前置要求:
- 安装并配置了SecurityManager
- Java版本低于7u21、6u45,或者设置了
java.rmi.server.useCodebaseOnly=false
java.rmi.server.useCodebaseOnly=true
时Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取,在这两个版本之后,默认值由false改为了true
我们可以通过设置VM选项为:-Djava.rmi.server.codebase=http://myserver.com/classes/
进行codebase的设置,实际中该利用的条件较为苛刻,通常难以遇到。
最后
这应该只是对RMI流程和最浅显的安全问题的一个总结,RMI最可能被人利用的反序列化问题由于我还未涉足所以暂且跳过,等未来学到相关知识再来进行完善。
参考资料
P神的Java安全漫谈4-6